---
fonte: 01-canonicos/seed-componentes.md
versao_da_fonte: v1.43
secao: 03
titulo: "Input de texto — `estável` · validado pelo Rafael em 2026-08-02 (v0.7)"
sequencia: 5 de 98
bytes_do_corpo: 17799
md5_do_corpo: 0620d3759fdee563caff710c4b72740a
gerado_por: 06-validacao/geradores/gen-camada-ia.py
nota: fatia GERADA — o corpo abaixo é byte a byte o trecho do canônico; edite o canônico, nunca esta fatia. Canônico inteiro em https://ds.seed.eng.br/01-canonicos/seed-componentes.md
---
## 3. Input de texto — `estável` · validado pelo Rafael em 2026-08-02 (v0.7)

> **Primeiro consumidor concreto do form-field (§2):** herda integralmente moldura, 9 estados, tokens, contrato HTML, máscaras (§2.7), microcopy (§2.8) e a11y (§2.9) — esta seção documenta APENAS o que é do input: os slots internos do controle, as decisões C1–C4, a subvariante senha (normativa), a escala de larguras e os utilitários prometidos na §2.12 (hook de validação B4 + registry de máscaras de referência).
>
> **Base de evidência:** 3 rodadas próprias (2026-08-02, somadas às 4 do form-field): Carbon, GOV.UK, USWDS, GC Canada, Vaadin, Designsystemet (NO), M3, NN/g, WebAIM/ONS/Scott O'Hara (rodada 1) · Ant Design, Salesforce Lightning, SAP Fiori input, Uber Base Web (rodada 2) · **WCAG 2.2 SC 3.3.8/3.3.7, NIST SP 800-63B rev.4, Animalia DS (BR), TOTVS PO UI (BR), Apple HIG** (rodada 3). Decisões C1–C5 + adições normativas aprovadas pelo Rafael em 2026-08-02 (C5 emendada no §2).

### 3.1 Papel e fronteiras (quando NÃO usar input)

Entrada livre de linha única. **Escolha vence digitação** (Apple HIG): se as respostas possíveis são conhecidas, use um controle de escolha. Régua de volume — **ATUALIZADA na v0.11 pela régua unificada da família de escolha (§7.1, decisão G1)**: 2–6 → radio/checkbox · 7–15 (teto ~20 no ERP denso) → select · acima, ou com busca → combobox · aberto de fato → input com sugestões. (A versão v0.7 desta linha citava só o corte Fiori de ~20; a régua do §7.1 a harmoniza com NN/g, USWDS, M3 e CMS.) Texto longo/multilinha → `textarea`. Busca → item próprio (padrões de teclado e clear diferentes).

### 3.2 Anatomia do controle — slots internos

```
┌──────────────────────────────────────────────────────┐
│ [ícone] [prefixo]  valor digitado   [sufixo] [✕] [⟳] │
└──────────────────────────────────────────────────────┘
   ①        ②            ③               ④      ⑤    ⑥
```

| # | Slot | Regras |
|---|---|---|
| ① | Ícone leading 20px | Decorativo/categórico (`aria-hidden`); nunca portador único de significado |
| ② | Prefixo | Símbolo/texto fixo NÃO editável (R$, +55, https://). Cor `--seed-field-help-text` |
| ③ | Valor | Herda §2. Dado de medição/identificador: JetBrains Mono, **alinhado à direita quando numérico em formulário de edição** (SAP Fiori — colunas de valores comparáveis no ERP) |
| ④ | Sufixo | Unidade/domínio (kWh, kW, %, @seed.com.br). Mesma cor do prefixo |
| ⑤ | Clear (✕) | Botão real 32×32px com hit-area estendida à altura do campo (≥24px WCAG 2.5.8, folga); ver C2 |
| ⑥ | Loading | Spinner 16px + `aria-busy` + texto assistivo "Consultando…" (padrão SLDS); substitui ⑤ durante a operação |

**Regra dura de DOM estável (armadilha Ant Design, bug real documentado):** os slots existem SEMPRE na árvore (vazios quando inativos) — adicionar/remover prefixo/sufixo/clear dinamicamente recria a estrutura e o input **perde o foco no meio da digitação**.

**Overflow:** valor maior que o campo rola horizontalmente (nativo); em desktop, `title` com o valor completo (tooltip de expansão — Apple HIG). Nunca truncar o dado armazenado.

### 3.3 Decisões C1–C4 (aprovadas 2026-08-02; C5 → emenda §2)

| # | Decisão | Porquê (fontes) | Descartado |
|---|---|---|---|
| C1 | Prefixo/sufixo internos para unidade/símbolo; **valor contém só o dado** | USWDS: unidade nunca dentro do value (quebra canônico e máscara); M3: prefixo=moeda, sufixo=unidade/domínio; não usar afixo em campo de resposta aberta | Unidade digitada no valor; afixo externo ao campo (Fiori — frágil em coluna estreita mobile); afixo como substituto de rótulo (Vaadin proíbe) |
| C2 | Clear como slot opcional, restrito: padrão em busca/filtros; opcional nos demais; NUNCA em dado crítico já validado | Carbon: aparece com conteúdo, tab stop, Esc limpa; WebAIM/WCAG: se existe p/ mouse, DEVE ser operável por teclado; Scott O'Hara: some em readonly/disabled, foco RETORNA ao campo | Clear universal (Ant permite — no ERP vira perda acidental); ✕ só-mouse decorativo (falha WCAG A) |
| C3 | Senha: subvariante normativa — ver §3.5 | WCAG 2.2 SC 3.3.8 (AA) + NIST SP 800-63B: virou norma, não preferência | — |
| C4 | Largura comunica o comprimento esperado: escala `xs·sm·md·full`, aplicada PELO LAYOUT, consistente POR GRUPO | Carbon: proporcional ao conteúdo; GC Canada: fixa p/ comprimento conhecido, ≤75 chars; Apple: larguras consistentes por grupo de campos; Fiori: aplicar via layout do form, não hardcode no componente | Tudo full-width (mente sobre o dado; padrão preguiçoso); largura campo-a-campo isolada (poluição visual — Apple) |

**Escala de larguras (aplicada via layout):** `xs` 120px (CEP, UC, sigla) · `sm` 200px (CPF, telefone, data) · `md` 320px (nome, e-mail) · `full` (endereço, campo dominante). Em viewport <640px, todas viram `full` em coluna única (fundamento §0.8).

### 3.4 Tipos cobertos (herdam o contrato §2.6)

text · email · tel · password · url + os mascarados do registry §2.7 (CPF, CNPJ, CPF/CNPJ, CEP+ViaCEP, telefones BR/0800/internacional, moeda, data). `search` e `number` são itens próprios do bloco.

### 3.5 Subvariante senha — NORMATIVA (WCAG 2.2 SC 3.3.8 AA + NIST SP 800-63B)

| Regra | Norma/fonte |
|---|---|
| **NUNCA bloquear colar/autofill** (senha e código de verificação) | WCAG 3.3.8 (técnica suficiente) + NIST "SHALL NOT prevent paste" — gerenciador de senha é aliado |
| `autocomplete="current-password"` / `"new-password"` sempre; nunca `off` em login | WCAG 3.3.8 (quebrar gerenciador = falha AA) |
| Toggle mostrar/ocultar: **botão semântico** 32px (hit-area = altura do campo), ícone olho, `aria-pressed`, `aria-label` "Mostrar senha" | NN/g (desmascarar apoia memória e conferência); auditoria: 9 em 10 sites usam div/anchor — o erro clássico |
| Mascarado por padrão em QUALQUER superfície (ERP roda em escritório aberto) | NN/g + política SEED |
| Nunca pré-popular campo de senha | Apple HIG |
| Sem "confirmar senha/e-mail" por redigitação | WCAG 3.3.7 Redundant Entry |
| Campo suporta 64+ caracteres, Unicode e espaços | NIST rev.4 |
| **Fronteira:** política de senha (mínimo 8/15, blocklist de vazadas, sem composição forçada, sem expiração periódica) pertence à feature de autenticação do produto — o componente garante o SUPORTE | NIST SP 800-63B |

### 3.6 Estados, tokens e a11y — herdados com 3 adições

Estados: os 9 do §2.4 tal qual. Tokens novos: **nenhum de cor** — afixos e ícones usam `--seed-field-help-text`/`currentColor` (decisão: menos tokens = menos manutenção; se afixo precisar de ênfase própria um dia, nasce token com par e medição). A11y além do §2.9: ① afixos são `aria-hidden` e a INFORMAÇÃO vai no rótulo — "Consumo médio (kWh)" — porque leitores de tela não anunciam prefixo/sufixo (Designsystemet); ② clear e toggle são botões no tab order, com foco visível e retorno de foco ao campo após limpar; ③ botão interno: 32×32px visível com hit-area estendida à altura do campo (WCAG 2.5.8 pede ≥24 — folga; exceção documentada ao fundamento 44px por ser controle INTERNO de um alvo que já tem 44px).

### 3.7 Microcopy — 2 regras além do §2.8

1. **Prevenção > correção (Animalia DS):** se só UM erro é possível no campo, o texto auxiliar já ensina a evitá-lo ("Somente números") — melhor que esperar o erro.
2. **Afixo repetido no rótulo, sempre:** o sufixo visual "kWh" é conforto de leitura; a informação oficial mora no rótulo "(kWh)".

### 3.8 Código — HTML/CSS (delta sobre §2.11)

```html
<div class="seed-field" style="max-width:320px"><!-- 320px direto, e nao `var(--seed-input-w-md,320px)`: o token nunca existiu, e exemplo de codigo num canonico e codigo que alguem copia — citar nele um `var()` que nao resolve e ensinar o defeito. Corrigido em 2026-08-26. -->
  <label class="seed-field__label" for="consumo">Consumo médio mensal (kWh)</label>
  <div class="seed-input">
    <span class="seed-input__affix" aria-hidden="true"><!-- prefixo (vazio = DOM estável) --></span>
    <input class="seed-field__control seed-input__control seed-input__control--num"
           id="consumo" name="consumo" type="text" inputmode="decimal"
           autocapitalize="off" autocorrect="off" spellcheck="false" enterkeyhint="next">
    <span class="seed-input__affix" aria-hidden="true">kWh</span>
    <span class="seed-input__actions"><!-- clear/toggle/loading --></span>
  </div>
  <p class="seed-field__help">Está na sua fatura de energia.</p>
</div>
```

```css
.seed-input{display:flex;align-items:center;gap:8px;height:var(--seed-field-height-md);
  padding:0 12px;background:var(--seed-field-bg);
  border:1px solid var(--seed-field-border);border-radius:var(--seed-field-radius);
  transition:border-color var(--seed-dur-productive-fast) var(--seed-ease-productive)}
.seed-input:hover{border-color:var(--seed-field-border-hover)}
.seed-input:focus-within{box-shadow:var(--seed-focus-ring)}       /* foco no WRAPPER */
.seed-input .seed-field__control{border:0;height:100%;padding:0;flex:1;min-width:0;
  background:transparent;outline:none;box-shadow:none}
.seed-input__affix{color:var(--seed-field-help-text);font-size:14px;flex:none}
.seed-input__affix:empty{display:none}
.seed-input__control--num{font-family:var(--seed-font-mono);text-align:right}
.seed-input__actions{display:flex;align-items:center;flex:none}
.seed-input__btn{display:flex;align-items:center;justify-content:center;width:32px;height:100%;
  background:none;border:0;color:var(--seed-field-help-text);cursor:pointer;border-radius:4px}
.seed-input__btn:focus-visible{outline:none;box-shadow:var(--seed-focus-ring)}
.seed-input[data-invalid="true"]{border-color:var(--seed-field-border-error)}
.seed-input[data-warning="true"]{border-color:var(--seed-field-border-warning)}
.seed-input[data-valid="true"]{border-color:var(--seed-field-border-success)}
@media (pointer: coarse){.seed-input .seed-field__control{font-size:var(--seed-fs-field-touch)}}
```

### 3.9 Código — React/TypeScript (Input + hook B4 + registry de máscaras)

```tsx
/* ============ useFieldValidation — B4: reward early, punish late (Polaris/Konjević) ============ */
export function useFieldValidation(validate: (v: string) => string | null) {
  const [error, setError] = React.useState<string | null>(null);
  const [touched, setTouched] = React.useState(false);
  return {
    error, invalid: !!error, valid: touched && !error,
    onBlur: (e: React.FocusEvent<HTMLInputElement>) => {
      const v = e.target.value;
      if (v.trim() !== "") { setTouched(true); setError(validate(v)); }
      /* vazio + intocado: só marca no SUBMIT (Polaris) */
    },
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
      if (error) setError(validate(e.target.value)); /* punish late: revalida por tecla SÓ em erro */
    },
    validateNow: (v: string) => { setTouched(true); const r = validate(v); setError(r); return r; },
  };
}

/* ============ Registry de máscaras — B5 (implementação de referência) ============ */
type MaskDef = {
  format: (digits: string) => string;   /* apresentação */
  maxDigits: number;
  canonical?: (digits: string) => string; /* default: dígitos puros; telefone: E.164 */
};
const maskRegistry = new Map<string, MaskDef>();
export function registerMask(name: string, def: MaskDef) { maskRegistry.set(name, def); }
export function applyMask(name: string, rawValue: string) {
  const def = maskRegistry.get(name);
  if (!def) throw new Error(`Máscara não registrada: ${name}`);
  const digits = rawValue.replace(/\D+/g, "").slice(0, def.maxDigits); /* tolerante na entrada */
  return { display: def.format(digits), canonical: def.canonical?.(digits) ?? digits };
}
/* registro inicial — demais máscaras do §2.7 seguem o mesmo molde; telefone usa libphonenumber-js */
registerMask("cpf-cnpj", { maxDigits: 14, format: d => d.length <= 11
  ? d.replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d)/, "$1.$2").replace(/(\d{3})(\d{1,2})$/, "$1-$2")
  : d.replace(/^(\d{2})(\d)/, "$1.$2").replace(/^(\d{2})\.(\d{3})(\d)/, "$1.$2.$3")
     .replace(/\.(\d{3})(\d)/, ".$1/$2").replace(/(\d{4})(\d{1,2})$/, "$1-$2") });
registerMask("tel-0800", { maxDigits: 11,
  format: d => d.replace(/^(\d{4})(\d)/, "$1 $2").replace(/^(\d{4}) (\d{3})(\d{1,4})/, "$1 $2 $3") });
/* Nota de cursor (armadilha BR §2.7): ao formatar programaticamente, reposicionar o caret
   relativo aos DÍGITOS antes do cursor, não ao índice bruto da string. */

/* ============ Input — consome o form-field §2.12 ============ */
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
  prefix?: React.ReactNode; suffix?: React.ReactNode;
  clearable?: boolean; onClear?: () => void; loading?: boolean; warning?: boolean;
};
export function Input({ prefix, suffix, clearable, onClear, loading, warning, className, ...props }: InputProps) {
  const { id, helpId, errId, invalid, valid } = useField();
  const ref = React.useRef<HTMLInputElement>(null);
  const showClear = clearable && !loading && !props.readOnly && !props.disabled &&
    String(props.value ?? "").length > 0;
  return (
    <div data-invalid={invalid || undefined} data-warning={warning || undefined}
         data-valid={valid || undefined}
         className={cn("flex h-11 items-center gap-2 rounded-md border px-3",
           "bg-[var(--seed-field-bg)] border-[var(--seed-field-border)]",
           "hover:border-[var(--seed-field-border-hover)] focus-within:ring-2",
           "focus-within:ring-[var(--seed-border-focus)] focus-within:ring-offset-2",
           "data-[invalid]:border-[var(--seed-field-border-error)]",
           "data-[warning]:border-[var(--seed-field-border-warning)]",
           "data-[valid]:border-[var(--seed-field-border-success)]",
           "pointer-coarse:text-[16px]", className)}>
      {/* slots SEMPRE presentes — DOM estável (Ant) */}
      <span aria-hidden className="flex-none text-sm text-[var(--seed-field-help-text)] empty:hidden">{prefix}</span>
      <input ref={ref} id={id} aria-invalid={invalid || undefined}
        aria-describedby={cn(invalid && errId, helpId) || undefined}
        className="h-full min-w-0 flex-1 bg-transparent text-sm text-[var(--seed-field-value)] outline-none placeholder:text-[var(--seed-field-placeholder)]"
        {...props} />
      <span aria-hidden className="flex-none text-sm text-[var(--seed-field-help-text)] empty:hidden">{suffix}</span>
      <span className="flex flex-none items-center">
        {showClear && (
          <button type="button" aria-label="Limpar campo"
            className="flex h-full w-8 items-center justify-center rounded text-[var(--seed-field-help-text)] focus-visible:ring-2 focus-visible:ring-[var(--seed-border-focus)]"
            onClick={() => { onClear?.(); ref.current?.focus(); /* foco RETORNA (O'Hara) */ }}>
            <X className="size-4" aria-hidden />
          </button>
        )}
        {loading && <Loader2 className="size-4 animate-spin" aria-hidden />}
        {loading && <span className="sr-only" aria-live="polite">Consultando…</span>}
      </span>
    </div>
  );
}

/* PasswordInput: Input + toggle normativo (§3.5) — aria-pressed, nunca bloquear paste,
   autocomplete current-password/new-password, mascarado por padrão. */
```

### 3.10 Decisões de implementação deste item

| Decisão | Porquê | Descartado |
|---|---|---|
| Foco no WRAPPER (`focus-within`) | Com slots internos, o anel precisa envolver o CONJUNTO; input interno sem borda própria | Anel só no input interno (anel "dentro" do campo — quebra a leitura de container) |
| Slots sempre no DOM (`empty:hidden`) | Bug real Ant: recriar estrutura derruba o foco durante a digitação | Renderização condicional dos slots |
| Zero token de cor novo | Afixo/ícone = papel de ajuda já tokenizado; menos manutenção | `--seed-input-affix` dedicado (duplicaria help-text sem caso de uso divergente) |
| Numérico = mono + direita SÓ em formulário de edição/ERP | Fiori: colunas de valores comparáveis; em formulário público de 1 coluna, esquerda normal | Direita universal (estranho em campo isolado de landing) |
| Clear limpa e NÃO valida | Limpar é intenção de recomeço; erro imediato no campo vazio pune (B4) | Validar no clear (flash de erro) |

### 3.11 Aplicação dos 7 testes (§1.12)

1. **Container 3:1:** herda §2 (borda 4.74/4.50; warning 4.82/8.21 — medidos). ✅ 2. **Escada de mecanismos:** warning ≠ error por cor E ícone (▲ vs ⚠) E comportamento (não bloqueia); clear/loading são mecanismos, não tons. ✅ 3. **Grayscale:** ▲/⚠/✓ distinguem os três estados de validação sem matiz. ✅ 4. **Par:** campo com afixo × sem afixo — hierarquia valor>afixo óbvia (13.86 vs 6.55). ✅ 5. **Regra do um:** uma mensagem por campo; um estado de validação por vez (warning cede ao error). ✅ 6. **Estado atual:** senha mostrada/oculta legível pelo ícone + aria-pressed. ✅ 7. **Polegar (360px, teclado aberto):** campo full-width em coluna única, fonte 16px, clear com hit-area da altura do campo, afixos não colapsam (min-width:0 no input) — faixa 360px no preview. ✅

---

