melissa/layout: topbar->tray bottom-right + dock 4 builtins + mobile collapse

Tray no canto inferior direito (substitui o topbar band do topo):
busca + plan-DEV + bell + ajuda + cog. Sibling de .melissa-dock
(fora de .win11-summary) pra ficar sempre interativo mesmo com
secao aberta (que aplica blur+pointer-none). z-index 66 (acima
do dock=65). Em <md (768px) collapse parcial — bell/help/cog/
plan-DEV somem e viram popup vertical no botao ⋮; dot vermelho
no ⋮ quando ha notificacoes nao lidas. Search sempre visivel.

Dock: 4 builtins na ordem Agenda · Pacientes · WhatsApp · Financeiro
(antes so Agenda+WhatsApp). MRU (max 3) ganha @media (max-width:
767px) display:none — utility 'hidden' do Tailwind perdia pro
.dock-pin{display:grid} por ordem de carga. Divisor entre builtins
e pins user some em mobile se so houver MRU (que ja esta oculto).

Wire-ups das commits anteriores:
- ref melissaBuscaRef + provide('openMelissaBusca') pra acoes
  contextuais futuras (botao tray chama direto via ref)
- @goto-date no <MelissaBusca> -> onBuscaGotoDate via _callOnAgenda
- @iniciar-cronometro no <MelissaTimelineHoje> -> handler que abre
  o cronometro com sessionPlan + autostart; opcao (b) "ja ativo"
  mostra toast warn sem trocar paciente
- Card "Proximo paciente" troca CTA pra "Iniciar cronometro" quando
  emCurso E tem patient_id; @open chama o mesmo handler do timeline

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-22 11:41:45 -03:00
parent 9f3a047d6d
commit 473e0f026e
+401 -95
View File
@@ -575,9 +575,50 @@ function setPreset(name) {
// ─────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────
// Settings popover (canto superior direito) // Settings popover (canto inferior direito — vive na .melissa-tray)
// ─────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────
const settingsOpen = ref(false); const settingsOpen = ref(false);
const cogBtnEl = ref(null);
// "More" tray popup — visivel so em mobile (<md). Collapse de bell/
// help/cog/plan-DEV num menu vertical pra economizar largura.
const trayMoreOpen = ref(false);
const trayMoreBtnEl = ref(null);
// Fechar ao clicar fora: listener so existe enquanto o popover esta
// aberto. mousedown (capture) fecha antes do click chegar — mas o
// proprio cog precisa ser ignorado, senao fecha aqui e o @click do
// botao re-abre na sequencia.
function onSettingsDocMouseDown(e) {
if (!settingsOpen.value) return;
const t = e.target;
if (!(t instanceof Element)) return;
if (t.closest('.mp-panel')) return; // clique dentro do panel
if (cogBtnEl.value?.contains(t)) return; // clique no proprio cog
settingsOpen.value = false;
}
watch(settingsOpen, (open) => {
if (open) document.addEventListener('mousedown', onSettingsDocMouseDown, true);
else document.removeEventListener('mousedown', onSettingsDocMouseDown, true);
});
onBeforeUnmount(() => document.removeEventListener('mousedown', onSettingsDocMouseDown, true));
// Mesmo padrao pro popup "More" do tray em mobile: ignora o proprio
// botao trigger (senao fecha + reabre no click) e fecha em qualquer
// outro lugar fora do panel.
function onTrayMoreDocMouseDown(e) {
if (!trayMoreOpen.value) return;
const t = e.target;
if (!(t instanceof Element)) return;
if (t.closest('.melissa-tray__more-panel')) return;
if (trayMoreBtnEl.value?.contains(t)) return;
trayMoreOpen.value = false;
}
watch(trayMoreOpen, (open) => {
if (open) document.addEventListener('mousedown', onTrayMoreDocMouseDown, true);
else document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true);
});
onBeforeUnmount(() => document.removeEventListener('mousedown', onTrayMoreDocMouseDown, true));
// ─────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────
// Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora" // Timeline horizontal — range/eco/posicoes/auto-scroll/cursor "Agora"
@@ -587,10 +628,19 @@ const settingsOpen = ref(false);
// Pai so passa eventos brutos + workRules/settings/feriados via props, // Pai so passa eventos brutos + workRules/settings/feriados via props,
// e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo). // e recebe @evento (pra abrir dialog) + @clear-filter (pra limpar tipo).
// Contagens por tipo + frase resumo do dia // Contagens por tipo + frase resumo do dia. Pra sessao tambem quebro
// por status (cancelado/remarcado) pra montar o sufixo "(x foi cancelado,
// x foi remarcado)" depois do chip de atendimentos.
const contagensDia = computed(() => { const contagensDia = computed(() => {
const c = { sessao: 0, supervisao: 0, reuniao: 0 }; const c = { sessao: 0, supervisao: 0, reuniao: 0, sessaoCancelada: 0, sessaoRemarcada: 0 };
for (const ev of eventosHojeReais.value) c[ev.tipo] = (c[ev.tipo] || 0) + 1; for (const ev of eventosHojeReais.value) {
c[ev.tipo] = (c[ev.tipo] || 0) + 1;
if (ev.tipo === 'sessao') {
const s = String(ev.status || '').toLowerCase();
if (s === 'cancelado' || s === 'cancelada') c.sessaoCancelada += 1;
else if (s === 'remarcado') c.sessaoRemarcada += 1;
}
}
return c; return c;
}); });
@@ -598,11 +648,34 @@ function pluralizar(n, singular, plural) {
return `${n} ${n === 1 ? singular : plural}`; return `${n} ${n === 1 ? singular : plural}`;
} }
// Sufixo "(1 foi cancelado, 2 foram remarcados)" depois do chip de
// atendimentos quando houver sessoes cancel/remarcado no dia.
function _statusSuffix(qtdCancel, qtdRemarc) {
const partes = [];
if (qtdCancel > 0) {
partes.push(qtdCancel === 1
? `${qtdCancel} foi cancelado`
: `${qtdCancel} foram cancelados`);
}
if (qtdRemarc > 0) {
partes.push(qtdRemarc === 1
? `${qtdRemarc} foi remarcado`
: `${qtdRemarc} foram remarcados`);
}
return partes.length ? ` (${partes.join(', ')})` : '';
}
// Partes estruturadas pro template renderizar cada contagem como link clicável // Partes estruturadas pro template renderizar cada contagem como link clicável
const resumoPartes = computed(() => { const resumoPartes = computed(() => {
const c = contagensDia.value; const c = contagensDia.value;
const partes = []; const partes = [];
if (c.sessao > 0) partes.push({ tipo: 'sessao', text: pluralizar(c.sessao, 'atendimento', 'atendimentos') }); if (c.sessao > 0) {
partes.push({
tipo: 'sessao',
text: pluralizar(c.sessao, 'atendimento', 'atendimentos'),
suffix: _statusSuffix(c.sessaoCancelada, c.sessaoRemarcada)
});
}
if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') }); if (c.supervisao > 0) partes.push({ tipo: 'supervisao', text: pluralizar(c.supervisao, 'supervisão', 'supervisões') });
if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') }); if (c.reuniao > 0) partes.push({ tipo: 'reuniao', text: pluralizar(c.reuniao, 'reunião', 'reuniões') });
return partes; return partes;
@@ -1745,6 +1818,22 @@ function _callOnAgenda(action) {
if (secaoAberta.value !== 'agenda') abrirSecao('agenda'); if (secaoAberta.value !== 'agenda') abrirSecao('agenda');
} }
// MelissaBusca @goto-date — usuario digitou "hoje"/"20/06" na busca
// global. Abre a agenda se fechada (via _callOnAgenda que enfileira a
// action ate o ref aparecer) e chama gotoDate exposto pela MelissaAgenda.
function onBuscaGotoDate(date) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return;
_callOnAgenda((agenda) => agenda.gotoDate?.(date));
}
// Ref + provide pra qualquer secao filha pedir pra abrir a busca global
// programaticamente. UI nao tem mais botao por secao (lupa unica fica
// na .melissa-tray), mas o inject permanece exposto pra acoes contextuais
// futuras (ex: "buscar paciente" num componente filho que quer abrir
// o spotlight com query pre-preenchida).
const melissaBuscaRef = ref(null);
provide('openMelissaBusca', () => melissaBuscaRef.value?.openDialog?.());
function onAbrirProntuario() { function onAbrirProntuario() {
const ev = eventoSelecionado.value; const ev = eventoSelecionado.value;
if (!ev?.patient_id) { if (!ev?.patient_id) {
@@ -1987,6 +2076,30 @@ const { toqueTermino, testarToque } = useMelissaToques('sino');
function abrirCronometro() { function abrirCronometro() {
cronoRef.value?.abrir(); cronoRef.value?.abrir();
} }
// Click no botao ⏱ de um evento da timeline (ou no CTA do card
// "Proximo paciente" quando o evento esta em curso). Pre-seleciona
// o paciente + autostart. Se ja houver cronometro rodando de outro
// paciente, mostra toast sem trocar (opcao b decidida 2026-05-22).
function onIniciarCronometroFromEvento(ev) {
if (!ev?.patient_id) return;
// Plano programado: horario original do evento na agenda. So passa se
// os campos forem numericos validos — abrir() sanitiza de novo internamente.
const sessionPlan = (typeof ev.startH === 'number' && typeof ev.endH === 'number')
? { startH: ev.startH, endH: ev.endH }
: null;
const ret = cronoRef.value?.abrir({ pacienteId: ev.patient_id, autostart: true, sessionPlan });
if (ret && !ret.opened && ret.alreadyRunning && !ret.samePaciente) {
const atualNome = pacientesReais.value.find((p) => String(p.id) === String(ret.pacienteId))?.nome
|| 'outro paciente';
toast.add({
severity: 'warn',
summary: 'Cronômetro já ativo',
detail: `Sessão de ${atualNome} em andamento. Pare o cronômetro atual antes de iniciar outro.`,
life: 3500
});
}
}
function fecharCronometro() { function fecharCronometro() {
cronoRef.value?.fechar(); cronoRef.value?.fechar();
} }
@@ -2347,71 +2460,6 @@ function onKeydown(e) {
<!-- PLANO DE TRÁS Resumo (recebe blur quando workspace abre) --> <!-- PLANO DE TRÁS Resumo (recebe blur quando workspace abre) -->
<!-- --> <!-- -->
<div class="win11-summary" :class="{ 'is-behind': summaryDimmed }"> <div class="win11-summary" :class="{ 'is-behind': summaryDimmed }">
<!-- Faixa de fundo do topbar gradiente horizontal
(cor solida na direita -> transparente na esquerda)
pra dar legibilidade aos icones sem virar barra solida.
Cor flipa com light/dark via --m-band. -->
<div class="melissa-topbar-band" aria-hidden="true"></div>
<!-- Topbar Melissa (canto sup. direito): plan-DEV + notificações
+ ajuda + cog. Os 3 primeiros vêm do AppTopbar replicados
aqui porque a rota /melissa é fullscreen e não monta o
AppLayout. Drawer de notificações também é montado abaixo
(AjudaDrawer é global no App.vue). -->
<div class="absolute top-5 right-5 z-30 flex items-center gap-2">
<!-- Plan switcher DEV ( aparece em dev / com flag) -->
<button
v-if="showPlanDevMenu"
ref="planBtn"
class="glass-btn w-10 h-10 grid place-items-center"
:disabled="planMenuLoading || trocandoPlano"
title="Plano (DEV)"
@click="openPlanMenu"
>
<i v-if="planMenuLoading || trocandoPlano" class="pi pi-spin pi-spinner text-white/90 text-base" />
<i v-else class="pi pi-sliders-h text-white/90 text-base" />
</button>
<!-- Notificações -->
<button
class="glass-btn w-10 h-10 grid place-items-center relative"
title="Notificações"
@click="notificationStore.drawerOpen = true"
>
<i class="pi pi-bell text-white/90 text-base" />
<span
v-if="notificationStore.unreadCount > 0"
class="m-topbar-badge"
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
</button>
<!-- Ajuda -->
<button
class="glass-btn w-10 h-10 grid place-items-center"
:class="{ 'glass-btn--active': ajudaDrawerOpen }"
:title="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
@click="toggleAjuda"
>
<i class="pi pi-question-circle text-white/90 text-base" />
</button>
<!-- Cog (settings popover) existente -->
<button
class="glass-btn w-10 h-10 grid place-items-center"
title="Personalizar"
@click="settingsOpen = !settingsOpen"
>
<i class="pi pi-cog text-white/90 text-base" />
</button>
<Transition name="settings-pop">
<MelissaSettingsPanel
v-if="settingsOpen"
@close="settingsOpen = false"
/>
</Transition>
</div>
<!-- Conteúdo central --> <!-- Conteúdo central -->
<div class="win11-summary__inner"> <div class="win11-summary__inner">
<!-- Bloco hero: relógio + data + saudação + resumo do dia --> <!-- Bloco hero: relógio + data + saudação + resumo do dia -->
@@ -2428,6 +2476,7 @@ function onKeydown(e) {
<!-- Busca rápida (entre o "Hoje há" e a timeline) --> <!-- Busca rápida (entre o "Hoje há" e a timeline) -->
<MelissaBusca <MelissaBusca
ref="melissaBuscaRef"
class="mt-8" class="mt-8"
:pacientes="pacientesReais" :pacientes="pacientesReais"
:eventos="eventosHojeReais" :eventos="eventosHojeReais"
@@ -2436,6 +2485,7 @@ function onKeydown(e) {
@evento="abrirEvento" @evento="abrirEvento"
@documento="(d) => d?.patient_id ? router.push({ path: '/melissa/paciente', query: { id: String(d.patient_id), tab: 'documentos' } }) : abrirSecao('pacientes')" @documento="(d) => d?.patient_id ? router.push({ path: '/melissa/paciente', query: { id: String(d.patient_id), tab: 'documentos' } }) : abrirSecao('pacientes')"
@intake="() => abrirSecao('cadastros-recebidos')" @intake="() => abrirSecao('cadastros-recebidos')"
@goto-date="onBuscaGotoDate"
/> />
<!-- Timeline horizontal + vertical (responsivo) --> <!-- Timeline horizontal + vertical (responsivo) -->
@@ -2448,6 +2498,7 @@ function onKeydown(e) {
:filtro-tipo="filtroTipo" :filtro-tipo="filtroTipo"
@evento="abrirEvento" @evento="abrirEvento"
@clear-filter="limparFiltro" @clear-filter="limparFiltro"
@iniciar-cronometro="onIniciarCronometroFromEvento"
/> />
<!-- Cards (catálogo + ativos + layout switchável) --> <!-- Cards (catálogo + ativos + layout switchável) -->
@@ -2460,14 +2511,24 @@ function onKeydown(e) {
:class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'" :class="cardsLayout === 'linha-unica' ? 'cards-row--linha' : 'cards-row--wrap'"
> >
<template v-for="cardId in cardsAtivos" :key="cardId"> <template v-for="cardId in cardsAtivos" :key="cardId">
<!-- Próximo paciente --> <!-- Próximo paciente. Se o evento esta em curso E tem
paciente, action vira "Iniciar cronômetro" pra
facilitar o fluxo "estou comecando a sessao agora". -->
<MelissaCard <MelissaCard
v-if="cardId === 'proximo-paciente'" v-if="cardId === 'proximo-paciente'"
icon="pi pi-user" icon="pi pi-user"
icon-color="text-emerald-300" icon-color="text-emerald-300"
title="Próximo paciente" title="Próximo paciente"
:action-title="proximoPaciente ? 'Abrir sessão' : 'Abrir Pacientes'" :action-title="proximoPaciente
@open="proximoPaciente ? abrirEvento(proximoPaciente.ev) : abrirSecao('pacientes')" ? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
? 'Iniciar cronômetro'
: 'Abrir sessão')
: 'Abrir Pacientes'"
@open="proximoPaciente
? (proximoPaciente.emCurso && proximoPaciente.ev?.patient_id
? onIniciarCronometroFromEvento(proximoPaciente.ev)
: abrirEvento(proximoPaciente.ev))
: abrirSecao('pacientes')"
> >
<div v-if="proximoPaciente" class="flex items-center gap-3"> <div v-if="proximoPaciente" class="flex items-center gap-3">
<div <div
@@ -2613,14 +2674,163 @@ function onKeydown(e) {
<span class="psi-kbd" aria-hidden="true">Ctrl \</span> <span class="psi-kbd" aria-hidden="true">Ctrl \</span>
</button> </button>
<!-- -->
<!-- TRAY Melissa (system tray win11-style, canto inf. direito) -->
<!-- busca + plan-DEV + notificações + ajuda + cog. Sibling de -->
<!-- .dock (fora de .win11-summary) pra ficar sempre interativo,-->
<!-- mesmo com secao aberta (que aplica blur+pointer-none na -->
<!-- summary). AjudaDrawer ja e global no App.vue. -->
<!-- -->
<div class="melissa-tray">
<!-- Busca global (Ctrl+K) afordancia visivel pra mouse/touch.
Centraliza o ponto de acesso entre seccoes (em vez de
cada toolbar ter o proprio botao). -->
<button
class="glass-btn w-10 h-10 grid place-items-center"
v-tooltip.top="'Buscar (Ctrl+K)'"
aria-label="Busca global"
@click="melissaBuscaRef?.openDialog?.()"
>
<i class="pi pi-search text-white/90 text-base" />
</button>
<!-- Plan switcher DEV ( aparece em dev / com flag).
Em mobile (<md) some vai pro popup do "more". -->
<button
v-if="showPlanDevMenu"
ref="planBtn"
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
:disabled="planMenuLoading || trocandoPlano"
v-tooltip.top="'Plano (DEV)'"
@click="openPlanMenu"
>
<i v-if="planMenuLoading || trocandoPlano" class="pi pi-spin pi-spinner text-white/90 text-base" />
<i v-else class="pi pi-sliders-h text-white/90 text-base" />
</button>
<!-- Notificações em mobile (<md) some, vai pro popup. -->
<button
class="glass-btn w-10 h-10 hidden md:grid place-items-center relative"
v-tooltip.top="'Notificações'"
@click="notificationStore.drawerOpen = true"
>
<i class="pi pi-bell text-white/90 text-base" />
<span
v-if="notificationStore.unreadCount > 0"
class="m-topbar-badge"
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
</button>
<!-- Ajuda (conteudo muda com a rota via AjudaDrawer global).
data-ajuda-toggle: marca pro AjudaDrawer ignorar este botao
na deteccao de "clicou fora pra fechar" (senao fecha aqui
e o @click reabre). Em mobile (<md) some, vai pro popup. -->
<button
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
:class="{ 'glass-btn--active': ajudaDrawerOpen }"
v-tooltip.top="ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda'"
data-ajuda-toggle
@click="toggleAjuda"
>
<i class="pi pi-question-circle text-white/90 text-base" />
</button>
<!-- Cog (settings popover abre pra CIMA vide MelissaSettingsPanel).
Em mobile (<md) some, vai pro popup. -->
<button
ref="cogBtnEl"
class="glass-btn w-10 h-10 hidden md:grid place-items-center"
v-tooltip.top="'Personalizar'"
@click="settingsOpen = !settingsOpen"
>
<i class="pi pi-cog text-white/90 text-base" />
</button>
<Transition name="settings-pop">
<MelissaSettingsPanel
v-if="settingsOpen"
@close="settingsOpen = false"
/>
</Transition>
<!-- "More" so em mobile (<md). Collapse de plan-DEV+bell+help+
cog num popup vertical. Dot vermelho aparece se houver
notificacoes nao-lidas (preserva o sinal visual da bell). -->
<button
ref="trayMoreBtnEl"
class="glass-btn w-10 h-10 grid md:hidden place-items-center relative"
:class="{ 'glass-btn--active': trayMoreOpen }"
v-tooltip.top="trayMoreOpen ? 'Fechar' : 'Mais'"
aria-label="Mais opcoes"
@click="trayMoreOpen = !trayMoreOpen"
>
<i class="pi pi-ellipsis-v text-white/90 text-base" />
<span
v-if="notificationStore.unreadCount > 0"
class="melissa-tray__more-dot"
aria-hidden="true"
/>
</button>
<Transition name="settings-pop">
<div v-if="trayMoreOpen" class="melissa-tray__more-panel glass-panel">
<!-- Notificacoes -->
<button
class="melissa-tray__more-item"
@click="notificationStore.drawerOpen = true; trayMoreOpen = false"
>
<i class="pi pi-bell" />
<span class="flex-1 text-left">Notificações</span>
<span
v-if="notificationStore.unreadCount > 0"
class="m-topbar-badge"
style="position: static; transform: none;"
>{{ notificationStore.unreadCount > 99 ? '99+' : notificationStore.unreadCount }}</span>
</button>
<!-- Ajuda -->
<button
class="melissa-tray__more-item"
:class="{ 'is-active': ajudaDrawerOpen }"
data-ajuda-toggle
@click="toggleAjuda(); trayMoreOpen = false"
>
<i class="pi pi-question-circle" />
<span class="flex-1 text-left">{{ ajudaDrawerOpen ? 'Fechar ajuda' : 'Ajuda' }}</span>
</button>
<!-- Personalizar (cog) -->
<button
class="melissa-tray__more-item"
@click="settingsOpen = !settingsOpen; trayMoreOpen = false"
>
<i class="pi pi-cog" />
<span class="flex-1 text-left">Personalizar</span>
</button>
<!-- Plano DEV (so se flag) -->
<button
v-if="showPlanDevMenu"
class="melissa-tray__more-item"
:disabled="planMenuLoading || trocandoPlano"
@click="openPlanMenu($event); trayMoreOpen = false"
>
<i :class="planMenuLoading || trocandoPlano ? 'pi pi-spin pi-spinner' : 'pi pi-sliders-h'" />
<span class="flex-1 text-left">Plano (DEV)</span>
</button>
</div>
</Transition>
</div>
<!-- --> <!-- -->
<!-- DOCK (taskbar Win11-style sem chrome) receptáculo pra --> <!-- DOCK (taskbar Win11-style sem chrome) receptáculo pra -->
<!-- elementos minimizados (cronômetro, futuros) + atalhos --> <!-- elementos minimizados (cronômetro, futuros) + atalhos -->
<!-- pinned (Agenda, WhatsApp). Transparent, os items são --> <!-- pinned (Agenda, Pacientes, WhatsApp, Financeiro). Transp.-->
<!-- clicáveis. ψ vive ao lado (absolute, bottom-left). --> <!-- os items sao clicaveis. ψ vive ao lado (absolute, -->
<!-- bottom-left). -->
<!-- --> <!-- -->
<div class="melissa-dock"> <div class="melissa-dock">
<!-- Pinned: atalhos diretos pras seções mais usadas. <!-- Pinned builtins: 4 atalhos pras secoes principais.
Tamanho menor que ψ (44px vs 56px) + cantos arredondados Tamanho menor que ψ (44px vs 56px) + cantos arredondados
mas não full-circle, pra hierarquia visual ficar óbvia. --> mas não full-circle, pra hierarquia visual ficar óbvia. -->
<button <button
@@ -2632,6 +2842,15 @@ function onKeydown(e) {
> >
<i class="pi pi-calendar" /> <i class="pi pi-calendar" />
</button> </button>
<button
type="button"
class="dock-pin"
v-tooltip.top="'Pacientes'"
:class="{ 'dock-pin--active': secaoAberta === 'pacientes' }"
@click="abrirSecao('pacientes')"
>
<i class="pi pi-users" />
</button>
<button <button
type="button" type="button"
class="dock-pin" class="dock-pin"
@@ -2646,12 +2865,24 @@ function onKeydown(e) {
:title="`${whatsappPendente.count} mensagens não lidas`" :title="`${whatsappPendente.count} mensagens não lidas`"
>{{ whatsappPendente.count > 99 ? '99+' : whatsappPendente.count }}</span> >{{ whatsappPendente.count > 99 ? '99+' : whatsappPendente.count }}</span>
</button> </button>
<button
type="button"
class="dock-pin"
v-tooltip.top="'Financeiro'"
:class="{ 'dock-pin--active': secaoAberta === 'financeiro' }"
@click="abrirSecao('financeiro')"
>
<i class="pi pi-wallet" />
</button>
<!-- Divisor entre builtins e pins dinâmicos. aparece se <!-- Divisor entre builtins e pins dinâmicos. aparece se
o user tem pelo menos 1 pin (fixo ou recente). --> o user tem pelo menos 1 pin (fixo ou recente).
Em mobile (<md), se so houver MRU (que e oculto), o
divisor tambem some pra nao "soltar" no fim do dock. -->
<div <div
v-if="dockPins.pinned.value.length || dockPins.recent.value.length" v-if="dockPins.pinned.value.length || dockPins.recent.value.length"
class="dock-divider" class="dock-divider"
:class="{ 'hidden md:block': !dockPins.pinned.value.length }"
aria-hidden="true" aria-hidden="true"
/> />
@@ -2671,7 +2902,11 @@ function onKeydown(e) {
</button> </button>
<!-- Pins MRU (max 3) empurrados pelas últimas seções abertas. <!-- Pins MRU (max 3) empurrados pelas últimas seções abertas.
Visual mais leve (opacity menor) pra destacar dos fixos. --> Visual mais leve (opacity menor) pra destacar dos fixos.
Em mobile (<md=768px) sao ocultos via media query no CSS
do .dock-pin--recent utility 'hidden' do Tailwind perde
pro 'display: grid' base do .dock-pin (mesma specificity,
ordem de carga). -->
<button <button
v-for="slug in dockPins.recent.value" :key="`r-${slug}`" v-for="slug in dockPins.recent.value" :key="`r-${slug}`"
type="button" type="button"
@@ -3856,7 +4091,7 @@ function onKeydown(e) {
.settings-pop-enter-from, .settings-pop-enter-from,
.settings-pop-leave-to { .settings-pop-leave-to {
opacity: 0; opacity: 0;
transform: translateY(-6px) scale(0.98); transform: translateY(6px) scale(0.98);
} }
/* line-clamp util (caso Tailwind não tenha) */ /* line-clamp util (caso Tailwind não tenha) */
@@ -4011,24 +4246,88 @@ function onKeydown(e) {
--m-hero-text-border: rgba(255, 255, 255, 0.12); --m-hero-text-border: rgba(255, 255, 255, 0.12);
} }
/* ─── Faixa de fundo do topbar (canto sup. direito) ────────────── /* ─── Tray Melissa (system tray win11-style, canto inferior direito)
Gradiente horizontal: cor solida na direita (onde os icones vivem) Posiciona o grupo de icones globais (plan-DEV, notificacoes, ajuda,
e fade pra transparente na esquerda. z-index abaixo do topbar cog) alinhado verticalmente com os pins do dock. z-index acima do
(z-30) e acima do conteudo principal. */ dock (65) pra ficar no mesmo plano interativo. */
.melissa-topbar-band { .melissa-tray {
position: fixed; position: fixed;
top: 0; bottom: 0;
left: 0; right: 1.25rem;
right: 0; height: var(--m-dock-h, 76px);
height: 80px; z-index: 66;
z-index: 25; display: flex;
align-items: center;
gap: 8px;
}
/* Dot vermelho no botao "More" da tray (mobile) — sinaliza que ha
notificacoes nao-lidas escondidas dentro do popup. */
.melissa-tray__more-dot {
position: absolute;
top: 6px;
right: 6px;
width: 8px;
height: 8px;
border-radius: 9999px;
background: rgb(239, 68, 68);
border: 1.5px solid var(--m-bg-medium, rgba(0, 0, 0, 0.4));
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
pointer-events: none; pointer-events: none;
background: linear-gradient( }
to left,
var(--m-band) 0%, /* Popup vertical do "More" — abre acima do botao trigger, mesmo padrao
var(--m-band) 25%, visual do MelissaSettingsPanel mas mais compacto. */
transparent 75% .melissa-tray__more-panel {
); position: absolute;
bottom: calc(var(--m-dock-h, 76px) - 8px);
right: 0;
min-width: 220px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
border-radius: 14px;
z-index: 67;
}
.melissa-tray__more-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
background: transparent;
border: none;
color: var(--m-text);
border-radius: 8px;
cursor: pointer;
font-family: inherit;
font-size: 0.88rem;
text-align: left;
transition: background-color 140ms ease, color 140ms ease;
}
.melissa-tray__more-item:hover:not(:disabled),
.melissa-tray__more-item:focus-visible {
background: var(--m-bg-soft-hover);
outline: none;
}
.melissa-tray__more-item.is-active {
background: var(--m-bg-soft-hover);
color: white;
}
.melissa-tray__more-item:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.melissa-tray__more-item > i {
font-size: 0.95rem;
width: 16px;
color: var(--m-text-muted);
flex-shrink: 0;
}
.melissa-tray__more-item:hover > i,
.melissa-tray__more-item.is-active > i {
color: var(--m-text);
} }
/* ─── Dock (global pra atravessar Teleport + evitar perda de scoped /* ─── Dock (global pra atravessar Teleport + evitar perda de scoped
@@ -4165,6 +4464,13 @@ html:not(.app-dark) .dock-divider {
.dock-pin--recent.dock-pin--active { .dock-pin--recent.dock-pin--active {
opacity: 1; opacity: 1;
} }
/* Em mobile (<md=768px) os pins MRU somem pra economizar largura
(4 builtins + user-fixed pins ja saturam o dock). Media query no
bloco do .dock-pin--recent ganha do utility 'hidden' do Tailwind
por ordem de carga (mesma specificity). */
@media (max-width: 767px) {
.dock-pin--recent { display: none; }
}
/* ─── Skeleton loading utilitário (global pra atravessar scoped CSS dos /* ─── Skeleton loading utilitário (global pra atravessar scoped CSS dos
componentes filhos). Use a classe .melissa-skeleton em qualquer componentes filhos). Use a classe .melissa-skeleton em qualquer