Files
agenciapsilmno/src/layout/melissa/MelissaCard.vue
T
Leonardo 1bcb969f72 Layout Melissa (Direção B): preview, /profile, Agenda, dock, cadastro
Sandbox completo do novo layout Win11 lockscreen-style. Não troca o
AppLayout atual — Fase 5 (router wire-up) fica pra sessão dedicada.

Estrutura
- src/layout/melissa/ — MelissaLayout (bg+ψ+overlays), MelissaCronometro,
  MelissaAgenda (fullscreen), MelissaCard, MelissaMenu, MelissaBusca
- composables/useMelissaEventos.js — semana real do FC + range mensal
  pros dots do mini-cal
- composables/useMelissaPacientes.js — agora retorna created_at p/ "novo"
- melissaToques.js — toques Web Audio do término

Rota e persistência
- /preview/melissa (sem auth, sem AppLayout)
- /account/profile ganha 3º card "Melissa" com badge "Em construção"
- bootstrapUserSettings + layout composable aceitam variant='melissa'
- Migration: CHECK constraint user_settings.layout_variant aceita 'melissa'

Light mode
- Gradiente Bloom flipa via CSS vars (--bloom-c1/c2/base-1/base-2)
  Dark: 400/300/950 · Light: 200/100/0
- Cronômetro/Personalização: color: white → var(--m-text)
- Pílula psi-kbd ganha tokens --m-kbd-bg/--m-kbd-text
- Override mapeia text-X-200/300/400 → text-X-600 (17 cores Tailwind)

Agenda fullscreen
- Mini-cal funcional: click pula FC, range visível destacado, dots reais
- Feriados nacional/municipal/personalizado (rose/amber/violet)
- Dias fechados (workRules) cinza apagado, mutex feriado vence
- Card "Hoje" (stats+sessões) mesclado e movido pra sidebar esquerda
- ProximosFeriadosCard reaproveitado entre mini-cal e Hoje
- Avatar paciente: bg --m-accent-strong → --m-accent (saturado em light)
- Cores light: 12 substituições color:white → var(--m-text)

Dock taskbar Win11-style
- .melissa-dock 76px fixed bottom (CSS global, não scoped — Vue static
  hoisting perderia data-v-{hash})
- ψ centralizado vertical na faixa (bottom:10px)
- Chip cronômetro teleportado pro dock + animação minimize macOS
  (dialog encolhe + voa pro canto bottom-left, 340ms cubic-bezier)
- transform-origin: 96px calc(100% - 38px) (posição do chip no dock)

Pacientes na sidebar
- Botão fake "+" no topo abre PatientCreatePopover (rápido/completo/link)
- Reaproveita PatientCadastroDialog + ComponentCadastroRapido
- Pacientes criados nos últimos 7d sobem pro topo + badge "novo"

Dock contextual (ações do paciente selecionado)
- Avatar + nome + count + 5 ações (sessões/whatsapp/prontuário/editar/fechar)
- Teleportado pro .melissa-dock quando há paciente selecionado
- Em mobile, ações vivem em <Menu> kebab por linha
- Pattern <Transition><Teleport v-if> obrigatório (NUNCA o contrário)
  pra evitar comment placeholder + emitsOptions:null no reconciler

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:10:53 -03:00

176 lines
5.2 KiB
Vue

<script setup>
/*
* MelissaCard
* --------------------------------------------------
* Card glass do dashboard Melissa.
*
* Dois variants:
* - default: header (ícone+título+badge opcional) + slot de conteúdo
* + botão redondo "+" centralizado na borda inferior que
* emite `open` (parent decide o que fazer)
* - add: placeholder tracejado, meia largura, com "+" centralizado;
* emite `add` ao clicar
*
* Largura é fixa (não estica) — o layout flex no parent define o número
* de cards visíveis conforme o tamanho da tela.
*/
defineProps({
variant: {
type: String,
default: 'default',
validator: (v) => ['default', 'add'].includes(v)
},
icon: { type: String, default: '' }, // ex.: 'pi pi-user'
iconColor: { type: String, default: '' }, // classe Tailwind ex.: 'text-emerald-300'
title: { type: String, default: '' },
badge: { type: [String, Number, null], default: null },
badgeColor: { type: String, default: 'bg-red-500/80' }, // classe Tailwind
actionTitle: { type: String, default: 'Abrir' }
});
defineEmits(['open', 'add']);
</script>
<template>
<article v-if="variant === 'default'" class="mc-card">
<div class="mc-card__head">
<span v-if="icon" class="mc-card__icon">
<i :class="[icon, iconColor]" />
</span>
<span class="mc-card__title">{{ title }}</span>
<span v-if="badge !== null && badge !== ''" class="mc-card__badge" :class="badgeColor">
{{ badge }}
</span>
</div>
<div class="mc-card__body">
<slot />
</div>
<button class="mc-card__go" :title="actionTitle" @click="$emit('open')">
<i class="pi pi-plus text-xs" />
</button>
</article>
<button v-else class="mc-card-add" :title="actionTitle" @click="$emit('add')">
<i class="pi pi-plus" />
</button>
</template>
<style scoped>
/* ─── Card padrão ──────────────────────────────────────────── */
.mc-card {
position: relative; /* ancora o botão "+" */
flex-shrink: 0;
width: 210px;
background: var(--m-bg-soft);
backdrop-filter: blur(20px) saturate(140%);
-webkit-backdrop-filter: blur(20px) saturate(140%);
border: 1px solid var(--m-border);
border-radius: 14px;
padding: 14px 16px;
transition: background-color 160ms ease, transform 160ms ease;
}
.mc-card:hover {
background: var(--m-bg-soft-hover);
transform: translateY(-2px);
}
.mc-card__head {
display: flex;
align-items: center;
gap: 10px;
color: var(--m-text);
font-size: 0.8rem;
font-weight: 500;
letter-spacing: 0.01em;
}
.mc-card__icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 12px;
background: var(--m-bg-soft-hover);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
border: 1px solid var(--m-border-strong);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
flex-shrink: 0;
}
.mc-card__title {
/* deixa o título absorver o espaço entre ícone e badge */
flex: 1;
min-width: 0;
}
.mc-card__badge {
/* margin-left: auto não precisa mais (title flex:1 empurra o badge) */
font-size: 0.7rem;
padding: 2px 8px;
border-radius: 9999px;
color: white;
font-weight: 600;
flex-shrink: 0;
}
.mc-card__body {
margin-top: 12px;
color: white;
}
/* Botão "+" centralizado na borda inferior do card */
.mc-card__go {
position: absolute;
bottom: -16px;
left: 50%;
transform: translateX(-50%);
width: 32px;
height: 32px;
border-radius: 12px;
background: rgba(30, 30, 45, 0.85);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border: 1px solid var(--m-border-strong);
color: white;
display: grid;
place-items: center;
cursor: pointer;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35);
transition: background-color 180ms ease, transform 180ms ease, border-color 180ms ease;
z-index: 5;
}
.mc-card__go:hover {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
transform: translateX(-50%) scale(1.08);
}
.mc-card__go:active {
transform: translateX(-50%) scale(0.96);
}
/* ─── Card "Adicionar" (tracejado, meia largura) ──────────── */
/* Sem align-self aqui — o parent (cards-shell) decide se estica
(linha única) ou mantém intrínseco (wrap). */
.mc-card-add {
flex-shrink: 0;
width: 130px; /* metade do card padrão */
min-height: 100px;
border: 1.5px dashed var(--m-border-strong);
border-radius: 14px;
background: var(--m-bg-soft);
color: var(--m-text-muted);
display: grid;
place-items: center;
cursor: pointer;
font-size: 1.5rem;
transition: all 200ms ease;
}
.mc-card-add:hover {
background: var(--m-bg-soft);
border-color: var(--m-text-muted);
color: white;
transform: translateY(-2px);
}
.mc-card-add:active {
transform: translateY(0);
}
</style>