Componentes
Campos de formulário
Field já costura rótulo, controle, dica e erro com as ligações de acessibilidade certas.Field — o que você usa
Esse nome aparece nas mensagens pros seus pacientes.
Número inválido. Use DDD + 9 dígitos.
Multi-linha é canto 16, não pílula.
<Field
name="telefone" // pesca errors["telefone"] sozinho
label="Telefone"
errors={state?.errors} // o objeto INTEIRO da server action
hint="Com DDD. É por aqui que o paciente recebe a confirmação."
/>
// id só quando precisa diferir do name (o mesmo form aberto várias vezes
// na página, com useId por instância).| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
| name | string | — | Obrigatório. É a chave do FormData e a chave que o campo pesca em errors. |
| label | ReactNode | — | Vira um <Label htmlFor> de verdade — clicar no texto foca o campo. |
| errors | Record<string, string[]> | — | O state.errors inteiro da action. O campo escolhe o próprio. |
| hint | ReactNode | — | Apoio entre o controle e o erro. Entra no aria-describedby junto com o erro. |
| after | ReactNode | — | Extra logo após o controle: um <datalist>, um apoio com cor própria. |
| className | string | — | Classe do WRAPPER (ex.: "sm:col-span-2"), não do controle. |
Armadilha já pagaErro de campo não é verdade permanente
Field apaga ao primeiro toque e reacende no submit do formulário. Não refaça isso por tela.Armadilha já pagaA <form action> do React 19 RESETA os campos ao responder
Inclusive quando a resposta é um erro de validação: a pessoa perde o que digitou. Sintoma vivido: criar um procedimento virou labirinto — digita o nome, salva, ele pede a duração; preenche a duração, salva, ele pede o nome que o primeiro erro já tinha apagado. E uma senha errada em /entrar limpava o e-mail já digitado.
A cura mora em components/keep-values.ts: useKeepValues() fotografa os campos no onSubmit e devolve como defaultValue. Chame keep.clear() no caminho de SUCESSO de todo formulário que continua montado, senão o próximo envio reposta os valores anteriores.
Texto é o caso fácil. Para <select> e para o Checkbox do Radix o React restaura a partir do valor de MONTAGEM — o estado do React guarda a escolha, o DOM volta pra primeira opção, e o próximo envio grava o valor velho. A primeira opção de papéis é administrador: errar o e-mail e reenviar o convite convidava como ADMIN sem ninguém tocar no campo. Para esses vão keepSelected / keepChecked / keepAll, e a cura fica no CHAMADOR, nunca dentro do wrapper.
Casos com anatomia própria — upload de imagem, conjunto de checkboxes, contador de caracteres — montam as três peças à mão. O Field é pro caso comum, que é a esmagadora maioria.
Input
| Detalhe | Valor | Por quê |
|---|---|---|
| Altura | h-9 · pointer-coarse:h-10 | 36px no mouse, 40px no dedo. |
| Forma | rounded-full | Campo de uma linha é coisa que se toca. |
| Padding | px-3.5 | A curva da pílula come espaço: menos que isso encosta o texto na borda. |
| Tamanho do texto | text-base md:text-sm | 16px no celular de propósito: abaixo disso o Safari dá zoom ao focar o campo. |
| Fundo | bg-surface | Nunca transparente — o campo precisa se separar do card. |
| Sombra | nenhuma | Borda 1px + halo de foco fazem o trabalho. |
| Seleção | selection:bg-primary selection:text-primary-foreground | O texto selecionado veste a marca. |
| Autofill | box-shadow interno + text-fill-color | O amarelo do Safari ignora background-color e acenderia no meio de um formulário escuro. |
Textarea, Select e Busca
valor: (vazio)
| Componente | Nota |
|---|---|
| SelectNative | Select do navegador, de propósito: no celular ele abre a roleta nativa. Dropdown custom só quando a UX pedir busca ou seleção múltipla — discrição > drama. E o color-scheme por tema é o que faz a lista aberta sair escura num tema escuro. |
| SearchInput | className é do INPUT; a LARGURA vai em wrapperClassName (flex-1, sm:max-w-xs). São dois alvos de estilo, então são dois nomes. |
| InputOTP | w-8 até 420px de viewport e w-10 acima: 6 slots + gaps cabem nos ~238px úteis de um card em tela de 320px. Colar limpa não-dígitos (sem isso, um espaço copiado do e-mail descartava o código inteiro em silêncio). |
| Textarea | Cuidado com o \r: o transporte multipart normaliza toda quebra de linha pra \r\n. Quem depende de \n precisa normalizar no schema E no consumidor. |
Checkbox
Armadilha já pagaO reset do React 19 não poupa o checkbox
<form action> retorna, o React reseta os controles — e o Radix volta ao valor de MONTAGEM e ainda dispara onCheckedChange(false). A cura mora no CHAMADOR (keepChecked/keepAll em components/keep-values.ts), nunca dentro do wrapper: curar no ui/checkbox.tsx foi aplicado e revertido no mesmo dia, porque a restauração passa por um toggle intermediário e callbacks não-idempotentes apagavam dados.Erro e apoio
| Peça | O que é | Marcação |
|---|---|---|
| FieldError | O primeiro erro de um campo, em 12px destructive. | id={`${inputId}-error`} role="alert" |
| FormAlert | O erro geral do formulário (state.message). Painel discreto em negative-soft — e ele rola até ficar visível quando surge. | role="alert" · scroll-mt-20 |
| PositiveBanner | A contraparte: estado bom, em positive-soft com escudo. | rounded-md px-3 py-2.5 |
| hint | Instrução do campo, 12px fg-subtle. | id={`${inputId}-hint`}, dentro do aria-describedby |
Sim
Telefone
Número inválido. Use DDD + 9 dígitos.
A mensagem diz o que fazer. O campo marca aria-invalid, e o texto está ligado a ele por aria-describedby.
Não
Telefone
Erro de validação: campo inválido (code 422).
Mensagem que descreve o sistema, não o problema. Quem lê não sabe o que corrigir — e o código HTTP não é assunto de quem marca consulta.
FormState.message significa erro: o FormAlert pinta de vermelho. Nunca devolva mensagem no caminho de sucesso — a copy de sucesso é da tela (um toast, um “Salvo ✓” na barra).
Layout de formulário
// Grade: uma coluna no celular, duas a partir de sm. <div className="grid gap-6 sm:grid-cols-2"> <Field name="nome" label="Nome" /> <Field name="telefone" label="Telefone" /> <TextareaField name="obs" label="Observação" className="sm:col-span-2" /> </div> // Dentro do Field: space-y-2 entre rótulo, controle, dica e erro. // Entre blocos de um formulário longo: space-y-phi-3.
O que anda junto
| Situação | Regra |
|---|---|
| Campos que se completam (CEP, rua, número) | Mesma linha quando couber; o CEP nunca sozinho numa linha inteira. |
| Campo largo (observação, endereço) | sm:col-span-2 — não espremer texto longo em meia largura. |
| Ações do formulário | Rodapé do bloco: Salvar (primária) + Cancelar (ghost). Em tela com barra, o Salvar SOBE pra barra sozinho. |
| Formulário sempre aberto | data-dim-solto: a elevação do foco entra por pseudo-elemento, sem tapete permanente. |
| Formulário que abre numa linha | Gaveta + EditSurface: a linha VIRA a ficha. |