Padrões de tela
Ações em massa
A barra
| Peça | Receita |
|---|---|
| Posição | fixed bottom-4 left-1/2 -translate-x-1/2 z-40 · w-[calc(100%-2rem)] max-w-fit |
| Forma | rounded-2xl — é painel flutuante, não um controle que se toca |
| Superfície | border border-border bg-surface shadow-md ring-1 ring-foreground/5 |
| Respiro | px-3 py-2 · gap-1.5 · flex-wrap (no celular ela quebra em linhas) |
| Contagem | px-1.5 text-sm font-medium tabular-nums — com singular e plural de verdade |
| Ações | Button variant=ghost size=sm; a destrutiva com text-destructive hover:bg-destructive/5 |
| Limpar | Button size=icon-sm com aria-label “Limpar seleção” |
| Semântica | role="group" + aria-label — NÃO role="toolbar": toolbar promete navegação por setas que a barra não tem |
| Entrada | sem animação: ela aparece quando a seleção existe (total === 0 → null) |
NotaPor que não é pílula
rounded-2xl, o degrau acima do card porque flutua) e os botões dentro dela é que são pílulas.A seleção
| Regra | Detalhe |
|---|---|
| “Selecionar todos” é o VISÍVEL | Marcar o cabeçalho seleciona o que o filtro em vigor mostra, não o banco inteiro. |
| Cada checkbox tem aria-label com o nome | “Selecionar Marina Alvez” — sem isso o leitor de tela anuncia cinco checkboxes idênticos. |
| A barra some sozinha | Seleção vazia = componente não renderiza (return null). Nada de barra fantasma com “0 selecionados”. |
| O alvo de toque cresce | pointer-coarse:size-5 — o checkbox pelado como controle primário de toque nasceu em Cadastros. |
Quais ações entram
| Entra | Fica de fora | Por quê |
|---|---|---|
| Adicionar tag | Mudar situação no funil | O estágio anda por EVENTO real (o paciente respondeu, compareceu), nunca por seleção em massa. Mudar situação é uma pessoa por vez, na ficha. |
| Exportar | Enviar mensagem em massa | Disparo em lote é o oposto do produto: o agente conversa, não bombardeia. |
| Marcar “não contatar” | Editar dados | Editar em massa apaga o que era específico de cada cadastro sem ninguém ver. |
| Excluir (com desfazer) | — | Ver abaixo: aqui a saída é desfazer, não confirmar. |
Excluir em massa é desfazer, não confirmar
1. clicou em Excluir → as linhas somem da tela (pendentes) e sobe um toast
com "Desfazer"; NADA foi escrito no banco ainda
2. clicou em Desfazer → as linhas voltam; a janela morre sem gravar
3. o toast se despede → só AÍ grava o deletedAt (soft-delete) de quem
continuava pendenteConfirmar × desfazer
Dá pra desfazer? Então não pergunte antes: aja e ofereça a volta. Não dá? Aí sim pergunte, com a consequência escrita — é o caso da tarja de confirmação e do ConfirmDialog.
O commit mora no toast
Nunca num setTimeout paralelo: o Sonner PAUSA a duração no hover, e o timer de fora commitava por baixo do toast pausado — deixando um “Desfazer” visível que não desfazia. Gravar no onAutoClose/onDismiss faz o hover estender a janela honestamente.
A contagem entra na frase do toast, com singular e plural resolvidos — “1 cadastro excluído” e “3 cadastros excluídos”, nunca “os itens selecionados”.
No celular
O que muda
A barra usa w-[calc(100%-2rem)] max-w-fit e flex-wrap: no celular ela ocupa a largura útil e as ações descem pra segunda linha em vez de espremer. O X de limpar continua sendo um alvo próprio — sair da seleção não pode ser mais difícil que entrar nela.
Se uma tela só tem UMA ação em lote, ela não precisa da barra: um botão na toolbar que age sobre a seleção resolve com menos peça.