Componentes
Diálogos & confirmação
Dialog
| Peça | Receita | Nota |
|---|---|---|
| Painel | max-w-md · max-h-[85dvh] overflow-y-auto · rounded-xl border bg-surface p-6 shadow-md ring-1 ring-foreground/5 | Mesma receita do DropdownMenu: o que flutua tem a mesma pele. |
| Largura no celular | w-[calc(100%-2rem)] | 16px de respiro de cada lado, sempre. |
| Entrada | animate-popup-in + motion-reduce:animate-none | 100ms de fade + zoom. |
| Overlay | bg-(--scrim) | Escurece SEM animação (estados calmos). No escuro o véu sobe pra 65%, porque tinta sobre fundo escuro não escurece nada. |
| Título | text-base font-semibold | DialogTitle é obrigatório: é o nome acessível do modal. |
| Descrição | mt-1 text-sm text-fg-muted | Uma frase. Se precisa de duas, repense a tarefa. |
| Fechar | botão redondo no canto, size-8, focus-ring | Além do Esc e do clique fora, que o Radix já entrega. |
| Botões | flex-col-reverse gap-2 sm:flex-row sm:justify-end | No celular empilham com a ação principal EMBAIXO, ao alcance do polegar. |
<Dialog open={aberto} onOpenChange={setAberto}>
<DialogTrigger asChild><Button variant="outline">Abrir</Button></DialogTrigger>
<DialogContent>
<DialogTitle>Novo agendamento</DialogTitle>
<DialogDescription>O que este diálogo faz, em uma frase.</DialogDescription>
{/* corpo */}
</DialogContent>
</Dialog>Armadilha já pagaRemonte com key quando fechar por código
onOpenChange: o estado da última abertura vaza pra próxima. Se o conteúdo depende do que estava aberto, use uma key por abertura.ConfirmDialog
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
| open / onOpenChange | boolean / (b) => void | — | Controlado pelo chamador. |
| title | ReactNode | — | Pergunta com o objeto dentro: “Remover o número (11) 91234-5678?”. |
| description | ReactNode | — | O que acontece, e o que NÃO acontece. É aqui que se diz o que sobrevive. |
| confirmLabel | string | — | Verbo + objeto: “Remover número”. Nunca “OK”. |
| cancelLabel | string | "Cancelar" | Raramente muda. |
| onConfirm | () => void | — | O diálogo fecha sozinho depois. |
| destructive | boolean | false | Pinta o botão de confirmar de vermelho. Só quando algo some. |
A tarja de confirmação
O gesto muda de lugar
Dois cliques no MESMO ponto não são confirmação: o dedo repete o gesto sem ler. Aqui o segundo clique é noutro lugar, com outro rótulo.
A consequência fica escrita
sentence diz o que some, em uma frase, no idioma do dono da clínica: “Sai das 5 fichas que usam este insumo e o custo delas cai.”
O foco abre no Cancelar
A saída segura é o padrão, e quem usa leitor de tela ouve a consequência (aria-describedby) antes de decidir.
Esc cancela
Como em qualquer diálogo — e trava enquanto pending: cancelar o que já foi enviado não desfaz nada.
A resposta desarma
useConfirm(state) escuta o retorno da action: uma exclusão RECUSADA fechava a pergunta antes, e cada clique novo repetia a recusa sem mudar nada na tela.
Âmbar pra quem apaga, verde pra quem só grava
tone="default" existe pro gesto que não destrói (aplicar preço): a mesma tarja em verde suave, sem o vermelho — o âmbar ali mentia o peso.
const confirmar = useConfirm(state); // state = retorno da action
<ConfirmTrigger label="Excluir" armed={confirmar.armed} onArm={confirmar.arm}
returnFocus={confirmar.returnFocus} ariaLabel={`Excluir ${nome}`} />
{confirmar.armed && (
<ConfirmStrip
sentence="O que some, em uma frase."
confirmLabel="Excluir insumo" busyLabel="Excluindo…"
pending={pending} action={excluirAction} fields={{ id }}
onCancel={confirmar.disarm}
/>
)}Armadilha já pagaO bug que matou o duplo clique
type="submit" dentro do próprio onClick submete NAQUELE clique: o React libera o setState de evento discreto antes de o navegador executar a ação padrão. A confirmação de dois cliques existia no código e nunca na tela — toda ação destrutiva de linha excluía no primeiro clique. Botão fica type="button" sempre, e o envio é explícito.Qual dos três
| Situação | Peça | Por quê |
|---|---|---|
| Criar/editar algo curto a partir de um botão de tela | Dialog | A tarefa precisa de foco, e a tela por trás continua sendo o contexto. |
| Ação destrutiva a partir de um botão de tela ou de menu | ConfirmDialog | Interromper é o ponto: a pessoa precisa parar antes de destruir. |
| Ação destrutiva numa LINHA de lista | ConfirmStrip | Modal por linha é pesado, e o duplo clique não confirma nada. |
| Ação que só grava (aplicar preço sugerido) | ConfirmStrip tone="default" | Nada some — o vermelho e o âmbar mentiriam o peso. |
| Formulário longo | Tela ou gaveta, nunca modal | Modal com rolagem interna esconde metade do que se está preenchendo. |
Sim
Excluir o cadastro de Marina Alvez?
O histórico de atendimento permanece, mas ela sai das listas ativas. Essa ação não tem volta.
Diz o que some, o que fica, e que não tem volta. Nomeia o objeto.
Não
Você tem certeza?
Esta ação não pode ser desfeita.
Certeza de quê? Sem o objeto e sem a consequência, a pergunta só treina o dedo a clicar em “Sim”.
O padrão de foco em camadas não entra em diálogo nem em gaveta: os dois já vêm com véu próprio, e empilhar os dois dá dois escurecimentos brigando.