Pular para o conteúdo
Vazio, carregando & erro

Componentes

Vazio, carregando & erro

Os três estados que a maioria dos sistemas trata como sobra. Aqui eles seguem o princípio dos estados calmos: informam o próximo passo e nunca alarmam. Erro é um painel discreto com “tentar de novo”, não uma tela vermelha.
ui/empty-state.tsxui/skeleton-blocos.tsxui/form-feedback.tsxui/sonner.tsx

Vazio

Duas variantes: o vazio de ONBOARDING (nunca teve nada) e o vazio de FILTRO (tem, mas não com esse recorte). Eles não dizem a mesma coisa e não oferecem o mesmo botão.

Nenhum paciente ainda

Conecte o WhatsApp ou importe sua base pra começar a organizar a agenda.

onboarding: ícone + título + próximo passo + CTA primário

Nenhum paciente com esse filtro

Tente outro termo ou volte pra lista completa.

filtro: sem ícone, respiro menor, CTA outline que LIMPA o filtro
PropTipoPadrãoO que faz
iconLucideIconOpcional. Com ícone, o gap sobe pra 4 e nasce o selo circular ink-100 de 56px; sem ícone, gap-3 (é a variante de filtro).
titlestringUma frase curta, 16/500.
descriptionstringO próximo passo, em max-w-xs. Não descreve o vazio: aponta a saída.
childrenReactNodeAs ações. Primária no onboarding, outline no filtro.
PeçaReceita
Moldurarounded-xl border border-dashed border-border
Respiropy-phi-4 (fluido)
Selo do íconesize-14 rounded-full bg-ink-100 · ícone size-6 em fg-subtle
Textocentrado, description em max-w-xs

NotaA borda tracejada é semântica

Ela diz “aqui vai entrar coisa”. É o único uso de tracejado no produto — nem card, nem upload, nem separador usam. Por isso, quando aparece, o olho já sabe o que significa.

Carregando

O cabeçalho REAL fica no lugar e só o conteúdo vira esqueleto. Assim a tela não pula quando os dados chegam — e a pessoa já sabe onde está.
SkeletonBlocos: a pilha de cards das telas de rail. O pulso e a tinta vêm do utility skeleton; aqui é só a forma.
RegraDetalhe
O esqueleto imita a FORMA realCampos 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 nadaSpinner só dentro de um botão que está trabalhando (o ícone Loader2 do toast de loading).
aria-busy e role=statusA região carregando se anuncia: role="status" aria-busy="true" aria-label="Carregando".
loading.tsx embrulha a PAGE, nunca o layout do mesmo segmentoUm await no layout faz o boundary de cima piscar antes do palco aparecer.
Sem fade de entradaO conteúdo entra no lugar do esqueleto. Fade duplicado faz a tela piscar.
o utility, e a fronteira dele
@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

Três alturas de erro: a rota inteira falhou, o formulário recusou, o campo está errado. Nenhuma delas culpa quem está na frente da tela.

Algo deu errado

A tela não conseguiu carregar. Tente de novo. Se continuar, recarregue a página.

código a1b2c3

RouteError: selo negative-soft + copy calma + código do digest + retry

Verificação em duas etapas ativa nesta conta.

FormAlert (erro geral do form) e PositiveBanner (a contraparte calma)
AlturaPeçaComportamento
A rota falhouRouteError (error.tsx)Selo, copy calma, o digest do erro em mono e um retry que faz router.refresh() + reset().
O formulário recusouFormAlertrole="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á erradoFieldError12px destructive, ligado ao input por aria-describedby, e some ao primeiro toque.
Deu certotoast.success / PositiveBannerToast 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

ERRO 422: UNPROCESSABLE ENTITY

Vermelho cheio, caixa alta e vocabulário de servidor. Assusta e não ajuda: quem marca consulta não conhece código HTTP.

Toast

Confirmação do que ACABOU de acontecer, no canto, sem interromper. Clique e veja: são os toasts reais do app.
Sonner com richColors, ícones Lucide (nunca emoji) e raio calc(--radius + 4)
RegraDetalhe
Título curto + descrição opcional“Consulta confirmada” + “Marina Alvez confirmou para amanhã, 14h.”
O tema vem por propO 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.
Toast não substitui estadoSe 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 toastO timer do Sonner PAUSA no hover: um setTimeout paralelo comete a ação enquanto o toast ainda está na tela. Comitar no onAutoClose/onDismiss, com uma flag de desfeito.
Erro no toast só pro que é passageiroFalha 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

A forma está pronta; o que decide se o estado é calmo é o texto.

Fórmulas

EstadoFórmulaExemplo
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.”
ErroO que houve + o que fazer (+ o código, discreto)“A tela não conseguiu carregar. Tente de novo. Se continuar, recarregue a página.”
SucessoO que aconteceu, com o objeto“207 cadastros importados.”

NuncaNunca culpar quem está lendo

“Você não cadastrou nada”, “Você preencheu errado”, “Você precisa selecionar”. A tela erra junto: “Nenhum paciente ainda”, “Número inválido. Use DDD + 9 dígitos”, “Escolha ao menos um horário”.