Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.3 KiB
Dialog — Padrão de Componente
Stack: Vue 3 + PrimeVue 4 + Tailwind CSS Tema-aware: header e footer respeitam dark/light automaticamente via CSS vars
Regras gerais
| Propriedade | Valor obrigatório |
|---|---|
modal |
sempre true |
maximizable |
sempre presente — botão nativo do PrimeVue, sem estado manual |
:draggable |
sempre false |
:closable |
!saving — desabilita o X durante operações assíncronas |
:dismissableMask |
!saving — impede fechar clicando fora durante saving |
pt:mask:class |
backdrop-blur-xs |
| Largura | w-[50rem] (padrão); responsivo via :breakpoints |
| Breakpoints | { '1199px': '90vw', '768px': '94vw' } |
Sistema de cores (tema-aware)
O dialog nunca deve usar bg-gray-100 ou cores hardcoded — isso quebra no dark mode.
Usar sempre as CSS vars do projeto:
| Var | Light | Dark | Uso |
|---|---|---|---|
--surface-card |
--p-surface-0 (branco) |
--p-surface-900 (quase preto) |
Fundo do corpo do dialog (default) |
--surface-ground |
--p-surface-100 (cinza claro) |
--p-surface-950 (preto) |
Fundo do header e footer — um shade mais escuro que o card |
--surface-border |
--p-content-border-color |
idem | Borda separadora entre header/content/footer |
--text-color |
preto | branco | Título principal |
--text-color-secondary |
cinza médio | cinza claro | Subtítulo, hints |
Resumo:
bg-[var(--surface-ground)]no header/footer fica sempre um pouco mais escuro que o corpo, em ambos os temas. Definido em_light.scss:19e_dark.scss:19.
Estrutura obrigatória
<Dialog>
├── #header ← dot de cor (se aplicável), título/subtítulo, btn Excluir
├── Banner ← preview visual (opcional — apenas quando há cor/identidade visual)
├── Corpo ← campos do formulário
└── #footer ← Cancelar (flat) | Salvar (primary)
Configuração completa do <Dialog>
<Dialog
v-model:visible="visible"
modal
:draggable="false"
:closable="!saving"
:dismissableMask="!saving"
maximizable
class="dc-dialog w-[50rem]"
:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"
:pt="{
header: { class: '!p-3 !rounded-t-[12px] border-b border-[var(--surface-border)] bg-[var(--surface-ground)]' },
content: { class: '!p-3' },
footer: { class: '!p-0 !rounded-b-[12px] border-t border-[var(--surface-border)] bg-[var(--surface-ground)]' },
pcCloseButton: { root: { class: '!rounded-md hover:!text-red-500' } },
pcMaximizeButton: { root: { class: '!rounded-md hover:!text-primary' } },
}"
pt:mask:class="backdrop-blur-xs"
>
Detalhes do pt
| Chave | O que faz |
|---|---|
header |
!p-3 padding uniforme; !rounded-t-[12px] borda top arredondada; border-b separador; bg-[var(--surface-ground)] fundo um shade mais escuro que o card (tema-aware) |
content |
!p-3 padding interno do corpo (herda bg-[var(--surface-card)] do PrimeVue) |
footer |
!p-0 remove padding nativo (controlado pelo wrapper interno); !rounded-b-[12px] borda bottom arredondada; border-t separador; bg-[var(--surface-ground)] mesmo fundo do header |
pcCloseButton |
!rounded-md remove o círculo nativo; hover:!text-red-500 feedback de danger no hover |
pcMaximizeButton |
!rounded-md remove o círculo nativo; hover:!text-primary feedback de cor primária no hover |
O
!(important) é necessário porque o PrimeVue injeta estilos inline nos botões e no root do Dialog — sem ele o Tailwind perde a disputa de especificidade.
Migração de dialogs antigos: trocar
bg-gray-100porbg-[var(--surface-ground)]. Oshadow-[0_1px_0_0_rgba(255,255,255,0.06)]antigo era um hack pro dark mode; pode ser removido (a borda já dá a separação).
Header — slot #header
[dot-cor] [título / subtítulo] [btn-excluir] ← Close e Maximize nativos vêm após
- O PrimeVue injeta Maximize e Close automaticamente à direita do slot
#header. - O botão Excluir fica sempre no header, nunca no footer.
- Excluir desabilitado quando o registro é nativo/padrão:
:disabled="saving || isNativeRecord".
<template #header>
<div class="flex w-full items-center justify-between gap-3 px-1">
<div class="flex items-center gap-3 min-w-0">
<!-- Dot de cor (omitir se não houver cor associada) -->
<span
class="shrink-0 w-3.5 h-3.5 rounded-full border-2 border-white/30
shadow-[0_0_0_3px_rgba(0,0,0,0.08)] transition-colors duration-200"
:style="{ backgroundColor: previewBgColor }"
/>
<div class="min-w-0">
<div class="text-base font-semibold truncate text-[var(--text-color)]">
{{ form.name || (mode === 'create' ? 'Novo item' : 'Editar item') }}
</div>
<div class="text-xs text-[var(--text-color-secondary)]">
{{ mode === 'create' ? 'Criando novo registro' : 'Editando registro' }}
</div>
</div>
</div>
<div class="flex items-center gap-1 shrink-0">
<!-- Excluir — visível apenas em edit, desabilitado se nativo -->
<Button
v-if="mode === 'edit' && canDelete !== undefined"
icon="pi pi-trash"
severity="danger"
text
rounded
:disabled="saving || isNativeRecord"
v-tooltip.top="'Excluir'"
@click="emitDelete"
/>
<!-- Maximize e Close nativos do PrimeVue são injetados aqui automaticamente -->
</div>
</div>
</template>
Cores: usar
text-[var(--text-color)]no título etext-[var(--text-color-secondary)]no subtítulo. Não usaropacity-50— a cor secondary já tem contraste calibrado por tema.
Footer — slot #footer
<template #footer>
<div class="flex items-center justify-end gap-2 px-3 py-3">
<!-- Cancelar: sempre flat, hover vermelho suave -->
<Button
label="Cancelar"
severity="secondary"
text
class="rounded-full hover:!text-red-500"
:disabled="saving"
@click="close"
/>
<!-- Salvar: sempre primary -->
<Button
label="Salvar"
icon="pi pi-check"
class="rounded-full"
:loading="saving"
:disabled="!canSubmit"
@click="submit"
/>
</div>
</template>
Regra: Cancelar =
severity="secondary" text+hover:!text-red-500. Salvar = primary (sem severity, usa o padrão do tema). Padding controlado pelodivinterno (px-3 py-3), não pelopt.footer.
Maximizar
Use a prop nativa maximizable. O PrimeVue injeta e gerencia o botão automaticamente — sem ref, sem isMaximized, sem <Button> manual.
<Dialog maximizable ...>
Se você precisar customizar a largura/altura quando maximizado (ex: 100vw), use :style reativo a um ref maximized E passe :maximizable="false" + um botão manual no #header. Padrão preferido: deixar o PrimeVue gerenciar.
Dialogs aninhados (Dialog dentro de Dialog)
Quando um Dialog secundário (criar tag, criar grupo, criar convênio) é aberto a partir do form de um Dialog principal:
- Cada Dialog é independente —
v-model:visiblepróprio - O Dialog secundário usa o mesmo blueprint (mesmo
pt, mesmas cores) - Pode ser menor:
w-[36rem]é o tamanho típico de "cadastro rápido" - Z-index: PrimeVue gerencia automaticamente (último aberto fica em cima)
- Ao salvar no Dialog secundário, o item criado pode ser auto-selecionado no Dialog principal (UX comum em formulários grandes)
Checklist antes de publicar um Dialog
modal,:draggable="false",:closable="!saving",:dismissableMask="!saving"presentesmaximizablena prop (botão nativo, sem estado manual)class="dc-dialog w-[50rem]"+:breakpoints="{ '1199px': '90vw', '768px': '94vw' }"ptcompleto: header, content, footer, pcCloseButton, pcMaximizeButton- Header com
bg-[var(--surface-ground)],border-b, e!rounded-t-[12px] - Footer com
bg-[var(--surface-ground)],border-t, e!rounded-b-[12px] - Nenhum
bg-gray-100ou cor hardcoded — só CSS vars tema-aware - Botão Excluir no header (nunca no footer), desabilitado se nativo
- Cancelar =
text+hover:!text-red-500| Salvar = primary - Padding do footer via
px-3 py-3nodivinterno - Texto usa
text-[var(--text-color)]etext-[var(--text-color-secondary)]
Variações de largura
| Uso | Classe |
|---|---|
| Cadastro rápido / formulário simples | w-[36rem] |
| Formulário padrão | w-[50rem] ← padrão |
| Formulário complexo (multi-coluna) | w-[70rem] |
| Cadastro completo (paciente, agenda) | w-[1100px] |
| Tela cheia | maximizable — usuário controla |
Anti-pattern
<!-- ❌ NÃO fazer: -->
<Dialog :pt="{
header: { class: 'bg-gray-100' }, // quebra no dark
footer: { class: 'bg-gray-100' }, // quebra no dark
}" />
<!-- ❌ NÃO fazer: -->
<div class="text-base opacity-50">subtítulo</div> <!-- usar text-color-secondary -->
<!-- ✅ Pattern correto: -->
<Dialog :pt="{
header: { class: 'bg-[var(--surface-ground)] border-b border-[var(--surface-border)]' },
footer: { class: 'bg-[var(--surface-ground)] border-t border-[var(--surface-border)]' },
}" />
<div class="text-xs text-[var(--text-color-secondary)]">subtítulo</div>