Ir ao conteúdo
SEED engenharia

Componentes

5. Número

estávelseed-componentes.md v2.00 · §05seção 11 de 18405-numero-estavel-validado-pelo-rafael-em-2026-08.md · MD5 d78d1349

Título completo no canon: Número — estável · validado pelo Rafael em 2026-08-02 (v0.9)

Em uma frase: Nunca type="number": é type="text" com inputmode, parsing tolerante de vírgula e ponto, e dinheiro em centavos inteiros.

Vocabulário desta seção: stepper

O que esta seção NÃO cobre: CEP, telefone, unidade consumidora e identificadores não são número: vão em input mascarado. — se é disto que você precisa, não preencha em silêncio: gere o pedido de cobertura (https://ds.seed.eng.br/pedidos/).

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.

(decisão registrada — ver Histórico da seção)

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 10 decisões (E1–E9 aprovadas 2026-08-02 · E10 aplicada em 2026-10-10)#

# 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 SIGA Adotar por moda (padrão sem função no nosso contexto)
E10 O stepper segue a MESMA regra de sobrevoo do campo (PR-P134, 2026-10-10 — veredito dele, verbatim: "a" à pergunta "o canon não escreve o sobrevoo do stepper. (a) mesma regra do campo"): a borda de sobrevoo --seed-field-border-hover vai no INVÓLUCRO .seed-stepper (a caixa de borda própria, 4,74:1) — nunca nos botões +/−, que têm o fundo de sobrevoo deles —, só sob ponteiro (@media (hover:hover), MH1) e não com o foco dentro (o anel de foco manda); e o erro vence o sobrevoo: com aria-invalid="true" no <input> interno o invólucro mostra --seed-field-border-error e o sobrevoo não o troca (§2.4) O stepper é um campo de borda própria: se o canon do campo responde ao ponteiro e o stepper não, o mesmo gesto dá duas respostas na mesma tela. A exclusão do erro é por seletor (:has([aria-invalid="true"])), pela mesma razão do §2.4 — a vitória não depende de ordem nem de especificidade. A bancada já respondia ao ponteiro desde a PR-P124; o que faltava era o canon dizer e o erro marcar o invólucro (antes, aria-invalid no <input> interno não pintava nada no invólucro) Sobrevoo só nos botões (a caixa, que é o campo, ficaria muda) · sobrevoo que troca a borda de erro (o erro some sob o ponteiro, medido no .seed-field__control)

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:has([aria-invalid="true"]){border-color:var(--seed-field-border-error)}   /* o erro marca o invólucro (E10) */
@media (hover:hover){.seed-stepper:hover:not(:focus-within):not(:has([aria-invalid="true"])){border-color:var(--seed-field-border-hover)}}   /* E10: a regra do campo; o erro vence o sobrevoo */
.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)#

  1. 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. ✅
BancadaSEED DS v2 — Bloco 2 · Preview v0.13 (13/13)abrir em página própria ↗

Também cita o §5: banco-credencial, tela-atividade, tela-autenticacao, tela-busca, tela-configuracoes, tela-onboarding, tela-painel, tela-site.

Esc