Componentes
5. Número
seed-componentes.md v1.43 · §05seção 7 de 9805-numero-estavel-validado-pelo-rafael-em-2026-08.md · MD5 62b548f4Título completo no canon: Número — estável · validado pelo Rafael em 2026-08-02 (v0.9)
Consome form-field (§2) e input (§3). Documenta o que é do número: a proibição do
type="number", as duas variantes (livre e stepper), a localização de decimais na base, o dinheiro em centavos inteiros e a acessibilidade de spinbutton.Base de evidência: 3 rodadas (2026-08-02): dossiê GOV.UK contra type=number (+ MDN/espec HTML), NN/g steppers, Setproduct, Carbon NumberInput (issue #7819) e SAP UI5 (#1637) — os dois gigantes quebrados em separador decimal, React Aria NumberField, Semrush Intergalactic · HMRC currency, uxpatterns.dev currency, Société Générale, Wise money input, Workday (84 locales), SAP Fiori Step Input · ISO 4217 minor units, Modern Treasury, dinero.js, W3C ARIA APG spinbutton (+issues #704/#797), apps de pagamento (padrão observado e descartado). Decisões E1–E9 aprovadas pelo Rafael em 2026-08-02.
5.1 Papel, variantes e fronteiras#
número-livre |
número-stepper |
|
|---|---|---|
| Emprego | Medições e valores digitados: kWh, kWp, kVA, demanda, R$, % | Quantidades pequenas de ajuste fino: módulos, parcelas, itens, licenças |
| Régua (Fiori) | valor que raramente muda ou não segue passos | ajuste em incrementos definidos é o uso dominante |
| Stepper | NÃO tem | +/− nas laterais (horizontal — NN/g: precisão do dedo no toque) |
Nunca número para o que não é quantidade (Fiori + espec HTML): CEP, telefone, UC, IDs → input mascarado (§2.7). Datas/horas → date picker (item futuro).
5.2 As 9 decisões (E1–E9, aprovadas 2026-08-02)#
| # | Decisão | Porquê | Descartado |
|---|---|---|---|
| E1 | type="number" PROIBIDO no DS — sempre type="text" + inputmode numeric/decimal |
Dossiê GOV.UK: não ditável (Dragon), sem rótulo no NVDA, spin button confuso; scroll acidental MUDA O VALOR; 16+ dígitos arredondam silenciosamente (corrupção de dado); grandes viram exponencial irreversível; pattern não funciona; espec HTML: number só p/ incrementável |
type=number "porque é número" (o erro nº 1 da web em campos numéricos) |
| E2 | Duas variantes; stepper com regras Fiori/Setproduct: min/max DESABILITA botão e seta correspondente (nunca esconde), sem layout shift 9→10, precision restringe na digitação, Shift+seta = passo 2×, valor inválido PERMANECE no campo com erro | Fiori Step Input (spec mais completa achada) + NN/g (horizontal no toque, +/− como rótulos, Fitts) + Baymard (nunca apagar entrada) | Stepper universal (ruído em medição digitada); vertical minúsculo nativo; apagar valor inválido |
| E3 | Localização na base: aceita vírgula E ponto na digitação; exibe no locale (1.234,56); canônico com ponto decimal | Carbon #7819 (rejeita vírgula) e SAP UI5 #1637 (ponto ESVAZIA o campo): IBM e SAP quebrados em sentidos opostos — quem não resolve na base sofre em issue pública; HMRC normatiza a tolerância (símbolo, espaços, separadores; máx 2 decimais em moeda) | Separador único rígido; resolver por produto (patch infinito) |
| E4 | Motor de implementação: React Aria NumberField com visual SEED | Parsing por locale em 30+ idiomas e sistemas de numeração, valida a digitação no locale, clamp/step, teclado mobile automático — headless, casa com shadcn; não reinventar parser | Parser próprio (reinventar o que a Adobe mantém); Intl.NumberFormat só na exibição (não cobre parsing de entrada) |
| E5 | Herdadas explícitas: JetBrains Mono + alinhado à direita em formulário de edição/tabela (§3/Fiori/SocGen); unidade no sufixo E no rótulo (C1); larguras xs/sm (C4) | Consistência com decisões já estáveis | — |
| E6 | Moeda: formatar SÓ no blur; valor cru no focus; atalhos opcionais k/m expandem milhares/milhões (ERP power-user: "2k" → 2.000) | uxpatterns: reformatar durante a digitação move o cursor; SocGen: velocidade p/ quem digita valores o dia todo | Live-format a cada tecla (guerra de cursor — a armadilha BR do §2.7 em dobro) |
| E7 | Dinheiro NUNCA em float: canônico = centavos INTEIROS + código da moeda (ISO 4217 minor units; BRL expoente 2: R$ 12,34 → {centavos: 1234, moeda: 'BRL'}) |
Modern Treasury (infra de pagamentos) armazena tudo em minor units; dinero.js LANÇA EXCEÇÃO em float por princípio; aritmética binária corrói centavos | Float/decimal no front (corrupção silenciosa em soma de parcelas de proposta) |
| E8 | Stepper acessível padrão APG: input role="spinbutton" + aria-valuemin/max/now; botões +/− com tabindex="-1" (fora do tab — redundantes com as setas — mas operáveis por mouse e TalkBack/swipe); entrada inválida: valor permanece + aria-invalid (nunca aria-valuenow inventado) |
W3C APG spinbutton + issues #704/#797; TalkBack incrementa por swipe vertical | Botões no tab order (2 paradas mortas por campo em formulário denso); zerar no inválido |
| E9 | Caixa-registradora (centavos-first dos apps de pagamento) DESCARTADA como padrão — registrada como candidata futura SE algum produto SEED tiver fluxo de pagamento | Prática observável (Nubank/Venmo/Cash App) SEM guideline primária publicada; contexto é transferência rápida em teclado próprio de fintech — o nosso é valor de proposta/fatura em formulário e ERP | Adotar por moda (padrão sem função no nosso contexto) |
5.3 Contrato HTML e parsing#
type="text" + inputmode="decimal" (com decimais) ou "numeric" (inteiros) + autocapitalize/autocorrect/spellcheck off + enterkeyhint conforme posição (§2.6). Parsing tolerante: 1.234,56 · 1234,56 · 1234.56 · R$ 1.234,56 · 2k (com atalho ativo) → todos viram canônico 1234.56 (ou {centavos:123456, moeda:'BRL'} em dinheiro). Regra de desambiguação de separador único (v0.9, bug pego em auto-teste antes da entrega): com vírgula E ponto presentes, o ÚLTIMO é o decimal; com um separador só, o padrão 1.250 / 12.345.678 (grupos exatos de 3) lê como MILHAR e 1.2 / 1.25 como decimal — sem a regra, "1.250" de kWh viraria 1,25 (erro de 1000×). Ambiguidade residual honesta: 1.250 que o usuário QUIS como decimal de 3 casas é raro em nossos domínios (kWh/R$ usam 2 casas) e o eco formatado no blur dá a chance de correção. Exibição: Intl.NumberFormat('pt-BR') (ou locale do produto — Workday sustenta 84; nós nascemos com pt-BR e en-US pelos clientes EUA).
5.4 Microcopy#
Rótulo carrega a unidade: "Consumo médio mensal (kWh)" · texto auxiliar ensina o formato SÓ quando ambíguo: "Use vírgula para decimais — ex.: 1.250,75" (prevenção Animalia) · erro de faixa diz os limites: "Informe entre 1 e 120 parcelas" (regra W3C: min/max invisíveis são jogo de adivinhação).
5.5 Código — essência (HTML/CSS delta + React)#
<!-- número-livre (medição) -->
<input class="seed-field__control seed-input__control--num" type="text" inputmode="decimal"
autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="next">
<!-- número-stepper -->
<div class="seed-stepper" role="group">
<button type="button" tabindex="-1" aria-label="Diminuir" class="seed-stepper__btn">−</button>
<input class="seed-field__control seed-stepper__value" type="text" inputmode="numeric"
role="spinbutton" aria-valuemin="1" aria-valuemax="120" aria-valuenow="12">
<button type="button" tabindex="-1" aria-label="Aumentar" class="seed-stepper__btn">+</button>
</div>
.seed-stepper{display:flex;align-items:stretch;border:1px solid var(--seed-field-border);
border-radius:var(--seed-field-radius);overflow:hidden;width:max-content}
.seed-stepper:focus-within{box-shadow:var(--seed-focus-ring)}
.seed-stepper__btn{width:44px;border:0;background:var(--seed-surface-sunken);
color:var(--seed-text-primary);font-size:18px;cursor:pointer}
.seed-stepper__btn:disabled{color:var(--seed-text-disabled);cursor:not-allowed}
.seed-stepper__value{border:0;width:72px;text-align:center;font-family:var(--seed-font-mono)}
/* Motor: React Aria (E4). Esqueleto de consumo com visual SEED: */
import { NumberField, Group, Input, Button, Label } from "react-aria-components";
export function SeedNumberStepper(props: { label: string; min?: number; max?: number;
step?: number; value?: number; onChange?: (v: number) => void }) {
return (
<NumberField minValue={props.min} maxValue={props.max} step={props.step ?? 1}
value={props.value} onChange={props.onChange}
formatOptions={{ maximumFractionDigits: 0 }}> {/* locale-aware: pt-BR/en-US */}
<Label className="text-sm font-semibold text-[var(--seed-field-label)]">{props.label}</Label>
<Group className="flex w-max items-stretch overflow-hidden rounded-md border border-[var(--seed-field-border)] focus-within:ring-2 focus-within:ring-[var(--seed-border-focus)]">
<Button slot="decrement" excludeFromTabOrder aria-label="Diminuir"
className="w-11 bg-[var(--seed-surface-sunken)] disabled:text-[var(--seed-text-disabled)]">−</Button>
<Input className="w-20 border-0 bg-transparent text-center font-mono text-sm outline-none" />
<Button slot="increment" excludeFromTabOrder aria-label="Aumentar"
className="w-11 bg-[var(--seed-surface-sunken)] disabled:text-[var(--seed-text-disabled)]">+</Button>
</Group>
</NumberField>
);
}
/* Moeda (E6+E7): <NumberField formatOptions={{style:'currency',currency:'BRL'}}> — React Aria
formata no blur e edita cru; canônico emitido: Math.round(value*100) centavos + 'BRL'.
Somas/operações com dinero.js — nunca float. */
5.6 Aplicação dos 7 testes (§1.12)#
- Container 3:1: herda; stepper com borda 4.74/4.50. ✅ 2. Mecanismos: variantes diferem por ESTRUTURA (botões), não por tom; min/max desabilita (fundo+cor+cursor). ✅ 3. Grayscale: +/− e desabilitado legíveis sem matiz. ✅ 4. Par: livre × stepper inconfundíveis. ✅ 5. Regra do um: um passo por clique; um valor por campo. ✅ 6. Estado atual: valor sempre visível; limites comunicados no erro/ajuda. ✅ 7. Polegar 360px: botões 44px nas LATERAIS (horizontal NN/g), fonte 16px, teclado decimal correto. ✅
Também cita o §5: banco-credencial, tela-autenticacao.