Melissa: preview teleport 3-way no Agendador/LinkExterno + chrome 6 paginas

PADRAO PREVIEW 3-WAY (mobile/sidebar/floating)
- Replica o pattern do MelissaNegocio em MelissaAgendador e MelissaLinkExterno.
- Mobile: preview teleporta pro topo do main, acima de tudo (diferente do
  Negocio que vai pro drawer).
- Mid-desktop (1024-1339): teleporta pro fim da sidebar inline.
- Wide-desktop (>=1340): painel flutuante glass fora do fake dialog,
  ancorado a +14px do right edge da .X-page com width 320px.

MELISSAAGENDADOR (.mag-page)
- Importa AgendadorPreview (componente legacy do ConfiguracoesAgendadorPage).
- isWideDesktop ref + matchMedia('(min-width: 1340px)') + previewTarget computed.
- 3 placeholders + Teleport com card mag-w--side mag-w--preview.
- Adiciona right: max(6px, min(50%, calc(100% - 1006px))) em .mag-page no
  @media >=1024px (necessario pra abrir espaco pro floating).

MELISSALINKEXTERNO (.ml-page)
- Restruturacao: sidebar (Como funciona / Boas praticas) movida da DIREITA
  pra ESQUERDA + mobile drawer pattern (botao Menu, Teleport, transitions,
  backdrop) espelhando MelissaAgendador.
- 3-way teleport do preview com placeholders nos 3 alvos.
- ml-side ganha width 320px + scroll proprio.
- Right rule + floating preview CSS.

COMPONENTE NOVO: src/components/cadastro/CadastroExternoPreview.vue (~350L)
- Phone-frame 260px estilo AgendadorPreview replicando o CadastroPacienteExterno
  publico: nav (logo Psi + chip verificado), hero (avatar 38px + nome split
  firstName/lastName em accent + work_description label + clinic name),
  stepper 4 dots (1 active), card etapa 1 (numero decorativo + tag "Etapa
  1 de 4" + title "Sobre voce" + 3 input bars + CTA "Continuar"), powered by.
- Recebe :token e busca info via mesma edge function que o publico
  (get-intake-invite-info), watch refetcha quando token rotaciona.
- Sem token ou sem dados, fallback gracioso pra placeholders ("Profissional"
  + iniciais).

CHROME EM 6 PAGINAS TABULARES (sem preview)
- Apenas o right: max(6px, min(50%, calc(100% - 1006px))) no @media >=1024px,
  fazendo a janela ficar do mesmo tamanho do MelissaAgendador.
- MelissaCadastrosRecebidos (.mcr), MelissaRecorrencias (.mr), MelissaGrupos
  (.mg), MelissaTags (.mt), MelissaCompromissos (.mc), MelissaMedicos (.mm).
- +9 a 12 linhas por arquivo. Cada um nao tinha @media >=1024px ainda.

ESLint: 0 errors da minha mudanca. 2 errors pre-existentes em
MelissaRecorrencias.vue (totalDone unused L235, v-for/v-bind:key L584) -
nao toquei aquelas linhas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-08 09:08:56 -03:00
parent 558922d1a5
commit f3f0d831d2
10 changed files with 1060 additions and 55 deletions
+145
View File
@@ -17,6 +17,7 @@
import { ref, computed, onMounted, onBeforeUnmount, nextTick } from 'vue';
import MelissaConfigList from './MelissaConfigList.vue';
import JoditTextEditor from '@/components/ui/JoditTextEditor.vue';
import AgendadorPreview from '@/components/agendador/AgendadorPreview.vue';
import { useToast } from 'primevue/usetoast';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
@@ -41,14 +42,27 @@ const AGENDADOR_BUCKET = 'agendador';
// ── Breakpoints + drawer ───────────────────────────────────
const drawerOpen = ref(false);
const isMobile = ref(false);
const isWideDesktop = ref(false); // >= 1340px — preview vira painel flutuante fora do fake dialog
let _mqMobile = null;
let _mqWide = null;
function _onMqMobileChange(e) {
isMobile.value = e.matches;
if (!e.matches) drawerOpen.value = false;
}
function _onMqWideChange(e) { isWideDesktop.value = e.matches; }
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
function fecharDrawer() { drawerOpen.value = false; }
// Onde o preview do celular e renderizado:
// - mobile: dentro do main, no topo do conteudo (acima de tudo)
// - mid-desktop (1024-1339): dentro da sidebar inline (apos Status/Resumos)
// - wide-desktop (>=1340): painel flutuante fora do fake dialog
const previewTarget = computed(() => {
if (isMobile.value) return '#mag-main-preview-target';
if (isWideDesktop.value) return '#mag-floating-preview-target';
return '#mag-sidebar-preview-target';
});
// Toggle entre cards (default) e lista de configs (alterna inline na sidebar)
const cfgOpen = ref(false);
function toggleCfg() { cfgOpen.value = !cfgOpen.value; }
@@ -533,6 +547,11 @@ onMounted(async () => {
isMobile.value = _mqMobile.matches;
try { _mqMobile.addEventListener('change', _onMqMobileChange); }
catch { _mqMobile.addListener(_onMqMobileChange); }
_mqWide = window.matchMedia('(min-width: 1340px)');
isWideDesktop.value = _mqWide.matches;
try { _mqWide.addEventListener('change', _onMqWideChange); }
catch { _mqWide.addListener(_onMqWideChange); }
}
await tenantStore.ensureLoaded();
await load();
@@ -543,6 +562,10 @@ onBeforeUnmount(() => {
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
catch { _mqMobile.removeListener(_onMqMobileChange); }
}
if (_mqWide) {
try { _mqWide.removeEventListener('change', _onMqWideChange); }
catch { _mqWide.removeListener(_onMqWideChange); }
}
clearTimeout(_copyTimer);
});
@@ -574,6 +597,12 @@ const summaryItems = computed(() => [
/>
</Transition>
<!-- Painel flutuante do Preview (wide-desktop >=1340px). Vive FORA do
fake dialog, ancorado a sua right edge + 14px gap. Em mobile o
preview teleporta pro #mag-main-preview-target (topo do main); em
mid-desktop (1024-1339) teleporta pro #mag-sidebar-preview-target. -->
<aside id="mag-floating-preview-target" class="mag-floating-preview" aria-label="Pré-visualização do agendador"></aside>
<section class="mag-page">
<header class="mag-page__head">
<button
@@ -766,11 +795,20 @@ const summaryItems = computed(() => [
</button>
</div>
</div>
<!-- Target do Teleport do Preview pra modo sidebar (mid-desktop 1024-1339).
Em mobile o preview teleporta pro #mag-main-preview-target (topo do main);
em wide-desktop (>=1340px) teleporta pro #mag-floating-preview-target. -->
<div id="mag-sidebar-preview-target" class="mag-sidebar-preview-target" />
</div>
</aside>
</Teleport>
<div class="mag-main">
<!-- Target do Teleport do Preview pra modo mobile (acima de tudo no main).
Em desktop fica oculto via CSS e o preview teleporta pra sidebar/floating. -->
<div id="mag-main-preview-target" class="mag-main-preview-target" />
<!-- Loading -->
<template v-if="loading">
<div class="mag-w" v-for="n in 3" :key="`sk-${n}`">
@@ -1350,6 +1388,24 @@ const summaryItems = computed(() => [
</div>
</div>
</section>
<!-- Card de Pré-visualização (Teleport). O target alterna conforme o
viewport: topo do main (mobile) / sidebar (mid-desktop) / painel
flutuante fora do fake dialog (wide-desktop). -->
<Teleport :to="previewTarget">
<div class="mag-w mag-w--side mag-w--preview">
<div class="mag-w__head">
<div class="mag-w__icon"><i class="pi pi-mobile" /></div>
<div class="mag-w__title">
<div class="mag-w__title-text">Pré-visualização</div>
<div class="mag-w__sub">Como aparece no celular</div>
</div>
</div>
<div class="mag-w__body">
<AgendadorPreview :cfg="cfg" />
</div>
</div>
</Teleport>
</template>
<style scoped>
@@ -1553,6 +1609,15 @@ const summaryItems = computed(() => [
.mag-main ja eh display:flex flex-direction:column gap:12px no base —
so adiciona limites de largura aqui. */
@media (min-width: 1024px) {
/* Fake dialog: largura adaptativa, alinhada a esquerda (apos o
config-aside global). Espelha o pattern de MelissaNegocio:
- 10241012px : full-width (right: 6px) — overlap minimo
- 10122012px : width = 1000px fixo (right cresce com viewport)
- >= 2012px : width = ~50% do viewport (right: 50%)
Necessario pra ter espaco a direita pro painel flutuante do preview. */
.mag-page {
right: max(6px, min(50%, calc(100% - 1006px)));
}
.mag-main {
max-width: 1100px;
margin: 0 auto;
@@ -2206,4 +2271,84 @@ const summaryItems = computed(() => [
.mag-page__title-icon { display: none; }
.mag-menu-btn--mobile-only { display: inline-flex; }
}
/* ═══════ Painel flutuante do Preview (wide-desktop >= 1340px) ═══════
Vive FORA do fake dialog, ancorado a sua right edge + 14px gap.
Largura fica em 320px pra acomodar o phone-frame (260px + bordas + padding).
Glass igual ao fake dialog: fundo, blur, borda, radius, sombra. */
.mag-floating-preview {
display: none; /* default: oculto. Wide-desktop ativa via @media abaixo. */
position: absolute;
top: 6px;
/* height segue o conteudo (sem bottom). max-height limita ao mesmo
espaco do fake dialog pra forcar scroll se ficar muito alto. */
max-height: calc(100% - var(--m-dock-h, 76px) - 12px);
width: 320px;
z-index: 39; /* abaixo do mag-page (40) — nao concorre por foco */
overflow-y: auto;
overflow-x: hidden;
/* Sem padding aqui: o card .mag-w--preview interno controla o espaco
e seu __head fica flush com o topo, alinhando com o head do fake dialog. */
padding: 0;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border);
border-radius: 18px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
color: var(--m-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mag-floating-preview::-webkit-scrollbar { width: 5px; }
.mag-floating-preview::-webkit-scrollbar-thumb { background: var(--m-border-strong); border-radius: 3px; }
/* Placeholders do preview na sidebar/main: nao introduzem wrapper visivel.
Os filhos teleportados se posicionam como flex items diretos do parent
(mag-side scroll ou mag-main), herdando o mesmo gap dos outros cards. */
.mag-sidebar-preview-target,
.mag-main-preview-target { display: contents; }
/* Esconde target da sidebar em mobile (preview vai pro main) e em
wide-desktop (vai pro floating) */
@media (max-width: 1023px) {
.mag-sidebar-preview-target { display: none; }
}
/* Esconde target do main em desktop (>=1024px) — preview vai pra
sidebar/floating */
@media (min-width: 1024px) {
.mag-main-preview-target { display: none; }
}
/* Dentro do painel flutuante, o card de preview perde o "card-em-card":
sem fundo/borda/sombra propria (o glass do painel ja faz esse papel). */
.mag-floating-preview > .mag-w--preview {
background: transparent;
border: none;
box-shadow: none;
border-radius: 0;
}
.mag-floating-preview > .mag-w--preview > .mag-w__head {
border-bottom: 1px solid var(--m-border);
padding: 14px 18px;
}
.mag-floating-preview > .mag-w--preview > .mag-w__body {
padding: 14px 18px;
}
/* Wide-desktop: floating ativo, ancorado a right edge do .mag-page + 14px gap.
.mag-page tem `right: max(6px, min(50%, calc(100% - 1006px)))`, entao seu
right edge esta a `100% - max(...)` do parent-left. O preview comeca 14px
apos isso. 1340px e o piso onde page (1006) + gap (14) + preview (320) +
margem (caso) cabem confortavelmente. */
@media (min-width: 1340px) {
.mag-floating-preview {
display: block;
left: calc(100% - max(6px, min(50%, calc(100% - 1006px))) + 14px);
}
/* Placeholder da sidebar some — preview foi pro painel flutuante */
.mag-sidebar-preview-target { display: none; }
}
</style>