Files
agenciapsilmno/blueprints/dialog-blueprint.md
T
Leonardo 86311ef305 Melissa: hub Configuracoes + Embed + 9 Pages novas + dialog blueprint dark
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>
2026-05-04 11:41:19 -03:00

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:19 e _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-100 por bg-[var(--surface-ground)]. O shadow-[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 e text-[var(--text-color-secondary)] no subtítulo. Não usar opacity-50 — a cor secondary já tem contraste calibrado por tema.


<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 pelo div interno (px-3 py-3), não pelo pt.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:visible pró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" presentes
  • maximizable na prop (botão nativo, sem estado manual)
  • class="dc-dialog w-[50rem]" + :breakpoints="{ '1199px': '90vw', '768px': '94vw' }"
  • pt completo: 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-100 ou 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-3 no div interno
  • Texto usa text-[var(--text-color)] e text-[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>