Componentes UI
Padrões visuais dos componentes do design system. Cada componente especifica as classes Tailwind que definem sua aparência, os estados obrigatórios e as restrições de uso.
Os componentes são implementados em
web/src/app/shared/components/. Esta página define o contrato visual — o que deve ser verdadeiro para qualquer implementação.
Botões
Hierarquia
Use apenas um botão Primary por tela ou seção. O restante deve ser Secondary, Ghost ou Danger.
| Variante | Quando usar |
|---|---|
| Primary | Ação principal da tela (ex.: "Salvar", "Criar Agendamento") |
| Secondary | Ação alternativa que não é destrutiva (ex.: "Exportar", "Visualizar") |
| Ghost | Ações terciárias, navegação, cancelar dentro de formulários |
| Danger | Ações destrutivas irreversíveis (ex.: "Excluir", "Cancelar Agendamento") |
| Link | Navegação inline em parágrafos de texto |
Primary
<button class="
inline-flex items-center gap-2
px-4 py-2 rounded-lg
bg-brand-600 hover:bg-brand-700 active:bg-brand-800
text-white text-sm font-medium
shadow-sm
transition-colors duration-150
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-brand-600 focus-visible:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none
">
<lucide-icon name="plus" [size]="16" aria-hidden="true" />
Novo Agendamento
</button>Secondary
<button class="
inline-flex items-center gap-2
px-4 py-2 rounded-lg
bg-white hover:bg-neutral-50 active:bg-neutral-100
border border-neutral-200 hover:border-neutral-300
text-neutral-700 text-sm font-medium
shadow-sm
transition-colors duration-150
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-brand-600 focus-visible:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none
">
Exportar
</button>Ghost
<button class="
inline-flex items-center gap-2
px-4 py-2 rounded-lg
text-neutral-600 hover:text-neutral-900
hover:bg-neutral-100 active:bg-neutral-200
text-sm font-medium
transition-colors duration-150
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-brand-600 focus-visible:ring-offset-2
disabled:opacity-40 disabled:pointer-events-none
">
Cancelar
</button>Danger
<button class="
inline-flex items-center gap-2
px-4 py-2 rounded-lg
bg-error-600 hover:bg-error-700 active:bg-error-800
text-white text-sm font-medium
shadow-sm
transition-colors duration-150
focus-visible:outline-none focus-visible:ring-2
focus-visible:ring-error-600 focus-visible:ring-offset-2
disabled:opacity-50 disabled:pointer-events-none
">
<lucide-icon name="trash-2" [size]="16" aria-hidden="true" />
Excluir Paciente
</button>Tamanhos
| Tamanho | Padding | Font size | Ícone |
|---|---|---|---|
sm | px-3 py-1.5 | text-xs | 14 px |
md (padrão) | px-4 py-2 | text-sm | 16 px |
lg | px-5 py-2.5 | text-base | 18 px |
Inputs e Formulários
Campo de Texto
<div class="flex flex-col gap-1.5">
<label for="nome" class="text-sm font-medium text-neutral-700">
Nome completo
<span class="text-error-600 ml-0.5" aria-hidden="true">*</span>
</label>
<input
id="nome"
type="text"
placeholder="Ex.: Ana Luíza Ferreira"
class="
w-full px-3 py-2 rounded-lg
bg-white border border-neutral-200
text-neutral-900 text-sm placeholder:text-neutral-400
outline-none
hover:border-neutral-300
focus:border-brand-600 focus:ring-2 focus:ring-brand-100
disabled:bg-neutral-50 disabled:text-neutral-400 disabled:cursor-not-allowed
transition-colors duration-150
"
/>
<p class="text-xs text-neutral-500">
Nome como aparece no cadastro oficial.
</p>
</div>Estado de Erro
<input class="
/* ... classes base ... */
border-error-500 ring-2 ring-error-100
focus:border-error-500 focus:ring-error-100
" aria-describedby="nome-error" aria-invalid="true" />
<p id="nome-error" class="text-xs text-error-600 flex items-center gap-1">
<lucide-icon name="alert-circle" [size]="12" aria-hidden="true" />
Informe o nome completo do paciente.
</p>Select
Mesmas dimensões e estados do input de texto. Use appearance-none + ícone chevron-down posicionado absolutamente.
Checkbox e Radio
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="
w-4 h-4 rounded
border-2 border-neutral-300
text-brand-600
focus:ring-2 focus:ring-brand-100 focus:ring-offset-0
cursor-pointer
"
/>
<span class="text-sm text-neutral-700">Notificar por e-mail</span>
</label>Cards
Card Padrão
<div class="
bg-white rounded-xl border border-neutral-200
shadow-sm
p-6
">
<!-- conteúdo -->
</div>Card de Stat (Dashboard)
<div class="
bg-white rounded-xl border border-neutral-200
shadow-sm p-6
flex items-start justify-between
">
<div>
<p class="text-sm font-medium text-neutral-500">Agendamentos hoje</p>
<p class="text-2xl font-semibold text-neutral-900 mt-1">24</p>
<p class="text-xs text-success-600 flex items-center gap-1 mt-2">
<lucide-icon name="trending-up" [size]="12" aria-hidden="true" />
+12% vs. ontem
</p>
</div>
<div class="p-2.5 bg-brand-50 rounded-lg">
<lucide-icon name="calendar" [size]="20" class="text-brand-600" aria-hidden="true" />
</div>
</div>Card Interativo (hover)
Adicionar ao card padrão:
hover:border-neutral-300 hover:shadow-md
cursor-pointer transition-all duration-150Badges / Tags
Badges indicam status. Seguem as paletas semânticas.
| Variante | Background | Texto | Uso |
|---|---|---|---|
default | neutral-100 | neutral-700 | Estado neutro |
brand | brand-50 | brand-700 | Destaque de produto |
success | success-50 | success-700 | Concluído, ativo, pago |
warning | warning-50 | warning-700 | Pendente, aguardando |
error | error-50 | error-700 | Cancelado, falha, vencido |
info | info-50 | info-700 | Novo, informativo |
<span class="
inline-flex items-center gap-1
px-2 py-0.5 rounded-full
text-xs font-medium
bg-success-50 text-success-700
">
<span class="w-1.5 h-1.5 rounded-full bg-success-500" aria-hidden="true"></span>
Concluído
</span>Modais e Dialogs
Estrutura
<!-- Overlay -->
<div class="fixed inset-0 bg-neutral-950/60 z-50 flex items-center justify-center p-4">
<!-- Container -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
class="
bg-white rounded-2xl shadow-xl
w-full max-w-md
max-h-[90vh] overflow-y-auto
"
>
<!-- Header -->
<div class="flex items-start justify-between p-6 border-b border-neutral-100">
<h2 id="modal-title" class="text-xl font-semibold text-neutral-900">
Novo Agendamento
</h2>
<button aria-label="Fechar" class="text-neutral-400 hover:text-neutral-700">
<lucide-icon name="x" [size]="20" aria-hidden="true" />
</button>
</div>
<!-- Body -->
<div class="p-6 space-y-4">
<!-- conteúdo do formulário -->
</div>
<!-- Footer -->
<div class="flex items-center justify-end gap-3 p-6 border-t border-neutral-100">
<button class="/* ghost button */">Cancelar</button>
<button class="/* primary button */">Salvar</button>
</div>
</div>
</div>Tamanhos
| Tamanho | max-w | Uso |
|---|---|---|
sm | max-w-sm (384 px) | Confirmações destrutivas |
md | max-w-md (448 px) | Formulários simples (padrão) |
lg | max-w-lg (512 px) | Formulários longos |
xl | max-w-xl (576 px) | Formulários complexos |
2xl | max-w-2xl (672 px) | Visualizadores de conteúdo |
Tabelas
<div class="rounded-xl border border-neutral-200 overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="bg-neutral-50 border-b border-neutral-200">
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wide">
Paciente
</th>
<th class="px-4 py-3 text-left text-xs font-medium text-neutral-500 uppercase tracking-wide">
Profissional
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-neutral-500 uppercase tracking-wide">
Ações
</th>
</tr>
</thead>
<tbody class="divide-y divide-neutral-100">
<tr class="hover:bg-neutral-50 transition-colors duration-100">
<td class="px-4 py-3 text-neutral-900 font-medium">Ana Luíza</td>
<td class="px-4 py-3 text-neutral-600">Dr. Rafael Costa</td>
<td class="px-4 py-3 text-right">
<!-- ações inline -->
</td>
</tr>
</tbody>
</table>
</div>Alerts e Toasts
Alert inline (dentro de formulário)
<div role="alert" class="
flex items-start gap-3 p-4 rounded-lg
bg-error-50 border border-error-100
text-error-800 text-sm
">
<lucide-icon name="alert-circle" [size]="16" class="mt-0.5 flex-shrink-0 text-error-500" aria-hidden="true" />
<p>Não foi possível salvar. Verifique os campos marcados e tente novamente.</p>
</div>Toast (notificação flutuante)
Posição: fixed bottom-4 right-4 z-50. Largura: max-w-sm. Auto-dismiss: 5 s.
<div role="status" aria-live="polite" class="
flex items-start gap-3 p-4 rounded-xl
bg-white border border-neutral-200 shadow-lg
w-full max-w-sm
">
<lucide-icon name="check-circle-2" [size]="18" class="mt-0.5 flex-shrink-0 text-success-500" aria-hidden="true" />
<div class="flex-1">
<p class="text-sm font-medium text-neutral-900">Agendamento criado</p>
<p class="text-xs text-neutral-500">Quinta-feira, 12h30 — Ana Luíza</p>
</div>
<button aria-label="Fechar notificação" class="text-neutral-400 hover:text-neutral-700">
<lucide-icon name="x" [size]="14" aria-hidden="true" />
</button>
</div>Empty States
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="p-4 bg-neutral-100 rounded-2xl mb-4">
<lucide-icon name="calendar" [size]="48" class="text-neutral-400" aria-hidden="true" />
</div>
<h3 class="text-base font-medium text-neutral-700 mb-1">
Nenhum agendamento para hoje
</h3>
<p class="text-sm text-neutral-400 max-w-xs mb-6">
A agenda está livre. Clique no botão abaixo para criar o primeiro agendamento do dia.
</p>
<button class="/* primary button */">
<lucide-icon name="plus" [size]="16" aria-hidden="true" />
Novo Agendamento
</button>
</div>Espaçamento
O sistema usa a escala de 4 px do Tailwind (spacing-1 = 4 px).
| Contexto | Valor | Tailwind |
|---|---|---|
| Gap entre ícone e rótulo | 8 px | gap-2 |
| Padding interno de botão | 8×16 px | py-2 px-4 |
| Padding interno de card | 24 px | p-6 |
| Gap entre campos de formulário | 16 px | space-y-4 |
| Gap entre seções de formulário | 24 px | space-y-6 |
| Margem entre cards de dashboard | 16 px | gap-4 |
| Padding de página | 24 px | p-6 |
Border Radius
| Elemento | Valor | Tailwind |
|---|---|---|
| Botão | 8 px | rounded-lg |
| Input | 8 px | rounded-lg |
| Badge | 999 px (pílula) | rounded-full |
| Card | 12 px | rounded-xl |
| Modal | 16 px | rounded-2xl |
| Ícone container (stat card) | 8 px | rounded-lg |
| Avatar | 50% (círculo) | rounded-full |
Sombras
| Contexto | Classe | Uso |
|---|---|---|
shadow-sm | box-shadow: 0 1px 2px rgba(0,0,0,0.05) | Cards padrão, botões |
shadow-md | box-shadow: 0 4px 6px rgba(0,0,0,0.07) | Cards hover, dropdowns |
shadow-lg | box-shadow: 0 10px 15px rgba(0,0,0,0.10) | Modais, toasts |
shadow-xl | box-shadow: 0 20px 25px rgba(0,0,0,0.10) | Overlays de alta prioridade |
Não use sombras em botões de estado disabled. Não use shadow-xl em cards de interface operacional.