Pular para o conteúdo
Navegação

Componentes

Navegação

Três níveis, uma forma só: a sidebar diz em que área você está, o rail diz em que sub-tela, e o segmented control diz em que recorte da mesma tela. Os três são pílula — o produto não tem aba sublinhada, e isso é decisão tomada, não falta.
components/app-shell.tsxui/rail-nav.tsxui/paginacao.tsx288px

Rail

O segundo nível: as sub-telas de uma área. Ele vive DENTRO do palco de tela, nunca como coluna própria ao lado do conteúdo.
RailGroups · coluna no lg+, tira horizontal com fade no celular

RailLinks pra uma lista simples; RailGroups quando os destinos têm categorias. No lg+ o grupo vira coluna com rótulo; abaixo disso o rótulo some e os itens voltam a ser uma tira horizontal única (contents derruba o wrapper) — três tiras empilhadas custariam a altura que o palco não tem.

A pista tem mask-image na borda direita no <lg: é a pista de que há mais itens, já que a barra de rolagem fica escondida.

O fio pendura em um item o destino que só existe DENTRO dele — a construção de um modelo de contrato, que se abre a partir da lista e leva o nome do modelo. É RailItem.filho, publicado pela própria tela (ui/rail-filho.tsx: ela sabe o nome, o rail não). Com um filho aceso o PAI perde a pílula e fica só em text-fg: quem está aceso é a tela aberta, e duas pílulas diriam que se está em dois lugares. Um filho por vez — dois já seriam um grupo, que o rail sabe fazer. O fio só existe no lg+; na tira horizontal o filho é a próxima pílula.

A saída mora no pé do rail, atrás de um fio: ui/voltar-do-rail.tsx, pílula cinza cheia (bg-ink-200, hover bg-border-strong) com a seta e o destino por extenso, “Voltar a Cadastros”. Não é destino da tela, por isso não fica entre os itens nem acende em verde. Só no lg+: no celular o rail é tira no topo e a tela decide o seu voltar. Vale na ficha da pessoa e na Configuração da Agenda; o “Sair” das Configurações da conta ocupa o mesmo lugar em vermelho, porque encerra a sessão.

Item ativo por pathname, com aria-current="page". Largura útil no palco: 288 − 40 − 26 = 222px — daí o min-w-0 truncate no rótulo, senão “Custos da clínica” vaza da pista.

Recorte de visão (segmented control)

O terceiro nível: recortes da MESMA tela. No app isso é sempre um segmented control em pílula, com semântica de abas por baixo — Agenda, Atendimento e a janela de Filtros de Cadastros repetem a peça.
Conteúdo de Pacientes — 207 registros.
trilho rounded-full bg-ink-100 p-1 · ativo bg-surface shadow-xs · a pílula DESLIZA (TrilhoSegmentado) · role=tablist + ← → + roving tabIndex
PeçaReceita
Trilhoflex items-center gap-1 rounded-full bg-ink-100 p-1 · overflow-x-auto
Itemh-8 rounded-full px-3.5 text-sm font-medium · pointer-coarse:h-10
Ativobg-surface text-fg shadow-xs — a pílula “sobe” dentro do trilho
Em códigoclasseDoSegmento(ativo, extra) em lib/ui/trilho.ts: a roupa do item num lugar só; a tela passa só o que acrescenta (gap, aria-disabled:)
MovimentoTrilhoSegmentado (components/ui/trilho-segmentado.tsx) no lugar do div do trilho: a superfície do escolhido vira UMA peça que anda até o item novo em 240ms na curva ease-deslizar, e o fundo do item sai sozinho (data-pilula). Nenhum item muda; sem JS, o trilho é o que sempre foi
Inativotext-fg-muted hover:text-fg (sem fundo)
Contagemtext-xs text-fg-subtle tabular-nums
Processo vivopulso size-1.5 rounded-full bg-info-strong
Semânticarole=tablist/tab + aria-selected + aria-controls + ← →; o painel leva role=tabpanel + aria-labelledby. Nunca aria-pressed: é crachá de toggle, não de aba
Variante moldurarounded-full border border-border (sem o trilho cinza) — a troca de visão da Agenda

Sim

DiaSemana

Um trilho, um selecionado. O conjunto lê como UM controle, e é uma parada só de Tab.

Não

DiaSemana

Dois chips soltos, cada um com sua cor: viram dois filtros independentes aos olhos de quem lê — e o verde da marca sugere “ativo”, não “selecionado entre dois”.

NuncaA aba sublinhada foi avaliada e recusada — 23/08/2026

O canon herdado do Prospify previa aba com sublinhado (border-b-2 border-brand) pra navegação entre sub-coleções. Nenhuma tela nunca usou, e ela saiu do vocabulário: o produto tem duas peças que cobrem o espaço inteiro — segmented control pra recorte da mesma lista, rail pra sub-telas de uma área.

Não é economia de desenho, é economia de decisão: um terceiro formato dormindo no guia acaba usado por engano, e aí a tela nasce falando uma língua que só ela fala. Se algum dia aparecer uma tela com seis ou mais recortes de nome longo — o único caso em que o trilho do segmented control vira uma barra cinza atravessando a tela —, isso é uma decisão nova, com o caso concreto na mão. Não reabra sem ele.

Pedaço da lista (paginação)

O quarto nível: que PEDAÇO do recorte está desenhado. Nasceu em 22/09, quando uma base de 10 mil cadastros mostrou que desenhar a lista inteira trava a rolagem. Tabela de trabalho pagina; rolagem infinita é forma de feed e some com a noção de lugar.
Paginacao (components/ui/paginacao.tsx) · frase à esquerda, degraus à direita · some sozinha com uma página só
PeçaReceita
Casconav aria-label="Paginação" · flex flex-wrap items-center justify-between gap-x-4 gap-y-3
Frase“51 a 100 de 10.044 cadastros” — text-sm text-fg-muted tabular-nums, com aria-live=polite: trocar de página não recarrega a tela
Anterior / PróximaButton variant=ghost size=sm, com data-icone e disabled nas pontas
DegrauButton size=icon-sm · o atual é variant=secondary + aria-current="page"
Salto… em text-fg-subtle, aria-hidden — reticência, nunca botão
Celularos degraus somem (sm:flex) e sobra “4 de 201”: numa tela estreita eles quebravam em duas linhas por cima dos botões
Regralib/paginacao.ts — 50 por página, primeira e última sempre visíveis, a atual com uma vizinha de cada lado, e buraco de UMA página vira a própria página

Armadilha já pagaA página é janela de leitura, nunca fronteira do que existe

Paginar corta o desenho, não o dado. Contagem de aba, alcance da vista, exportar e ação em lote continuam lendo o recorte inteiro — quem contar o que está na tela achando que conta o filtro erra pelo tamanho da página.

Por isso a paginação chega acompanhada da faixa “Marcar os N do filtro”: com a lista cortada, o ☑ do cabeçalho passa a valer a página, e sem a faixa a tela teria PERDIDO a seleção em massa que já tinha.

Peças de identidade e de linha

Três componentes pequenos que aparecem em toda tela de conta e de ajustes.
RC?
AvatarBubble — 32px, foto redonda ou a inicial em brand-muted; foto cujo arquivo morreu volta pra inicial pelo use-imagem-quebrada, que cobre o erro DEPOIS da montagem (onError) e o que já falhou ANTES dela (ref)
CardIconBadge — selo de 48px no topo de cards de fluxo

Verificação em duas etapas

Um código a mais na hora de entrar.

Códigos de recuperação

Nenhum código restante.

SettingsRow — texto à esquerda, ação à direita; empilha no celular
agrupar SettingsRow
// A linha cuida do próprio ritmo vertical; quem separa é o pai.
<div className="divide-y divide-border">
  <SettingsRow title="…" description="…"><Button/></SettingsRow>
  <SettingsRow title="…" description="…" tone="negative"><Button/></SettingsRow>
</div>

// Dentro de um Card com o fio de ponta a ponta, o recuo lateral passa a ser
// da LINHA — é pra isso que existe o `className` (Configurações da agenda).
<Card>
  <CardHeader>…</CardHeader>
  <div className="divide-y divide-border">
    <SettingsRow className="px-6" title="…" description="…"><Button/></SettingsRow>
  </div>
</Card>

No celular a SettingsRow empilha (texto em cima, ação embaixo): lado a lado, a descrição ficava espremida em ~114px. É a regra 11 aplicada a um componente pequeno.