Componentes
Vazio, carregando & erro
Vazio
Nenhum paciente ainda
Conecte o WhatsApp ou importe sua base pra começar a organizar a agenda.
Nenhum paciente com esse filtro
Tente outro termo ou volte pra lista completa.
| Prop | Tipo | Padrão | O que faz |
|---|---|---|---|
| icon | LucideIcon | — | Opcional. Com ícone, o gap sobe pra 4 e nasce o selo circular ink-100 de 56px; sem ícone, gap-3 (é a variante de filtro). |
| title | string | — | Uma frase curta, 16/500. |
| description | string | — | O próximo passo, em max-w-xs. Não descreve o vazio: aponta a saída. |
| children | ReactNode | — | As ações. Primária no onboarding, outline no filtro. |
| densidade | "tela" | "bloco" | — | tela (padrão) é o vazio de uma tela ou lista. bloco é o vazio DENTRO de um cartão (Ajustes, Precificação): título 14px, texto 12px, respiro phi-3, selo de ícone opcional. É o que o CrudEmptyState veste por baixo do CTA dele. |
| Peça | Receita |
|---|---|
| Moldura | rounded-xl border border-dashed border-border |
| Respiro | py-phi-4 (fluido) |
| Selo do ícone | size-14 rounded-full bg-ink-100 · ícone size-6 em fg-subtle |
| Texto | centrado, description em max-w-xs |
NotaA borda tracejada é semântica
Carregando
| Regra | Detalhe |
|---|---|
| O esqueleto imita a FORMA real | Campos são pílulas no app, então o esqueleto deles é rounded-full. Retângulo no lugar de pílula faz a tela “trocar de roupa” quando carrega. |
| Nunca um spinner no meio do nada | Spinner só dentro de um botão que está trabalhando (o ícone Loader2 do toast de loading). |
| aria-busy e role=status | A região carregando se anuncia: role="status" aria-busy="true" aria-label="Carregando". |
| loading.tsx embrulha a PAGE, nunca o layout do mesmo segmento | Um await no layout faz o boundary de cima piscar antes do palco aparecer. |
| Sem fade de entrada | O conteúdo entra no lugar do esqueleto. Fade duplicado faz a tela piscar. |
@utility skeleton { @apply animate-pulse bg-ink-100 motion-reduce:animate-none }
/* NÃO combinar com outro animate-* na mesma classe: utility composto sai antes
no CSS, então o animate-* de fora venceria o animation:none do reduced-motion. */
<div className="skeleton h-9 w-full rounded-full" /> ✓
<div className="skeleton animate-bounce" /> ✗Erro
Algo deu errado
A tela não conseguiu carregar. Tente de novo. Se continuar, recarregue a página.
código a1b2c3
Verificação em duas etapas ativa nesta conta.
| Altura | Peça | Comportamento |
|---|---|---|
| A rota falhou | RouteError (error.tsx) | Selo, copy calma, o digest do erro em algarismo tabular e um retry que faz router.refresh() + reset() (RouteErrorBoundary, o miolo dos três error.tsx). |
| O formulário recusou | FormAlert | role="alert" + rolagem até ficar visível quando o erro SURGE ou MUDA — em form longo no celular ele nascia acima da dobra e passava batido. |
| O campo está errado | FieldError | 12px destructive, ligado ao input por aria-describedby, e some ao primeiro toque. |
| Deu certo | toast.success / PositiveBanner | Toast pro que acabou de acontecer; banner pro estado que permanece. |
Sim
Diz o que houve e dá o caminho. Painel discreto, sem ícone de alarme, sem borda vermelha grossa.
Não
Vermelho cheio, caixa alta e vocabulário de servidor. Assusta e não ajuda: quem marca consulta não conhece código HTTP.
Toast
| Regra | Detalhe |
|---|---|
| Título curto + descrição opcional | “Consulta confirmada” + “Marina Alvez confirmou para amanhã, 14h.” O título não leva ponto nem exclamação; a frase inteira vai na descrição, com ponto. Erro fica de fora: costuma ser a frase que o servidor devolveu. |
| O tipo diz o que aconteceu | success = um gesto terminou e o servidor confirmou, inclusive desligar, arquivar e excluir. info = aviso que não é resultado de gesto (“Nada pra exportar”). warning = resultado parcial ou limite (“Máximo de 500 marcados”). error = falha. Neutro, só com Desfazer. |
| Cor é token, nunca a paleta do Sonner | Neutro = surface, fg e border (o mesmo chão do menu e do modal). Sucesso, erro, aviso e informação = texto -strong sobre fundo -soft, o par que contraste.test.ts mede nos cinco temas. Fonte = a Inter do body. Tudo em ui/sonner.tsx; a descrição do neutro, em globals.css. |
| O tema vem por prop | O root layout lê o cookie e passa; nunca useTheme/next-themes — sem provider o hook cai em “system” e o toast herdaria o escuro do sistema num app claro. |
| Salvar sozinho confirma pela forma da tela | Tela em fileiras de campo (a ficha da clínica) confirma no marcador do próprio campo, sem toast. A ficha da pessoa, por decisão, não confirma: grava ao sair do campo e só fala quando o campo é recusado, no próprio campo. Tela de cartões ou de escolhas que gravam sozinhas (Precificação, Configurações da agenda) chama avisarSalvo (src/lib/aviso.ts): um “Salvo” curto, de id fixo, que se atualiza no lugar em vez de empilhar. |
| Toast não substitui estado | Se a mudança fica na tela (o item sumiu da lista), o toast é confirmação; se não fica, a informação precisa de outro lugar. |
| Desfazer vive dentro do toast | Neutro, e não verde: o feito ainda pode voltar. Dura DESFAZER_MS (src/lib/aviso.ts), a mesma janela no app inteiro. Dois jeitos, conforme o gesto: gravar só quando o toast se despede (onAutoClose/onDismiss, com uma flag de desfeito; o timer do Sonner PAUSA no hover, então um setTimeout paralelo comete por baixo do toast), ou gravar na hora e o Desfazer chamar o gesto contrário (avisarComDesfazer, da agenda). toast.test.ts confere as quatro regras. |
| Erro no toast só pro que é passageiro | Falha de envio, timeout. Erro de formulário mora no FormAlert, perto do campo. |
Um toast.loading pode virar sucesso reusando o mesmo id (o botão acima demonstra). Hoje nenhuma tela do app usa esse caminho — as ações longas desabilitam o botão e trocam o rótulo (“Salvando…”). Se for usar, lembre que ele ocupa a fila de toasts durante toda a espera.
A copy dos três estados
Fórmulas
| Estado | Fórmula | Exemplo |
|---|---|---|
| Vazio (onboarding) | O que falta + o próximo passo | “Nenhum paciente ainda. Conecte o WhatsApp e comece a organizar a agenda.” |
| Vazio (filtro) | O que não bateu + como voltar | “Nenhum paciente com esse filtro. Tente outro termo ou volte pra lista completa.” |
| Erro | O que houve + o que fazer (+ o código, discreto) | “A tela não conseguiu carregar. Tente de novo. Se continuar, recarregue a página.” |
| Sucesso | O que aconteceu, com o objeto | “207 cadastros importados.” |
NuncaNunca culpar quem está lendo