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:
@@ -49,3 +49,53 @@ Touched: none
|
||||
|
||||
## [2026-05-08 00:00] session | Melissa cfg-* nativas + temas + cronometro DB
|
||||
Touched: none
|
||||
|
||||
## [2026-05-08 09:30] session | Chrome+preview em 7 paginas Melissa (LinkExterno preview novo)
|
||||
Touched: none (sem mudanca de wiki - aplicacao do pattern existente)
|
||||
Detalhes: Aplicou o chrome `right: max(6px, min(50%, calc(100% - 1006px)))`
|
||||
em 6 paginas tabulares (CadastrosRecebidos .mcr / Recorrencias .mr / Grupos
|
||||
.mg / Tags .mt / Compromissos .mc / Medicos .mm) - so o tamanho de janela,
|
||||
sem preview, conforme pedido pelo user. Adicionou novo @media (min-width:
|
||||
1024px) ao final de cada arquivo (cada um nao tinha esse breakpoint ainda).
|
||||
|
||||
MelissaLinkExterno (.ml) ganhou tratamento completo: chrome + sidebar
|
||||
restruturada (2-col com aside agora a ESQUERDA, antes era a direita) +
|
||||
mobile drawer pattern (Teleport pro #ml-mobile-drawer-target, transitions,
|
||||
backdrop, botao Menu mobile-only) + 3-way teleport do preview (mobile=topo
|
||||
do main / mid-desktop=bottom da sidebar / wide-desktop>=1340=floating glass).
|
||||
Sidebar agora com Como funciona + Boas praticas (movidos da .ml-side direita)
|
||||
+ scroll proprio. Sem cfg toggle (nao havia necessidade conceitual).
|
||||
|
||||
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 prop e busca info do convite via mesma edge function que o publico
|
||||
(get-intake-invite-info), watch refetcha quando token rotaciona. Sem token
|
||||
ou sem dados, fallbacks pra "Profissional" + iniciais.
|
||||
|
||||
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. Working tree: 9 arquivos modificados +
|
||||
src/components/cadastro/ (untracked). Nao commitado, nao testado em browser.
|
||||
|
||||
## [2026-05-08 07:55] session | MelissaAgendador preview celular teleport 3-way
|
||||
Touched: none (aplicacao do padrao MelissaNegocio - sem mudanca de wiki)
|
||||
Detalhes: Replicou o padrao floating preview do MelissaNegocio em
|
||||
MelissaAgendador.vue (+145L). Importou AgendadorPreview (phone-frame ja
|
||||
existente do legacy ConfiguracoesAgendadorPage). Adicionou ref isWideDesktop
|
||||
+ matchMedia('(min-width: 1340px)') + computed previewTarget com 3-way
|
||||
branching: mobile -> #mag-main-preview-target (topo do main, acima de tudo,
|
||||
DIFERENTE do MelissaNegocio que vai pro drawer); mid-desktop (1024-1339) ->
|
||||
#mag-sidebar-preview-target (dentro da sidebar apos Status/Resumos);
|
||||
wide-desktop (>=1340) -> #mag-floating-preview-target (painel flutuante glass
|
||||
fora do fake dialog, 320px de largura, ancorado a +14px do right edge da
|
||||
.mag-page). Adicionou regra `right: max(6px, min(50%, calc(100% - 1006px)))`
|
||||
em .mag-page no @media >=1024px (necessario pra abrir espaco a direita pro
|
||||
floating). CSS: .mag-floating-preview com glass igual ao fake dialog;
|
||||
placeholders com display:contents; hide rules por breakpoint. Card de preview
|
||||
usa mag-w--side e perde fundo/borda no floating (glass do painel ja faz papel).
|
||||
ESLint 0 errors. Working tree: src/auto-imports.d.ts (auto-gerado) +
|
||||
MelissaAgendador.vue. Nao commitado, nao testado em browser ainda.
|
||||
|
||||
@@ -0,0 +1,476 @@
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Phone-frame preview da página pública de auto-cadastro do paciente
|
||||
| (CadastroPacienteExterno). Mostra como o paciente vê o link gerado
|
||||
| em MelissaLinkExterno.
|
||||
|
|
||||
| Recebe :token e busca info do convite (terapeuta + clínica) via mesma
|
||||
| edge function que o público usa (get-intake-invite-info). Sem token
|
||||
| ou sem dados, renderiza placeholders ilustrativos.
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Agência Psi
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
|
||||
const props = defineProps({
|
||||
token: { type: String, default: '' }
|
||||
});
|
||||
|
||||
const TOKEN_RX = /^[0-9a-f]{32}$|^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
const info = ref(null);
|
||||
const loading = ref(false);
|
||||
const avatarFailed = ref(false);
|
||||
|
||||
const therapist = computed(() => info.value?.therapist || null);
|
||||
const clinic = computed(() => info.value?.clinic || null);
|
||||
|
||||
const displayName = computed(() => therapist.value?.display_name || 'Profissional');
|
||||
const firstName = computed(() => String(displayName.value || '').trim().split(/\s+/)[0] || '');
|
||||
const lastName = computed(() => String(displayName.value || '').trim().split(/\s+/).slice(1).join(' '));
|
||||
const avatar = computed(() => (!avatarFailed.value ? therapist.value?.avatar_url || '' : ''));
|
||||
const initials = computed(() => {
|
||||
const parts = String(displayName.value || '').trim().split(/\s+/).filter(Boolean);
|
||||
if (!parts.length) return '·';
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
|
||||
});
|
||||
|
||||
const WORK_DESCRIPTION_LABEL = {
|
||||
psicologo_clinico: 'Psicólogo(a) Clínico(a)',
|
||||
psicanalista: 'Psicanalista',
|
||||
psiquiatra: 'Psiquiatra',
|
||||
psicoterapeuta: 'Psicoterapeuta',
|
||||
neuropsicologo: 'Neuropsicólogo(a)',
|
||||
psicologo_organizacional: 'Psic. Organizacional',
|
||||
psicologo_escolar: 'Psic. Escolar',
|
||||
psicologo_hospitalar: 'Psic. Hospitalar',
|
||||
coach_mentor: 'Coach / Mentor(a)',
|
||||
terapeuta_holistico: 'Terapeuta Holístico(a)',
|
||||
outro: 'Profissional da saúde mental'
|
||||
};
|
||||
const workLabel = computed(() => {
|
||||
const key = therapist.value?.work_description;
|
||||
if (!key) return '';
|
||||
return WORK_DESCRIPTION_LABEL[key] || 'Profissional da saúde mental';
|
||||
});
|
||||
|
||||
async function fetchInviteInfo() {
|
||||
if (!props.token || !TOKEN_RX.test(props.token)) {
|
||||
info.value = null;
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
avatarFailed.value = false;
|
||||
try {
|
||||
const { data, error } = await supabase.functions.invoke('get-intake-invite-info', {
|
||||
body: { token: props.token }
|
||||
});
|
||||
if (error) return;
|
||||
if (data?.ok && data.info) info.value = data.info;
|
||||
} catch {
|
||||
/* silencioso — preview cai pros placeholders */
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.token, fetchInviteInfo);
|
||||
onMounted(fetchInviteInfo);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- Frame de celular -->
|
||||
<div class="phone-frame">
|
||||
<div class="phone-notch" />
|
||||
|
||||
<div class="cep-root">
|
||||
<!-- Nav -->
|
||||
<div class="cep-nav">
|
||||
<div class="cep-nav__brand">
|
||||
<span class="cep-nav__logo">Ψ</span>
|
||||
<span class="cep-nav__name">Agência PSI</span>
|
||||
</div>
|
||||
<span class="cep-nav__chip">
|
||||
<i class="pi pi-check" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="cep-hero">
|
||||
<div class="cep-hero__eyebrow">Você foi convidado(a) por</div>
|
||||
<div class="cep-hero__identity">
|
||||
<div class="cep-hero__avatar-wrap">
|
||||
<img
|
||||
v-if="avatar"
|
||||
:src="avatar"
|
||||
:alt="displayName"
|
||||
class="cep-hero__avatar"
|
||||
@error="avatarFailed = true"
|
||||
/>
|
||||
<div v-else class="cep-hero__avatar cep-hero__avatar--initials">{{ initials }}</div>
|
||||
</div>
|
||||
<div class="cep-hero__content">
|
||||
<div v-if="clinic?.name" class="cep-hero__clinic">{{ clinic.name }}</div>
|
||||
<div class="cep-hero__title">
|
||||
<span>{{ firstName }}</span>
|
||||
<span v-if="lastName" class="cep-accent">{{ lastName }}</span>
|
||||
</div>
|
||||
<div v-if="workLabel" class="cep-hero__work">{{ workLabel }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stepper -->
|
||||
<div class="cep-stepper">
|
||||
<div class="cep-stepper__item is-active">
|
||||
<span class="cep-stepper__num">01</span>
|
||||
</div>
|
||||
<span class="cep-stepper__line" />
|
||||
<div class="cep-stepper__item">
|
||||
<span class="cep-stepper__num">02</span>
|
||||
</div>
|
||||
<span class="cep-stepper__line" />
|
||||
<div class="cep-stepper__item">
|
||||
<span class="cep-stepper__num">03</span>
|
||||
</div>
|
||||
<span class="cep-stepper__line" />
|
||||
<div class="cep-stepper__item">
|
||||
<span class="cep-stepper__num">04</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step card -->
|
||||
<article class="cep-card">
|
||||
<div class="cep-card__num" aria-hidden="true">01</div>
|
||||
<div class="cep-card__head">
|
||||
<span class="cep-card__tag">Etapa 1 de 4</span>
|
||||
<span class="cep-card__progress">25%</span>
|
||||
</div>
|
||||
<div class="cep-card__body">
|
||||
<h2 class="cep-card__title">
|
||||
<span>Sobre</span>
|
||||
<span class="cep-accent">você</span>
|
||||
</h2>
|
||||
<p class="cep-card__desc">Preencha seus dados básicos.</p>
|
||||
|
||||
<div class="cep-fields">
|
||||
<div class="cep-field">
|
||||
<label class="cep-field__label">Nome completo</label>
|
||||
<div class="cep-input"></div>
|
||||
</div>
|
||||
<div class="cep-field">
|
||||
<label class="cep-field__label">E-mail</label>
|
||||
<div class="cep-input"></div>
|
||||
</div>
|
||||
<div class="cep-field">
|
||||
<label class="cep-field__label">Telefone</label>
|
||||
<div class="cep-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" class="cep-cta" disabled>
|
||||
<span>Continuar</span>
|
||||
<i class="pi pi-arrow-right text-[0.55rem]" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<p class="cep-powered">Powered by <strong>Agência Psi</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Frame de celular ──────────────────── */
|
||||
.phone-frame {
|
||||
position: relative;
|
||||
width: 260px;
|
||||
min-height: 500px;
|
||||
margin: 0 auto;
|
||||
border-radius: 2.5rem;
|
||||
border: 8px solid #1e293b;
|
||||
background: #1e293b;
|
||||
box-shadow:
|
||||
0 0 0 2px #334155,
|
||||
0 32px 64px rgba(0, 0, 0, 0.35),
|
||||
0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
.phone-notch {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 72px;
|
||||
height: 10px;
|
||||
background: #1e293b;
|
||||
border-radius: 0 0 10px 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* ── Root (mimica fundo do CadastroExterno light) ─── */
|
||||
.cep-root {
|
||||
background: linear-gradient(180deg, #fafafa 0%, #f1f5f9 100%);
|
||||
min-height: 100%;
|
||||
padding: 12px 10px 14px;
|
||||
overflow-y: auto;
|
||||
max-height: 560px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ── Nav ──────────────────────────────── */
|
||||
.cep-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px 0;
|
||||
}
|
||||
.cep-nav__brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #111827;
|
||||
font-weight: 800;
|
||||
}
|
||||
.cep-nav__logo {
|
||||
font-size: 0.92rem;
|
||||
color: #6366f1;
|
||||
}
|
||||
.cep-nav__name {
|
||||
font-size: 0.62rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.cep-nav__chip {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #10b98122;
|
||||
color: #10b981;
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
.cep-nav__chip > i { font-size: 0.5rem; }
|
||||
|
||||
/* ── Hero ──────────────────────────────── */
|
||||
.cep-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 4px 4px 0;
|
||||
}
|
||||
.cep-hero__eyebrow {
|
||||
font-size: 0.52rem;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.cep-hero__identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.cep-hero__avatar-wrap {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cep-hero__avatar {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
background: #f1f5f9;
|
||||
object-fit: cover;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #6366f1;
|
||||
font-weight: 800;
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
.cep-hero__avatar--initials { background: linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%); }
|
||||
.cep-hero__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.cep-hero__clinic {
|
||||
font-size: 0.5rem;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.cep-hero__title {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.cep-hero__title .cep-accent {
|
||||
color: #6366f1;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.cep-hero__work {
|
||||
font-size: 0.55rem;
|
||||
color: #6b7280;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* ── Stepper ───────────────────────────── */
|
||||
.cep-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.cep-stepper__item {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
border: 1.5px solid #e5e7eb;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #9ca3af;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
.cep-stepper__item.is-active {
|
||||
background: #6366f1;
|
||||
border-color: #6366f1;
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 10px rgba(99, 102, 241, 0.32);
|
||||
}
|
||||
.cep-stepper__num {
|
||||
font-size: 0.5rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
.cep-stepper__line {
|
||||
width: 8px;
|
||||
height: 1.5px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
/* ── Card de etapa ─────────────────────── */
|
||||
.cep-card {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
border-radius: 0.875rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
0 6px 18px rgba(0, 0, 0, 0.06),
|
||||
0 2px 6px rgba(0, 0, 0, 0.04);
|
||||
overflow: hidden;
|
||||
padding: 10px 10px 12px;
|
||||
}
|
||||
.cep-card__num {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 8px;
|
||||
font-size: 2rem;
|
||||
font-weight: 900;
|
||||
color: #6366f1;
|
||||
opacity: 0.08;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.cep-card__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.cep-card__tag {
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: #6366f1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.cep-card__progress {
|
||||
font-size: 0.5rem;
|
||||
font-weight: 700;
|
||||
color: #6b7280;
|
||||
}
|
||||
.cep-card__title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.02em;
|
||||
color: #111827;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.cep-card__title .cep-accent {
|
||||
color: #6366f1;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.cep-card__desc {
|
||||
font-size: 0.55rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* ── Fields ────────────────────────────── */
|
||||
.cep-fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.cep-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.cep-field__label {
|
||||
font-size: 0.5rem;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
}
|
||||
.cep-input {
|
||||
height: 18px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* ── CTA ───────────────────────────────── */
|
||||
.cep-cta {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 7px 10px;
|
||||
background: #6366f1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
cursor: default;
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.28);
|
||||
}
|
||||
|
||||
/* ── Powered ───────────────────────────── */
|
||||
.cep-powered {
|
||||
text-align: center;
|
||||
font-size: 0.5rem;
|
||||
color: #9ca3af;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.cep-powered strong {
|
||||
color: #6b7280;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
@@ -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:
|
||||
- 1024–1012px : full-width (right: 6px) — overlap minimo
|
||||
- 1012–2012px : 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>
|
||||
|
||||
@@ -1887,4 +1887,16 @@ onBeforeUnmount(() => {
|
||||
via tableStyle min-width:640px. Coluna "Ação" frozen à direita
|
||||
continua fixa enquanto o user scrolla horizontalmente. */
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita pra dock e contexto.
|
||||
- 1024–1012px : full-width (right: 6px) — overlap minimo
|
||||
- 1012–2012px : width = 1000px fixo (right cresce com viewport)
|
||||
- >= 2012px : width = ~50% do viewport (right: 50%) */
|
||||
@media (min-width: 1024px) {
|
||||
.mcr-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1922,4 +1922,13 @@ async function onDelete(c) {
|
||||
.mc-act-btn--primary span { display: none; }
|
||||
.mc-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita. */
|
||||
@media (min-width: 1024px) {
|
||||
.mc-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2406,4 +2406,13 @@ watch(editPatientDialog, (isOpen) => {
|
||||
.mg-act-btn--primary span { display: none; }
|
||||
.mg-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita. */
|
||||
@media (min-width: 1024px) {
|
||||
.mg-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
* rotate_patient_invite_token_v2 + copy/openLink). Só o chrome muda pra
|
||||
* casar com o blueprint Melissa (1 header só, sem hero sticky redundante).
|
||||
*/
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import CadastroExternoPreview from '@/components/cadastro/CadastroExternoPreview.vue';
|
||||
// Button/InputText/InputGroup/InputGroupAddon/Message: auto via PrimeVueResolver
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
@@ -25,6 +26,30 @@ const inviteToken = ref('');
|
||||
const rotating = ref(false);
|
||||
const loading = ref(false);
|
||||
|
||||
// ── 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 cards info)
|
||||
// - wide-desktop (>=1340): painel flutuante fora do fake dialog
|
||||
const previewTarget = computed(() => {
|
||||
if (isMobile.value) return '#ml-main-preview-target';
|
||||
if (isWideDesktop.value) return '#ml-floating-preview-target';
|
||||
return '#ml-sidebar-preview-target';
|
||||
});
|
||||
|
||||
// ── Conteúdo estático ─────────────────────────────────────
|
||||
const howItWorks = [
|
||||
{ n: 1, title: 'Você envia o link', desc: 'Por WhatsApp, e-mail ou mensagem direta.' },
|
||||
@@ -107,14 +132,67 @@ async function copyInviteMessage() {
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────
|
||||
onMounted(async () => {
|
||||
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||
_mqMobile = window.matchMedia('(max-width: 1023px)');
|
||||
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 loadOrCreateInvite();
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (_mqMobile) {
|
||||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||||
}
|
||||
if (_mqWide) {
|
||||
try { _mqWide.removeEventListener('change', _onMqWideChange); }
|
||||
catch { _mqWide.removeListener(_onMqWideChange); }
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="ml-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="ml-mobile-drawer"
|
||||
:class="{ 'is-open': drawerOpen }"
|
||||
>
|
||||
<div id="ml-mobile-drawer-target" class="ml-mobile-drawer__scroll" />
|
||||
</div>
|
||||
</Transition>
|
||||
<Transition name="ml-drawer-fade">
|
||||
<div
|
||||
v-show="isMobile && drawerOpen"
|
||||
class="ml-mobile-drawer__backdrop"
|
||||
@click="fecharDrawer"
|
||||
/>
|
||||
</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 #ml-main-preview-target (topo do main); em
|
||||
mid-desktop (1024-1339) teleporta pro #ml-sidebar-preview-target. -->
|
||||
<aside id="ml-floating-preview-target" class="ml-floating-preview" aria-label="Pré-visualização do link externo"></aside>
|
||||
|
||||
<section class="ml-page">
|
||||
<!-- Header único (sem hero sticky — só o chrome Melissa) -->
|
||||
<header class="ml-page__head">
|
||||
<button
|
||||
class="ml-menu-btn ml-menu-btn--mobile-only"
|
||||
v-tooltip.bottom="'Como funciona & Boas práticas'"
|
||||
@click="toggleDrawer"
|
||||
>
|
||||
<i class="pi pi-bars" />
|
||||
<span>Menu</span>
|
||||
</button>
|
||||
<div class="ml-page__title">
|
||||
<i class="pi pi-share-alt ml-page__title-icon" />
|
||||
<span>Link externo de cadastro</span>
|
||||
@@ -162,8 +240,66 @@ onMounted(async () => {
|
||||
|
||||
<!-- Body 2-col -->
|
||||
<div class="ml-body">
|
||||
<!-- ═══ COL 1: Link + Mensagem ═══ -->
|
||||
<!-- ═══ COL 1: Sidebar (Como funciona / Boas práticas) — esquerda ═══ -->
|
||||
<Teleport to="#ml-mobile-drawer-target" :disabled="!isMobile">
|
||||
<aside class="ml-side">
|
||||
<!-- Como funciona -->
|
||||
<div class="ml-card">
|
||||
<div class="ml-card__head ml-card__head--simple">
|
||||
<div class="ml-info-head">
|
||||
<span class="ml-info-head__icon ml-info-head__icon--primary">
|
||||
<i class="pi pi-list-check" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="ml-info-head__title">Como funciona</div>
|
||||
<div class="ml-info-head__sub">Simples e sem fricção pro paciente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="ml-steps">
|
||||
<li v-for="step in howItWorks" :key="step.n" class="ml-steps__item">
|
||||
<div class="ml-steps__num">{{ step.n }}</div>
|
||||
<div class="ml-steps__text">
|
||||
<div class="ml-steps__title">{{ step.title }}</div>
|
||||
<div class="ml-steps__desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Boas práticas -->
|
||||
<div class="ml-card">
|
||||
<div class="ml-card__head ml-card__head--simple">
|
||||
<div class="ml-info-head">
|
||||
<span class="ml-info-head__icon ml-info-head__icon--success">
|
||||
<i class="pi pi-shield" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="ml-info-head__title">Boas práticas</div>
|
||||
<div class="ml-info-head__sub">Segurança e privacidade</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="ml-tips">
|
||||
<li v-for="tip in goodPractices" :key="tip" class="ml-tips__item">
|
||||
<i class="pi pi-check ml-tips__check" />
|
||||
<span>{{ tip }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Target do Teleport do Preview pra modo sidebar (mid-desktop 1024-1339).
|
||||
Em mobile vai pro #ml-main-preview-target; em wide-desktop pro floating. -->
|
||||
<div id="ml-sidebar-preview-target" class="ml-sidebar-preview-target" />
|
||||
</aside>
|
||||
</Teleport>
|
||||
|
||||
<!-- ═══ COL 2: Main (Link + Mensagem) ═══ -->
|
||||
<div class="ml-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="ml-main-preview-target" class="ml-main-preview-target" />
|
||||
|
||||
<!-- Card: Seu link público -->
|
||||
<div class="ml-card">
|
||||
<div class="ml-card__head">
|
||||
@@ -268,55 +404,30 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ COL 2: Instruções ═══ -->
|
||||
<aside class="ml-side">
|
||||
<!-- Como funciona -->
|
||||
<div class="ml-card">
|
||||
</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="ml-card ml-card--preview">
|
||||
<div class="ml-card__head ml-card__head--simple">
|
||||
<div class="ml-info-head">
|
||||
<span class="ml-info-head__icon ml-info-head__icon--primary">
|
||||
<i class="pi pi-list-check" />
|
||||
<i class="pi pi-mobile" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="ml-info-head__title">Como funciona</div>
|
||||
<div class="ml-info-head__sub">Simples e sem fricção pro paciente</div>
|
||||
<div class="ml-info-head__title">Pré-visualização</div>
|
||||
<div class="ml-info-head__sub">Como o paciente vê o link</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ol class="ml-steps">
|
||||
<li v-for="step in howItWorks" :key="step.n" class="ml-steps__item">
|
||||
<div class="ml-steps__num">{{ step.n }}</div>
|
||||
<div class="ml-steps__text">
|
||||
<div class="ml-steps__title">{{ step.title }}</div>
|
||||
<div class="ml-steps__desc">{{ step.desc }}</div>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<!-- Boas práticas -->
|
||||
<div class="ml-card">
|
||||
<div class="ml-card__head ml-card__head--simple">
|
||||
<div class="ml-info-head">
|
||||
<span class="ml-info-head__icon ml-info-head__icon--success">
|
||||
<i class="pi pi-shield" />
|
||||
</span>
|
||||
<div>
|
||||
<div class="ml-info-head__title">Boas práticas</div>
|
||||
<div class="ml-info-head__sub">Segurança e privacidade</div>
|
||||
<div class="ml-card__body ml-card__body--preview">
|
||||
<CadastroExternoPreview :token="inviteToken" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ul class="ml-tips">
|
||||
<li v-for="tip in goodPractices" :key="tip" class="ml-tips__item">
|
||||
<i class="pi pi-check ml-tips__check" />
|
||||
<span>{{ tip }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -454,6 +565,25 @@ onMounted(async () => {
|
||||
.ml-act-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||||
.ml-act-btn > i { font-size: 0.78rem; }
|
||||
|
||||
/* Botao "Menu" mobile (abre drawer com Como funciona / Boas praticas) */
|
||||
.ml-menu-btn {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
background: var(--m-bg-soft);
|
||||
border: 1px solid var(--m-border);
|
||||
border-radius: 8px;
|
||||
color: var(--m-text);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
.ml-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||||
.ml-menu-btn > i { font-size: 0.78rem; color: var(--p-primary-color); }
|
||||
|
||||
/* Subheader (blueprint §9) */
|
||||
.ml-subheader {
|
||||
display: flex;
|
||||
@@ -495,13 +625,22 @@ onMounted(async () => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ─── Col 2: Aside (instruções) ─── */
|
||||
/* ─── Col 1: Aside (instruções) — agora à ESQUERDA, drawer no mobile ─── */
|
||||
.ml-side {
|
||||
width: 280px;
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.ml-side::-webkit-scrollbar { width: 5px; }
|
||||
.ml-side::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Card-base */
|
||||
@@ -786,20 +925,158 @@ onMounted(async () => {
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
/* Mobile: 2-col vira 1-col, instruções vão pra baixo */
|
||||
/* Mobile: 2-col vira 1-col, instruções viraram drawer (escondido por default) */
|
||||
@media (max-width: 1023px) {
|
||||
.ml-body {
|
||||
flex-direction: column;
|
||||
padding: 8px;
|
||||
}
|
||||
.ml-side {
|
||||
width: 100%;
|
||||
}
|
||||
/* Em mobile, sidebar inline e escondida (vai pro drawer via Teleport) */
|
||||
.ml-body > .ml-side { display: none; }
|
||||
.ml-page__title > span:not(.ml-page__title-icon):not(.ml-page__status) {
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
.ml-page__status { display: none; }
|
||||
.ml-act-btn--primary span { display: none; }
|
||||
.ml-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||
.ml-menu-btn { display: inline-flex; }
|
||||
}
|
||||
|
||||
/* ═══════ Mobile drawer (esquerda, com Como funciona / Boas práticas) ═══════ */
|
||||
.ml-mobile-drawer {
|
||||
position: fixed;
|
||||
top: 0; left: 0;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
width: min(380px, 90vw);
|
||||
z-index: 80;
|
||||
background: var(--m-bg-medium);
|
||||
backdrop-filter: blur(28px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(160%);
|
||||
border-right: 1px solid var(--m-border);
|
||||
transform: translateX(-100%);
|
||||
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
color: var(--m-text);
|
||||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ml-mobile-drawer.is-open { transform: translateX(0); }
|
||||
.ml-mobile-drawer__scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--m-border-strong) transparent;
|
||||
}
|
||||
.ml-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||||
.ml-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||||
background: var(--m-border-strong);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.ml-mobile-drawer__scroll .ml-side {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.ml-mobile-drawer__backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
z-index: 79;
|
||||
}
|
||||
.ml-drawer-fade-enter-active,
|
||||
.ml-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||||
.ml-drawer-fade-enter-from,
|
||||
.ml-drawer-fade-leave-to { opacity: 0; }
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita pro painel flutuante do preview. */
|
||||
@media (min-width: 1024px) {
|
||||
.ml-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══════ Painel flutuante do Preview (wide-desktop >= 1340px) ═══════
|
||||
Vive FORA do fake dialog, ancorado a sua right edge + 14px gap.
|
||||
Largura 320px pra acomodar o phone-frame (260px + bordas + padding). */
|
||||
.ml-floating-preview {
|
||||
display: none; /* default: oculto. Wide-desktop ativa via @media abaixo. */
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
max-height: calc(100% - var(--m-dock-h, 76px) - 12px);
|
||||
width: 320px;
|
||||
z-index: 39;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
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;
|
||||
}
|
||||
.ml-floating-preview::-webkit-scrollbar { width: 5px; }
|
||||
.ml-floating-preview::-webkit-scrollbar-thumb { background: var(--m-border-strong); border-radius: 3px; }
|
||||
|
||||
/* Card de preview ganha estilo distinto pra destacar do .ml-card padrao */
|
||||
.ml-card--preview {
|
||||
background: var(--m-bg-medium);
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
.ml-card__body--preview {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
/* Placeholders do preview na sidebar/main: nao introduzem wrapper visivel.
|
||||
Os filhos teleportados se posicionam como flex items diretos do parent. */
|
||||
.ml-sidebar-preview-target,
|
||||
.ml-main-preview-target { display: contents; }
|
||||
|
||||
/* Hide rules por breakpoint */
|
||||
@media (max-width: 1023px) {
|
||||
.ml-sidebar-preview-target { display: none; }
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.ml-main-preview-target { display: none; }
|
||||
}
|
||||
|
||||
/* Dentro do painel flutuante, o card perde o "card-em-card":
|
||||
sem fundo/borda/sombra propria (o glass do painel ja faz esse papel). */
|
||||
.ml-floating-preview > .ml-card--preview {
|
||||
background: transparent;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.ml-floating-preview > .ml-card--preview > .ml-card__head {
|
||||
border-bottom: 1px solid var(--m-border);
|
||||
padding: 14px 18px;
|
||||
}
|
||||
.ml-floating-preview > .ml-card--preview > .ml-card__body--preview {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
/* Wide-desktop: floating ativo, ancorado a right edge da .ml-page + 14px gap */
|
||||
@media (min-width: 1340px) {
|
||||
.ml-floating-preview {
|
||||
display: block;
|
||||
left: calc(100% - max(6px, min(50%, calc(100% - 1006px))) + 14px);
|
||||
}
|
||||
.ml-sidebar-preview-target { display: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2546,4 +2546,13 @@ watch(editPatientDialog, (isOpen) => {
|
||||
.mm-act-btn--primary span { display: none; }
|
||||
.mm-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita. */
|
||||
@media (min-width: 1024px) {
|
||||
.mm-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1445,4 +1445,13 @@ onBeforeUnmount(() => {
|
||||
.mr-card__btn { font-size: 0.72rem; padding: 5px 9px; }
|
||||
.mr-stats-row { padding: 0 12px 6px; }
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita. */
|
||||
@media (min-width: 1024px) {
|
||||
.mr-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2368,4 +2368,13 @@ watch(editPatientDialog, (isOpen) => {
|
||||
.mt-act-btn--primary span { display: none; }
|
||||
.mt-act-btn--primary { width: 32px; padding: 0; justify-content: center; }
|
||||
}
|
||||
|
||||
/* ═══════ Fake dialog: largura adaptativa (>=1024px) ═══════
|
||||
Espelha pattern de MelissaAgendador/Negocio — fica mesma janela,
|
||||
drawer a esquerda, sobra espaco a direita. */
|
||||
@media (min-width: 1024px) {
|
||||
.mt-page {
|
||||
right: max(6px, min(50%, calc(100% - 1006px)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user