f3f0d831d2
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>
2355 lines
94 KiB
Vue
2355 lines
94 KiB
Vue
<script setup>
|
||
/*
|
||
* MelissaAgendador — Pagina nativa Melissa pra "Agendador Online".
|
||
*
|
||
* Substitui o embed cfg-agendador que vivia dentro do MelissaConfiguracoes.
|
||
* Layout 2-col:
|
||
* - COL 1 (sidebar) — Card "Status" (toggle ativo + tag PRO + link
|
||
* publico com copy/open + slug personalizado) + Card "Resumos"
|
||
* (6 atalhos clicaveis pras secoes do main)
|
||
* - COL 2 (main) — 6 cards: Identidade Visual / Perfil Publico /
|
||
* Fluxo de Agendamento / Pagamento / Triagem & LGPD / Textos da
|
||
* Jornada. Cada um com seu botao "Salvar" proprio (saveCard).
|
||
*
|
||
* Logica espelhada do ConfiguracoesAgendadorPage (tabela
|
||
* agendador_configuracoes + bucket agendador + payment_settings).
|
||
*/
|
||
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';
|
||
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
||
// InputText/Select/SelectButton/Textarea/RadioButton/Checkbox/ColorPicker/
|
||
// ToggleSwitch/Tag/Skeleton: auto via PrimeVueResolver
|
||
// (PrimeVue <Editor> trocado por JoditTextEditor — Quill colapsava em
|
||
// flex layouts com 3 editores empilhados, Jodit faz layout imperativo
|
||
// via JS sem depender de medicao do flex pai.)
|
||
|
||
const emit = defineEmits(['close']);
|
||
|
||
const toast = useToast();
|
||
const tenantStore = useTenantStore();
|
||
const entitlements = useEntitlementsStore();
|
||
|
||
const hasAgendador = computed(() => entitlements.can('agendador.online'));
|
||
const hasLinkPersonalizado = computed(() => entitlements.can('agendador.link_personalizado'));
|
||
|
||
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; }
|
||
function fecharCfg() { cfgOpen.value = false; }
|
||
|
||
// ── Estado ─────────────────────────────────────────────────
|
||
const loading = ref(true);
|
||
const ownerId = ref(null);
|
||
const savingCard = ref(null);
|
||
const uploadingField = ref(null);
|
||
const fileInputLogo = ref(null);
|
||
const fileInputHeader = ref(null);
|
||
const fileInputFundo = ref(null);
|
||
|
||
const linkCopied = ref(false);
|
||
let _copyTimer = null;
|
||
|
||
const DEFAULT_CFG = {
|
||
ativo: false,
|
||
link_slug: '',
|
||
imagem_fundo_url: '',
|
||
imagem_header_url: '',
|
||
logomarca_url: '',
|
||
cor_primaria: '#4b6bff',
|
||
nome_exibicao: '',
|
||
endereco: '',
|
||
botao_como_chegar_ativo: true,
|
||
maps_url: '',
|
||
modo_aprovacao: 'aprovacao',
|
||
modalidade: 'presencial',
|
||
tipos_habilitados: ['primeira', 'retorno'],
|
||
duracao_sessao_min: 50,
|
||
antecedencia_minima_horas: 24,
|
||
prazo_resposta_horas: 2,
|
||
reserva_horas: 2,
|
||
pagamento_obrigatorio: false,
|
||
pagamento_modo: 'sem_pagamento',
|
||
pagamento_metodos_visiveis: [],
|
||
pix_chave: '',
|
||
pix_countdown_minutos: 20,
|
||
triagem_motivo: true,
|
||
triagem_como_conheceu: false,
|
||
verificacao_email: false,
|
||
exigir_aceite_lgpd: true,
|
||
mensagem_boas_vindas: '',
|
||
texto_como_se_preparar: '',
|
||
texto_termos_lgpd: ''
|
||
};
|
||
|
||
const cfg = ref({ ...DEFAULT_CFG });
|
||
const paymentSettings = ref({});
|
||
|
||
// ── Opções ─────────────────────────────────────────────────
|
||
const tiposOptions = [
|
||
{ label: 'Primeira Entrevista', value: 'primeira' },
|
||
{ label: 'Retorno', value: 'retorno' },
|
||
{ label: 'Reagendar', value: 'reagendar' }
|
||
];
|
||
const modalidadeOptions = [
|
||
{ label: 'Presencial', value: 'presencial' },
|
||
{ label: 'Online (vídeo)', value: 'online' },
|
||
{ label: 'Ambos', value: 'ambos' }
|
||
];
|
||
const modoOptions = [
|
||
{ label: 'Aprovação manual', value: 'aprovacao' },
|
||
{ label: 'Automático', value: 'automatico' }
|
||
];
|
||
const duracoesOptions = [
|
||
{ label: '30 min', value: 30 },
|
||
{ label: '40 min', value: 40 },
|
||
{ label: '45 min', value: 45 },
|
||
{ label: '50 min', value: 50 },
|
||
{ label: '55 min', value: 55 },
|
||
{ label: '1 hora', value: 60 },
|
||
{ label: '1h 30', value: 90 },
|
||
{ label: '2 horas', value: 120 }
|
||
];
|
||
const antecedenciaOptions = [
|
||
{ label: 'Sem limite', value: 0 },
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 },
|
||
{ label: '72 horas', value: 72 }
|
||
];
|
||
const reservaOptions = [
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '4 horas', value: 4 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 }
|
||
];
|
||
const pixCountdownOptions = [
|
||
{ label: '5 min', value: 5 },
|
||
{ label: '10 min', value: 10 },
|
||
{ label: '15 min', value: 15 },
|
||
{ label: '20 min', value: 20 },
|
||
{ label: '30 min', value: 30 },
|
||
{ label: '60 min', value: 60 }
|
||
];
|
||
const prazoRespostaOptions = [
|
||
{ label: '1 hora', value: 1 },
|
||
{ label: '2 horas', value: 2 },
|
||
{ label: '4 horas', value: 4 },
|
||
{ label: '6 horas', value: 6 },
|
||
{ label: '12 horas', value: 12 },
|
||
{ label: '24 horas', value: 24 },
|
||
{ label: '48 horas', value: 48 },
|
||
{ label: '72 horas', value: 72 }
|
||
];
|
||
const modosPagamento = [
|
||
{
|
||
value: 'sem_pagamento',
|
||
label: 'Sem cobrança antecipada',
|
||
desc: 'O horário fica reservado sem exigir pagamento.',
|
||
icon: 'pi-calendar-clock'
|
||
},
|
||
{
|
||
value: 'pagar_na_hora',
|
||
label: 'Pagar na hora da sessão',
|
||
desc: 'O paciente vê as formas aceitas e paga no dia.',
|
||
icon: 'pi-wallet'
|
||
},
|
||
{
|
||
value: 'pix_antecipado',
|
||
label: 'Pix antecipado obrigatório',
|
||
desc: 'O paciente paga via Pix antes do agendamento.',
|
||
icon: 'pi-qrcode'
|
||
}
|
||
];
|
||
// METODO_LABEL removido — nao e mais usado nos resumos da sidebar
|
||
// (resumoPagamento agora mostra contagem de metodos em vez de labels).
|
||
|
||
// ── Link público ───────────────────────────────────────────
|
||
const linkPublico = computed(() => {
|
||
if (!cfg.value.link_slug) return '';
|
||
return `${window.location.origin}/agendar/${cfg.value.link_slug}`;
|
||
});
|
||
|
||
async function copyLink() {
|
||
try {
|
||
await navigator.clipboard.writeText(linkPublico.value);
|
||
linkCopied.value = true;
|
||
clearTimeout(_copyTimer);
|
||
_copyTimer = setTimeout(() => { linkCopied.value = false; }, 2000);
|
||
} catch {
|
||
toast.add({
|
||
severity: 'warn',
|
||
summary: 'Copie manualmente',
|
||
detail: linkPublico.value,
|
||
life: 5000
|
||
});
|
||
}
|
||
}
|
||
|
||
function abrirLinkPublico() {
|
||
if (!linkPublico.value) return;
|
||
window.open(linkPublico.value, '_blank', 'noopener');
|
||
}
|
||
|
||
// ── Resumos pra sidebar ────────────────────────────────────
|
||
const resumoIdentidade = computed(() => {
|
||
const parts = [];
|
||
if (cfg.value.nome_exibicao) parts.push(cfg.value.nome_exibicao);
|
||
if (cfg.value.cor_primaria) parts.push(cfg.value.cor_primaria);
|
||
return parts.join(' · ') || 'Não configurado';
|
||
});
|
||
const resumoPerfil = computed(() => {
|
||
const parts = [];
|
||
if (cfg.value.endereco) parts.push(cfg.value.endereco.slice(0, 40) + (cfg.value.endereco.length > 40 ? '…' : ''));
|
||
if (cfg.value.botao_como_chegar_ativo) parts.push('Como chegar ativo');
|
||
return parts.join(' · ') || 'Não configurado';
|
||
});
|
||
const resumoFluxo = computed(() => {
|
||
const modo = cfg.value.modo_aprovacao === 'aprovacao' ? 'Aprovação manual' : 'Automático';
|
||
const tipos = (cfg.value.tipos_habilitados || []).length;
|
||
return `${modo} · ${tipos} tipo${tipos !== 1 ? 's' : ''} · ${cfg.value.duracao_sessao_min}min`;
|
||
});
|
||
const resumoPagamento = computed(() => {
|
||
const modo = cfg.value.pagamento_modo;
|
||
if (modo === 'pix_antecipado') return `Pix antecipado · ${cfg.value.pix_countdown_minutos}min`;
|
||
if (modo === 'pagar_na_hora') {
|
||
const ativos = cfg.value.pagamento_metodos_visiveis || [];
|
||
return ativos.length
|
||
? `Pagar na hora · ${ativos.length} método${ativos.length !== 1 ? 's' : ''}`
|
||
: 'Pagar na hora';
|
||
}
|
||
return 'Sem cobrança antecipada';
|
||
});
|
||
const resumoTriagem = computed(() => {
|
||
const campos = [];
|
||
if (cfg.value.triagem_motivo) campos.push('Motivo');
|
||
if (cfg.value.triagem_como_conheceu) campos.push('Origem');
|
||
if (cfg.value.verificacao_email) campos.push('E-mail');
|
||
if (cfg.value.exigir_aceite_lgpd) campos.push('LGPD');
|
||
return campos.join(' · ') || 'Sem triagem extra';
|
||
});
|
||
const resumoTextos = computed(() => {
|
||
const bv = cfg.value.mensagem_boas_vindas?.trim();
|
||
const cp = cfg.value.texto_como_se_preparar?.trim();
|
||
if (bv || cp) return 'Textos configurados';
|
||
return 'Nenhum texto configurado';
|
||
});
|
||
|
||
// Métodos pra "pagar na hora"
|
||
const metodosDisponiveis = computed(() => {
|
||
const ps = paymentSettings.value;
|
||
return [
|
||
{ key: 'pix', label: 'Pix', icon: 'pi-qrcode', ativo: ps.pix_ativo },
|
||
{ key: 'deposito', label: 'Depósito/TED', icon: 'pi-building-columns', ativo: ps.deposito_ativo },
|
||
{ key: 'dinheiro', label: 'Dinheiro', icon: 'pi-wallet', ativo: ps.dinheiro_ativo },
|
||
{ key: 'cartao', label: 'Cartão', icon: 'pi-credit-card', ativo: ps.cartao_ativo },
|
||
{ key: 'convenio', label: 'Convênio', icon: 'pi-heart', ativo: ps.convenio_ativo }
|
||
];
|
||
});
|
||
const algumMetodoConfigurado = computed(() => metodosDisponiveis.value.some((m) => m.ativo));
|
||
|
||
function toggleMetodoVisivel(key) {
|
||
const arr = [...(cfg.value.pagamento_metodos_visiveis || [])];
|
||
const idx = arr.indexOf(key);
|
||
if (idx === -1) arr.push(key);
|
||
else arr.splice(idx, 1);
|
||
cfg.value.pagamento_metodos_visiveis = arr;
|
||
}
|
||
function isMetodoVisivel(key) {
|
||
return (cfg.value.pagamento_metodos_visiveis || []).includes(key);
|
||
}
|
||
|
||
function toggleTipo(value) {
|
||
const list = [...(cfg.value.tipos_habilitados || [])];
|
||
const idx = list.indexOf(value);
|
||
if (idx >= 0) list.splice(idx, 1);
|
||
else list.push(value);
|
||
cfg.value.tipos_habilitados = list;
|
||
}
|
||
|
||
// ── Auth / tenant ──────────────────────────────────────────
|
||
async function getOwnerId() {
|
||
const { data, error } = await supabase.auth.getUser();
|
||
if (error) throw error;
|
||
return data?.user?.id;
|
||
}
|
||
async function getActiveTenantId(uid) {
|
||
const fromStore = tenantStore.activeTenantId || tenantStore.tenantId || tenantStore.tenant?.id || null;
|
||
if (fromStore) return fromStore;
|
||
const { data } = await supabase
|
||
.from('tenant_members')
|
||
.select('tenant_id')
|
||
.eq('user_id', uid)
|
||
.eq('status', 'active')
|
||
.limit(1)
|
||
.maybeSingle();
|
||
return data?.tenant_id || null;
|
||
}
|
||
|
||
// ── Load ───────────────────────────────────────────────────
|
||
async function loadPaymentSettings(uid) {
|
||
try {
|
||
const { data } = await supabase
|
||
.from('payment_settings')
|
||
.select('pix_ativo, pix_chave, pix_tipo, deposito_ativo, dinheiro_ativo, cartao_ativo, convenio_ativo')
|
||
.eq('owner_id', uid)
|
||
.maybeSingle();
|
||
paymentSettings.value = data || {};
|
||
} catch {
|
||
paymentSettings.value = {};
|
||
}
|
||
}
|
||
|
||
async function load() {
|
||
loading.value = true;
|
||
try {
|
||
const uid = await getOwnerId();
|
||
ownerId.value = uid;
|
||
|
||
const [{ data, error }] = await Promise.all([
|
||
supabase.from('agendador_configuracoes').select('*').eq('owner_id', uid).maybeSingle(),
|
||
loadPaymentSettings(uid)
|
||
]);
|
||
if (error) throw error;
|
||
|
||
if (data) {
|
||
const loaded = {
|
||
...DEFAULT_CFG,
|
||
...data,
|
||
tipos_habilitados: Array.isArray(data.tipos_habilitados)
|
||
? data.tipos_habilitados
|
||
: DEFAULT_CFG.tipos_habilitados,
|
||
pagamento_metodos_visiveis: Array.isArray(data.pagamento_metodos_visiveis)
|
||
? data.pagamento_metodos_visiveis
|
||
: []
|
||
};
|
||
if (!data.pagamento_modo) {
|
||
loaded.pagamento_modo = data.pagamento_obrigatorio ? 'pix_antecipado' : 'sem_pagamento';
|
||
}
|
||
cfg.value = loaded;
|
||
} else {
|
||
cfg.value = { ...DEFAULT_CFG };
|
||
if (tenantStore.tenant?.name) cfg.value.nome_exibicao = tenantStore.tenant.name;
|
||
}
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao carregar', detail: e.message, life: 4000 });
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
// ── Toggle ativo ───────────────────────────────────────────
|
||
async function toggleAtivo() {
|
||
const uid = ownerId.value;
|
||
if (!uid) return;
|
||
const novoAtivo = !cfg.value.ativo;
|
||
cfg.value.ativo = novoAtivo;
|
||
try {
|
||
const tenantId = await getActiveTenantId(uid);
|
||
const { error } = await supabase
|
||
.from('agendador_configuracoes')
|
||
.upsert(
|
||
{ owner_id: uid, tenant_id: tenantId, ativo: novoAtivo, updated_at: new Date().toISOString() },
|
||
{ onConflict: 'owner_id' }
|
||
);
|
||
if (error) throw error;
|
||
toast.add({
|
||
severity: novoAtivo ? 'success' : 'info',
|
||
summary: novoAtivo ? 'Agendador ativado' : 'Agendador desativado',
|
||
life: 3000
|
||
});
|
||
if (novoAtivo) await load();
|
||
} catch (e) {
|
||
cfg.value.ativo = !novoAtivo;
|
||
toast.add({ severity: 'error', summary: 'Erro', detail: e.message, life: 4000 });
|
||
}
|
||
}
|
||
|
||
// ── Save card ──────────────────────────────────────────────
|
||
function buildPayload(cardKey) {
|
||
const c = cfg.value;
|
||
if (cardKey === 'identidade') {
|
||
return {
|
||
imagem_fundo_url: c.imagem_fundo_url,
|
||
imagem_header_url: c.imagem_header_url,
|
||
logomarca_url: c.logomarca_url,
|
||
cor_primaria: c.cor_primaria
|
||
};
|
||
}
|
||
if (cardKey === 'perfil') {
|
||
return {
|
||
nome_exibicao: c.nome_exibicao,
|
||
endereco: c.endereco,
|
||
botao_como_chegar_ativo: c.botao_como_chegar_ativo,
|
||
maps_url: c.maps_url
|
||
};
|
||
}
|
||
if (cardKey === 'fluxo') {
|
||
return {
|
||
modo_aprovacao: c.modo_aprovacao,
|
||
modalidade: c.modalidade,
|
||
tipos_habilitados: c.tipos_habilitados,
|
||
duracao_sessao_min: c.duracao_sessao_min,
|
||
antecedencia_minima_horas: c.antecedencia_minima_horas,
|
||
prazo_resposta_horas: c.prazo_resposta_horas
|
||
};
|
||
}
|
||
if (cardKey === 'pagamento') {
|
||
const modo = c.pagamento_modo || 'sem_pagamento';
|
||
return {
|
||
pagamento_modo: modo,
|
||
pagamento_obrigatorio: modo === 'pix_antecipado',
|
||
pagamento_metodos_visiveis: c.pagamento_metodos_visiveis || [],
|
||
pix_chave: c.pix_chave?.trim() ?? '',
|
||
pix_countdown_minutos: c.pix_countdown_minutos,
|
||
reserva_horas: c.reserva_horas
|
||
};
|
||
}
|
||
if (cardKey === 'triagem') {
|
||
return {
|
||
triagem_motivo: c.triagem_motivo,
|
||
triagem_como_conheceu: c.triagem_como_conheceu,
|
||
verificacao_email: c.verificacao_email,
|
||
exigir_aceite_lgpd: c.exigir_aceite_lgpd
|
||
};
|
||
}
|
||
if (cardKey === 'textos') {
|
||
return {
|
||
mensagem_boas_vindas: c.mensagem_boas_vindas,
|
||
texto_como_se_preparar: c.texto_como_se_preparar,
|
||
texto_termos_lgpd: c.texto_termos_lgpd
|
||
};
|
||
}
|
||
if (cardKey === 'slug') {
|
||
return {
|
||
link_slug:
|
||
c.link_slug
|
||
?.trim()
|
||
.toLowerCase()
|
||
.replace(/[^a-z0-9-]/g, '-') || null
|
||
};
|
||
}
|
||
return {};
|
||
}
|
||
|
||
async function saveCard(cardKey) {
|
||
savingCard.value = cardKey;
|
||
try {
|
||
const uid = ownerId.value;
|
||
const tenantId = await getActiveTenantId(uid);
|
||
const payload = buildPayload(cardKey);
|
||
const { error } = await supabase
|
||
.from('agendador_configuracoes')
|
||
.upsert(
|
||
{ owner_id: uid, tenant_id: tenantId, ...payload, updated_at: new Date().toISOString() },
|
||
{ onConflict: 'owner_id' }
|
||
);
|
||
if (error) throw error;
|
||
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 });
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e.message, life: 4000 });
|
||
} finally {
|
||
savingCard.value = null;
|
||
}
|
||
}
|
||
|
||
// ── Upload imagens ─────────────────────────────────────────
|
||
async function uploadImagem(file, field) {
|
||
if (!file || !ownerId.value) return null;
|
||
uploadingField.value = field;
|
||
try {
|
||
const ext = file.name.split('.').pop() || 'jpg';
|
||
const path = `${ownerId.value}/${field}-${Date.now()}.${ext}`;
|
||
const { error: upErr } = await supabase.storage
|
||
.from(AGENDADOR_BUCKET)
|
||
.upload(path, file, { upsert: true, contentType: file.type });
|
||
if (upErr) throw upErr;
|
||
const { data } = supabase.storage.from(AGENDADOR_BUCKET).getPublicUrl(path);
|
||
return data?.publicUrl || null;
|
||
} finally {
|
||
uploadingField.value = null;
|
||
}
|
||
}
|
||
|
||
async function onFileSelected(event, field) {
|
||
const file = event.target.files?.[0];
|
||
if (!file) return;
|
||
try {
|
||
const url = await uploadImagem(file, field);
|
||
if (url) {
|
||
if (field === 'logomarca') cfg.value.logomarca_url = url;
|
||
if (field === 'header') cfg.value.imagem_header_url = url;
|
||
if (field === 'fundo') cfg.value.imagem_fundo_url = url;
|
||
const uid = ownerId.value;
|
||
const tenantId = await getActiveTenantId(uid);
|
||
await supabase
|
||
.from('agendador_configuracoes')
|
||
.upsert(
|
||
{ owner_id: uid, tenant_id: tenantId, ...buildPayload('identidade'), updated_at: new Date().toISOString() },
|
||
{ onConflict: 'owner_id' }
|
||
);
|
||
toast.add({ severity: 'success', summary: 'Imagem salva', life: 2000 });
|
||
}
|
||
} catch (e) {
|
||
toast.add({ severity: 'error', summary: 'Erro no upload', detail: e.message, life: 4000 });
|
||
}
|
||
}
|
||
|
||
// ── Ancora pra sessao ──────────────────────────────────────
|
||
function scrollToCard(cardKey) {
|
||
if (isMobile.value) drawerOpen.value = false;
|
||
nextTick(() => {
|
||
const el = document.getElementById('mag-sec-' + cardKey);
|
||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||
});
|
||
}
|
||
|
||
// ── 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 tenantStore.ensureLoaded();
|
||
await load();
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (_mqMobile) {
|
||
try { _mqMobile.removeEventListener('change', _onMqMobileChange); }
|
||
catch { _mqMobile.removeListener(_onMqMobileChange); }
|
||
}
|
||
if (_mqWide) {
|
||
try { _mqWide.removeEventListener('change', _onMqWideChange); }
|
||
catch { _mqWide.removeListener(_onMqWideChange); }
|
||
}
|
||
clearTimeout(_copyTimer);
|
||
});
|
||
|
||
const summaryItems = computed(() => [
|
||
{ key: 'identidade', label: 'Identidade Visual', icon: 'pi pi-palette', color: '#a855f7', resumo: resumoIdentidade.value },
|
||
{ key: 'perfil', label: 'Perfil Público', icon: 'pi pi-map-marker', color: '#3b82f6', resumo: resumoPerfil.value },
|
||
{ key: 'fluxo', label: 'Fluxo', icon: 'pi pi-sitemap', color: '#06b6d4', resumo: resumoFluxo.value },
|
||
{ key: 'pagamento', label: 'Pagamento', icon: 'pi pi-credit-card', color: '#10b981', resumo: resumoPagamento.value },
|
||
{ key: 'triagem', label: 'Triagem & LGPD', icon: 'pi pi-shield', color: '#f97316', resumo: resumoTriagem.value },
|
||
{ key: 'textos', label: 'Textos', icon: 'pi pi-file-edit', color: '#ec4899', resumo: resumoTextos.value }
|
||
]);
|
||
</script>
|
||
|
||
<template>
|
||
<Transition name="mag-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mag-mobile-drawer"
|
||
:class="{ 'is-open': drawerOpen }"
|
||
>
|
||
<div id="mag-mobile-drawer-target" class="mag-mobile-drawer__scroll" />
|
||
</div>
|
||
</Transition>
|
||
<Transition name="mag-drawer-fade">
|
||
<div
|
||
v-show="isMobile && drawerOpen"
|
||
class="mag-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 #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
|
||
class="mag-menu-btn mag-menu-btn--mobile-only"
|
||
v-tooltip.bottom="'Status & Resumos'"
|
||
@click="toggleDrawer"
|
||
>
|
||
<i class="pi pi-bars" />
|
||
<span>Menu</span>
|
||
</button>
|
||
<div class="mag-page__title">
|
||
<i class="pi pi-calendar-clock mag-page__title-icon" />
|
||
<span>Agendador Online</span>
|
||
<Tag value="PRO" severity="contrast" class="mag-page__pro" />
|
||
<Tag
|
||
v-if="cfg.ativo"
|
||
value="Ativo"
|
||
severity="success"
|
||
/>
|
||
<Tag
|
||
v-else
|
||
value="Inativo"
|
||
severity="secondary"
|
||
/>
|
||
</div>
|
||
<div class="mag-page__actions">
|
||
<button class="mag-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
||
<i class="pi pi-times" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="mag-subheader">
|
||
<i class="pi pi-info-circle mag-subheader__icon" />
|
||
<span class="mag-subheader__text">
|
||
Configure o link público que seus pacientes usarão para
|
||
solicitar horários online. Cada card abaixo tem seu próprio
|
||
botão de salvar.
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mag-body">
|
||
<Teleport to="#mag-mobile-drawer-target" :disabled="!isMobile">
|
||
<aside class="mag-side">
|
||
<button class="mag-cfg-btn" :class="{ 'is-open': cfgOpen }" @click="toggleCfg">
|
||
<i :class="cfgOpen ? 'pi pi-arrow-left' : 'pi pi-cog'" />
|
||
<span>{{ cfgOpen ? 'Voltar' : 'Configurações' }}</span>
|
||
<i v-if="!cfgOpen" class="pi pi-chevron-down mag-cfg-btn__chev" />
|
||
</button>
|
||
<div v-if="cfgOpen" class="mag-side__scroll mag-side__scroll--cfg">
|
||
<MelissaConfigList @select="fecharCfg" />
|
||
</div>
|
||
<div v-else class="mag-side__scroll">
|
||
<!-- Card: Status -->
|
||
<div class="mag-w mag-w--side">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon"><i class="pi pi-calendar-clock" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Status</div>
|
||
<div class="mag-w__sub">Ativar e link público</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<!-- Toggle ativar -->
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">
|
||
{{ cfg.ativo ? 'Agendador ativo' : 'Agendador inativo' }}
|
||
</div>
|
||
<div class="mag-toggle-row__hint">
|
||
<template v-if="hasAgendador">
|
||
{{ cfg.ativo
|
||
? 'Pacientes podem solicitar horários.'
|
||
: 'Ative pra liberar o link público.' }}
|
||
</template>
|
||
<template v-else>
|
||
Seu plano não inclui o Agendador Online.
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<ToggleSwitch
|
||
v-if="hasAgendador"
|
||
:modelValue="cfg.ativo"
|
||
@update:modelValue="toggleAtivo"
|
||
/>
|
||
<ToggleSwitch
|
||
v-else
|
||
:modelValue="false"
|
||
disabled
|
||
v-tooltip.left="'Seu plano não inclui o Agendador'"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Link público -->
|
||
<template v-if="cfg.ativo">
|
||
<div v-if="!cfg.link_slug" class="mag-link-loading">
|
||
<i class="pi pi-spin pi-spinner" />
|
||
<span>Gerando link…</span>
|
||
</div>
|
||
<template v-else>
|
||
<div class="mag-link-label">Link público</div>
|
||
<div class="mag-link-row">
|
||
<input
|
||
:value="linkPublico"
|
||
readonly
|
||
class="mag-link-input"
|
||
@click="$event.target.select()"
|
||
/>
|
||
<button
|
||
class="mag-icon-btn"
|
||
v-tooltip.top="linkCopied ? 'Copiado!' : 'Copiar'"
|
||
@click="copyLink"
|
||
>
|
||
<i :class="linkCopied ? 'pi pi-check' : 'pi pi-copy'" />
|
||
</button>
|
||
<button
|
||
class="mag-icon-btn"
|
||
v-tooltip.top="'Abrir'"
|
||
@click="abrirLinkPublico"
|
||
>
|
||
<i class="pi pi-external-link" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Slug personalizado -->
|
||
<div v-if="hasLinkPersonalizado" class="mag-slug">
|
||
<div class="mag-slug__label">Apelido personalizado</div>
|
||
<div class="mag-slug__row">
|
||
<span class="mag-slug__prefix">/agendar/</span>
|
||
<input
|
||
v-model="cfg.link_slug"
|
||
placeholder="dra-ana-silva"
|
||
class="mag-slug__input"
|
||
/>
|
||
</div>
|
||
<button
|
||
class="mag-btn mag-btn--primary mag-btn--full mag-btn--sm"
|
||
:disabled="savingCard === 'slug'"
|
||
@click="saveCard('slug')"
|
||
>
|
||
<i :class="savingCard === 'slug' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar apelido</span>
|
||
</button>
|
||
</div>
|
||
<div v-else class="mag-upgrade">
|
||
<i class="pi pi-lock" />
|
||
<div>
|
||
<strong>Apelido personalizado</strong> — disponível em planos superiores.
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
<div v-else class="mag-empty-link">
|
||
Ative o agendador acima pra gerar o link público.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Card: Resumos -->
|
||
<div class="mag-w mag-w--side">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon"><i class="pi pi-list" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Configurações</div>
|
||
<div class="mag-w__sub">Click pra ir à seção</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<button
|
||
v-for="s in summaryItems"
|
||
:key="s.key"
|
||
type="button"
|
||
class="mag-summary-item"
|
||
@click="scrollToCard(s.key)"
|
||
>
|
||
<div
|
||
class="mag-summary-item__icon"
|
||
:style="{
|
||
background: s.color + '22',
|
||
color: s.color
|
||
}"
|
||
>
|
||
<i :class="s.icon" />
|
||
</div>
|
||
<div class="mag-summary-item__text">
|
||
<div class="mag-summary-item__label">{{ s.label }}</div>
|
||
<div class="mag-summary-item__resumo">{{ s.resumo }}</div>
|
||
</div>
|
||
<i class="pi pi-chevron-right mag-summary-item__chev" />
|
||
</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}`">
|
||
<div class="mag-w__body">
|
||
<Skeleton width="40%" height="20px" class="mb-3" />
|
||
<Skeleton v-for="m in 4" :key="`sk-${n}-${m}`" width="100%" height="36px" class="mb-2" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<!-- ── Identidade Visual ── -->
|
||
<div id="mag-sec-identidade" class="mag-w">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--purple"><i class="pi pi-palette" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Identidade Visual</div>
|
||
<div class="mag-w__sub">Logomarca, cor e imagens</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-grid-2">
|
||
<div class="mag-field">
|
||
<label class="mag-label">Nome de exibição</label>
|
||
<InputText
|
||
v-model="cfg.nome_exibicao"
|
||
placeholder="Ex: Dra. Ana Silva — Psicóloga"
|
||
class="w-full"
|
||
/>
|
||
<span class="mag-hint">Aparece no topo do agendador.</span>
|
||
</div>
|
||
<div class="mag-field">
|
||
<label class="mag-label">Cor principal</label>
|
||
<div class="mag-color">
|
||
<ColorPicker v-model="cfg.cor_primaria" format="hex" />
|
||
<InputText
|
||
v-model="cfg.cor_primaria"
|
||
placeholder="#4b6bff"
|
||
class="mag-color__input"
|
||
maxlength="7"
|
||
/>
|
||
<div
|
||
class="mag-color__swatch"
|
||
:style="{ background: cfg.cor_primaria }"
|
||
/>
|
||
</div>
|
||
<span class="mag-hint">Botões e destaques.</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-uploads">
|
||
<!-- Logomarca -->
|
||
<div class="mag-upload">
|
||
<div class="mag-upload__head">
|
||
<i class="pi pi-user-circle" />
|
||
<span>Logomarca</span>
|
||
</div>
|
||
<div class="mag-upload__body">
|
||
<div class="mag-upload__hint">Avatar circular — 300×300</div>
|
||
<div v-if="cfg.logomarca_url" class="mag-upload__preview">
|
||
<img :src="cfg.logomarca_url" alt="Logomarca" class="mag-upload__img mag-upload__img--circle" />
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="mag-upload__zone"
|
||
@click="fileInputLogo?.click()"
|
||
>
|
||
<i :class="uploadingField === 'logomarca' ? 'pi pi-spin pi-spinner' : 'pi pi-upload'" />
|
||
<span>{{ uploadingField === 'logomarca' ? 'Enviando…' : 'Clique para enviar' }}</span>
|
||
</button>
|
||
<input
|
||
ref="fileInputLogo"
|
||
type="file"
|
||
accept="image/*"
|
||
hidden
|
||
@change="(e) => onFileSelected(e, 'logomarca')"
|
||
/>
|
||
<InputText
|
||
v-model="cfg.logomarca_url"
|
||
placeholder="ou cole uma URL…"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Header -->
|
||
<div class="mag-upload">
|
||
<div class="mag-upload__head">
|
||
<i class="pi pi-image" />
|
||
<span>Header</span>
|
||
</div>
|
||
<div class="mag-upload__body">
|
||
<div class="mag-upload__hint">Faixa superior — 1400×300</div>
|
||
<div v-if="cfg.imagem_header_url" class="mag-upload__preview">
|
||
<img :src="cfg.imagem_header_url" alt="Header" class="mag-upload__img" />
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="mag-upload__zone"
|
||
@click="fileInputHeader?.click()"
|
||
>
|
||
<i :class="uploadingField === 'header' ? 'pi pi-spin pi-spinner' : 'pi pi-upload'" />
|
||
<span>{{ uploadingField === 'header' ? 'Enviando…' : 'Clique para enviar' }}</span>
|
||
</button>
|
||
<input
|
||
ref="fileInputHeader"
|
||
type="file"
|
||
accept="image/*"
|
||
hidden
|
||
@change="(e) => onFileSelected(e, 'header')"
|
||
/>
|
||
<InputText
|
||
v-model="cfg.imagem_header_url"
|
||
placeholder="ou cole uma URL…"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Fundo -->
|
||
<div class="mag-upload">
|
||
<div class="mag-upload__head">
|
||
<i class="pi pi-stop" />
|
||
<span>Fundo</span>
|
||
</div>
|
||
<div class="mag-upload__body">
|
||
<div class="mag-upload__hint">Fundo da página — 1920×1080</div>
|
||
<div v-if="cfg.imagem_fundo_url" class="mag-upload__preview">
|
||
<img :src="cfg.imagem_fundo_url" alt="Fundo" class="mag-upload__img" />
|
||
</div>
|
||
<button
|
||
type="button"
|
||
class="mag-upload__zone"
|
||
@click="fileInputFundo?.click()"
|
||
>
|
||
<i :class="uploadingField === 'fundo' ? 'pi pi-spin pi-spinner' : 'pi pi-upload'" />
|
||
<span>{{ uploadingField === 'fundo' ? 'Enviando…' : 'Clique para enviar' }}</span>
|
||
</button>
|
||
<input
|
||
ref="fileInputFundo"
|
||
type="file"
|
||
accept="image/*"
|
||
hidden
|
||
@change="(e) => onFileSelected(e, 'fundo')"
|
||
/>
|
||
<InputText
|
||
v-model="cfg.imagem_fundo_url"
|
||
placeholder="ou cole uma URL…"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'identidade'"
|
||
@click="saveCard('identidade')"
|
||
>
|
||
<i :class="savingCard === 'identidade' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar identidade</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Perfil Público ── -->
|
||
<div id="mag-sec-perfil" class="mag-w">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--blue"><i class="pi pi-map-marker" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Perfil Público</div>
|
||
<div class="mag-w__sub">Endereço e botão "Como chegar"</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-field">
|
||
<label class="mag-label">Endereço</label>
|
||
<InputText
|
||
v-model="cfg.endereco"
|
||
placeholder="Rua das Flores, 123, Centro — São Paulo, SP"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">Botão "Como chegar"</div>
|
||
<div class="mag-toggle-row__hint">Abre o mapa pro paciente.</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.botao_como_chegar_ativo" />
|
||
</div>
|
||
|
||
<div v-if="cfg.botao_como_chegar_ativo" class="mag-field">
|
||
<label class="mag-label">URL do Google Maps <span class="mag-label__opt">(opcional)</span></label>
|
||
<InputText
|
||
v-model="cfg.maps_url"
|
||
placeholder="https://maps.google.com/..."
|
||
class="w-full"
|
||
/>
|
||
<span class="mag-hint">Vazio: abre uma busca pelo endereço acima.</span>
|
||
</div>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'perfil'"
|
||
@click="saveCard('perfil')"
|
||
>
|
||
<i :class="savingCard === 'perfil' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar perfil</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Fluxo de Agendamento ── -->
|
||
<div id="mag-sec-fluxo" class="mag-w">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--cyan"><i class="pi pi-sitemap" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Fluxo de Agendamento</div>
|
||
<div class="mag-w__sub">Aprovação, modalidade, tipos e duração</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-group-title">Aprovação</div>
|
||
<div class="mag-radio-list">
|
||
<button
|
||
v-for="opt in modoOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="mag-radio"
|
||
:class="{ 'is-active': cfg.modo_aprovacao === opt.value }"
|
||
@click="cfg.modo_aprovacao = opt.value"
|
||
>
|
||
<RadioButton :modelValue="cfg.modo_aprovacao" :value="opt.value" />
|
||
<div>
|
||
<div class="mag-radio__label">{{ opt.label }}</div>
|
||
<div class="mag-radio__desc">
|
||
<template v-if="opt.value === 'aprovacao'">
|
||
Você analisa cada solicitação. Horário fica reservado.
|
||
</template>
|
||
<template v-else>
|
||
Confirmados automaticamente. Sem revisão.
|
||
</template>
|
||
</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
|
||
<div v-if="cfg.modo_aprovacao === 'aprovacao'" class="mag-field">
|
||
<label class="mag-label">Prazo para responder</label>
|
||
<Select
|
||
v-model="cfg.prazo_resposta_horas"
|
||
:options="prazoRespostaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<span class="mag-hint">Sem resposta: paciente notificado e horário liberado.</span>
|
||
</div>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-group-title">Atendimento</div>
|
||
<div class="mag-field">
|
||
<label class="mag-label">Modalidade</label>
|
||
<SelectButton
|
||
v-model="cfg.modalidade"
|
||
:options="modalidadeOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mag-field">
|
||
<label class="mag-label">Tipos de agendamento</label>
|
||
<div class="mag-chips">
|
||
<button
|
||
v-for="opt in tiposOptions"
|
||
:key="opt.value"
|
||
type="button"
|
||
class="mag-chip"
|
||
:class="{ 'is-active': cfg.tipos_habilitados?.includes(opt.value) }"
|
||
@click="toggleTipo(opt.value)"
|
||
>
|
||
<i v-if="cfg.tipos_habilitados?.includes(opt.value)" class="pi pi-check" />
|
||
<span>{{ opt.label }}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-group-title">Horários</div>
|
||
<div class="mag-grid-2">
|
||
<div class="mag-field">
|
||
<label class="mag-label">Duração da sessão</label>
|
||
<Select
|
||
v-model="cfg.duracao_sessao_min"
|
||
:options="duracoesOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
<div class="mag-field">
|
||
<label class="mag-label">Antecedência mínima</label>
|
||
<Select
|
||
v-model="cfg.antecedencia_minima_horas"
|
||
:options="antecedenciaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'fluxo'"
|
||
@click="saveCard('fluxo')"
|
||
>
|
||
<i :class="savingCard === 'fluxo' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar fluxo</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Pagamento ── -->
|
||
<div id="mag-sec-pagamento" class="mag-w">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--green"><i class="pi pi-credit-card" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Pagamento</div>
|
||
<div class="mag-w__sub">Como o paciente vai pagar</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-radio-list">
|
||
<button
|
||
v-for="modo in modosPagamento"
|
||
:key="modo.value"
|
||
type="button"
|
||
class="mag-radio mag-radio--mode"
|
||
:class="{ 'is-active': cfg.pagamento_modo === modo.value }"
|
||
@click="cfg.pagamento_modo = modo.value"
|
||
>
|
||
<div
|
||
class="mag-radio__icon"
|
||
:class="{ 'is-active': cfg.pagamento_modo === modo.value }"
|
||
>
|
||
<i :class="['pi', modo.icon]" />
|
||
</div>
|
||
<div class="mag-radio__text">
|
||
<div class="mag-radio__label">{{ modo.label }}</div>
|
||
<div class="mag-radio__desc">{{ modo.desc }}</div>
|
||
</div>
|
||
<i v-if="cfg.pagamento_modo === modo.value" class="pi pi-check-circle mag-radio__check" />
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Pagar na hora -->
|
||
<template v-if="cfg.pagamento_modo === 'pagar_na_hora'">
|
||
<div class="mag-divider" />
|
||
<div class="mag-group-title">Formas aceitas</div>
|
||
<div v-if="!algumMetodoConfigurado" class="mag-warn">
|
||
<i class="pi pi-exclamation-triangle" />
|
||
<span>Nenhuma forma de pagamento configurada. Vá em Configurações › Pagamento.</span>
|
||
</div>
|
||
<div class="mag-methods">
|
||
<label
|
||
v-for="m in metodosDisponiveis"
|
||
:key="m.key"
|
||
class="mag-method"
|
||
:class="{
|
||
'is-disabled': !m.ativo,
|
||
'is-active': m.ativo && isMetodoVisivel(m.key)
|
||
}"
|
||
>
|
||
<Checkbox
|
||
:modelValue="isMetodoVisivel(m.key)"
|
||
:disabled="!m.ativo"
|
||
binary
|
||
@change="m.ativo && toggleMetodoVisivel(m.key)"
|
||
/>
|
||
<i :class="['pi', m.icon]" />
|
||
<span class="mag-method__label">{{ m.label }}</span>
|
||
<Tag v-if="!m.ativo" value="Não configurado" severity="secondary" class="mag-method__tag" />
|
||
</label>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Pix antecipado -->
|
||
<template v-if="cfg.pagamento_modo === 'pix_antecipado'">
|
||
<div class="mag-divider" />
|
||
<div class="mag-group-title">Configurações do Pix</div>
|
||
<div class="mag-grid-2">
|
||
<div class="mag-field">
|
||
<label class="mag-label">Chave Pix</label>
|
||
<InputText
|
||
v-model="cfg.pix_chave"
|
||
:placeholder="paymentSettings.pix_chave ? `Usando: ${paymentSettings.pix_chave}` : 'CPF, e-mail, telefone…'"
|
||
class="w-full"
|
||
/>
|
||
<span v-if="!cfg.pix_chave && paymentSettings.pix_chave" class="mag-hint">
|
||
Vazio: usa a chave de Pagamento ({{ paymentSettings.pix_chave }}).
|
||
</span>
|
||
</div>
|
||
<div class="mag-field">
|
||
<label class="mag-label">Tempo para pagar</label>
|
||
<Select
|
||
v-model="cfg.pix_countdown_minutos"
|
||
:options="pixCountdownOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<span class="mag-hint">Sem pagamento no prazo: horário liberado.</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-group-title">Reserva do horário</div>
|
||
<div class="mag-field">
|
||
<label class="mag-label">Tempo de reserva</label>
|
||
<Select
|
||
v-model="cfg.reserva_horas"
|
||
:options="reservaOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="w-full"
|
||
/>
|
||
<span class="mag-hint">
|
||
<template v-if="cfg.pagamento_modo === 'pix_antecipado'">
|
||
Horário bloqueado aguardando o pagamento Pix.
|
||
</template>
|
||
<template v-else>
|
||
Horário bloqueado enquanto a solicitação está pendente.
|
||
</template>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'pagamento'"
|
||
@click="saveCard('pagamento')"
|
||
>
|
||
<i :class="savingCard === 'pagamento' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar pagamento</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Triagem & LGPD ── -->
|
||
<div id="mag-sec-triagem" class="mag-w">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--orange"><i class="pi pi-shield" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Triagem & LGPD</div>
|
||
<div class="mag-w__sub">Campos extras e termos</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-group-title">Campos extras no formulário</div>
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">Motivo da consulta</div>
|
||
<div class="mag-toggle-row__hint">Texto livre opcional pro paciente.</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.triagem_motivo" />
|
||
</div>
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">Como nos conheceu?</div>
|
||
<div class="mag-toggle-row__hint">Origem (indicação, redes sociais, busca…).</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.triagem_como_conheceu" />
|
||
</div>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-group-title">Segurança & LGPD</div>
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">Verificação de e-mail</div>
|
||
<div class="mag-toggle-row__hint">Paciente confirma o e-mail antes de finalizar.</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.verificacao_email" />
|
||
</div>
|
||
<div class="mag-toggle-row">
|
||
<div class="mag-toggle-row__text">
|
||
<div class="mag-toggle-row__label">Aceite obrigatório de termos</div>
|
||
<div class="mag-toggle-row__hint">Checkbox de LGPD antes de finalizar.</div>
|
||
</div>
|
||
<ToggleSwitch v-model="cfg.exigir_aceite_lgpd" />
|
||
</div>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'triagem'"
|
||
@click="saveCard('triagem')"
|
||
>
|
||
<i :class="savingCard === 'triagem' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar triagem</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ── Textos da Jornada ── -->
|
||
<div id="mag-sec-textos" class="mag-w mag-w--full">
|
||
<div class="mag-w__head">
|
||
<div class="mag-w__icon mag-w__icon--pink"><i class="pi pi-file-edit" /></div>
|
||
<div class="mag-w__title">
|
||
<div class="mag-w__title-text">Textos da Jornada</div>
|
||
<div class="mag-w__sub">Mensagens exibidas ao paciente</div>
|
||
</div>
|
||
</div>
|
||
<div class="mag-w__body">
|
||
<div class="mag-field">
|
||
<label class="mag-label">Mensagem de boas-vindas</label>
|
||
<span class="mag-hint">Aparece após o paciente informar o nome.</span>
|
||
<JoditTextEditor
|
||
v-model="cfg.mensagem_boas_vindas"
|
||
:min-height="100"
|
||
/>
|
||
</div>
|
||
|
||
<div class="mag-divider" />
|
||
|
||
<div class="mag-field">
|
||
<label class="mag-label">Como se preparar para a sessão</label>
|
||
<span class="mag-hint">Aparece na confirmação. Dicas, instruções, etc.</span>
|
||
<JoditTextEditor
|
||
v-model="cfg.texto_como_se_preparar"
|
||
:min-height="140"
|
||
/>
|
||
</div>
|
||
|
||
<template v-if="cfg.exigir_aceite_lgpd">
|
||
<div class="mag-divider" />
|
||
<div class="mag-field">
|
||
<label class="mag-label">Termos de uso / Política de privacidade</label>
|
||
<span class="mag-hint">Aparece no checkbox de aceite.</span>
|
||
<JoditTextEditor
|
||
v-model="cfg.texto_termos_lgpd"
|
||
:min-height="140"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<div class="mag-card-actions">
|
||
<button
|
||
class="mag-btn mag-btn--primary"
|
||
:disabled="savingCard === 'textos'"
|
||
@click="saveCard('textos')"
|
||
>
|
||
<i :class="savingCard === 'textos' ? 'pi pi-spin pi-spinner' : 'pi pi-check'" />
|
||
<span>Salvar textos</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
</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>
|
||
/* ═══════ Page chrome ═══════ */
|
||
.mag-page {
|
||
position: absolute;
|
||
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) var(--m-config-aside-left, 6px);
|
||
z-index: 40;
|
||
display: flex;
|
||
flex-direction: column;
|
||
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);
|
||
overflow: hidden;
|
||
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
|
||
color: var(--m-text);
|
||
animation: mag-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
||
}
|
||
@keyframes mag-page-enter {
|
||
from { opacity: 0; transform: scale(0.985); }
|
||
to { opacity: 1; transform: scale(1); }
|
||
}
|
||
|
||
.mag-page__head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 14px 18px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
flex-shrink: 0;
|
||
gap: 10px;
|
||
}
|
||
.mag-page__title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
font-size: 1rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
flex-wrap: wrap;
|
||
}
|
||
.mag-page__title-icon { color: var(--p-primary-color); font-size: 1.05rem; }
|
||
.mag-page__pro { font-size: 0.66rem !important; }
|
||
.mag-page__actions { display: flex; align-items: center; gap: 6px; }
|
||
|
||
.mag-close {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: transparent;
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: background-color 120ms ease, color 120ms ease;
|
||
}
|
||
.mag-close:hover { background: var(--m-bg-soft-hover); color: var(--m-text); }
|
||
.mag-menu-btn {
|
||
display: none;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 7px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
||
|
||
.mag-subheader {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 18px;
|
||
background: var(--m-bg-soft);
|
||
border-bottom: 1px solid var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-subheader__icon {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.85rem;
|
||
margin-top: 2px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Body 2-col ═══════ */
|
||
.mag-body {
|
||
flex: 1;
|
||
min-height: 0;
|
||
display: flex;
|
||
overflow: hidden;
|
||
}
|
||
.mag-side {
|
||
width: 360px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background: var(--m-bg-soft);
|
||
border-right: 1px solid var(--m-border);
|
||
overflow: hidden;
|
||
}
|
||
.mag-side__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;
|
||
}
|
||
.mag-side__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mag-side__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Botao "Configuracoes" no topo da .mag-side. Click alterna entre
|
||
cards (default) e lista de configs (estado is-open: vira "Voltar"). */
|
||
.mag-cfg-btn {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: calc(100% - 24px);
|
||
margin: 12px 12px 0;
|
||
padding: 10px 12px;
|
||
background: var(--m-bg-medium);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 9px;
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.84rem;
|
||
font-weight: 600;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-cfg-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mag-cfg-btn.is-open {
|
||
background: color-mix(in srgb, var(--p-primary-color) 14%, transparent);
|
||
border-color: color-mix(in srgb, var(--p-primary-color) 38%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mag-cfg-btn > i:first-child {
|
||
color: var(--p-primary-color);
|
||
font-size: 0.92rem;
|
||
}
|
||
.mag-cfg-btn > span { flex: 1; }
|
||
.mag-cfg-btn__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
}
|
||
.mag-side__scroll--cfg {
|
||
padding: 8px;
|
||
gap: 0;
|
||
}
|
||
|
||
.mag-main {
|
||
flex: 1;
|
||
min-width: 0;
|
||
min-height: 0;
|
||
overflow-y: auto;
|
||
overflow-x: hidden;
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--m-border-strong) transparent;
|
||
}
|
||
.mag-main::-webkit-scrollbar { width: 5px; }
|
||
.mag-main::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
|
||
/* Desktop (>=1024): cards empilhados verticalmente (1-col), cada um
|
||
com altura propria do conteudo. O grid 2-col anterior forcava cells
|
||
na mesma row a igualarem altura — com align-self: start cards menores
|
||
ficavam no topo do cell e o espaco vazio embaixo dava aparencia de
|
||
"encavalado". 1-col com max-width centralizado eh mais legivel em
|
||
telas largas e respeita "100% do conteudo interno" pedido pelo user.
|
||
.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;
|
||
width: 100%;
|
||
}
|
||
.mag-main > .mag-w {
|
||
height: auto;
|
||
flex-shrink: 0;
|
||
}
|
||
}
|
||
|
||
/* ═══════ Card-base ═══════ */
|
||
.mag-w {
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mag-w--side {
|
||
background: var(--m-bg-medium);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
||
}
|
||
.mag-w__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 12px 14px;
|
||
border-bottom: 1px solid var(--m-border);
|
||
}
|
||
.mag-w__icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 9px;
|
||
flex-shrink: 0;
|
||
background: color-mix(in srgb, var(--p-primary-color) 15%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mag-w__icon > i { font-size: 0.95rem; }
|
||
.mag-w__icon--purple { background: color-mix(in srgb, #a855f7 15%, transparent); color: #a855f7; }
|
||
.mag-w__icon--blue { background: color-mix(in srgb, #3b82f6 15%, transparent); color: #3b82f6; }
|
||
.mag-w__icon--cyan { background: color-mix(in srgb, #06b6d4 15%, transparent); color: #06b6d4; }
|
||
.mag-w__icon--green { background: color-mix(in srgb, #10b981 15%, transparent); color: #10b981; }
|
||
.mag-w__icon--orange { background: color-mix(in srgb, #f97316 15%, transparent); color: #f97316; }
|
||
.mag-w__icon--pink { background: color-mix(in srgb, #ec4899 15%, transparent); color: #ec4899; }
|
||
.mag-w__title { flex: 1; min-width: 0; }
|
||
.mag-w__title-text {
|
||
font-size: 0.92rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
line-height: 1.2;
|
||
}
|
||
.mag-w__sub {
|
||
font-size: 0.74rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
.mag-w__body {
|
||
padding: 14px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
/* ═══════ Sidebar: Status ═══════ */
|
||
.mag-toggle-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
}
|
||
.mag-toggle-row__text { flex: 1; min-width: 0; }
|
||
.mag-toggle-row__label {
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mag-toggle-row__hint {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.mag-link-loading {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
}
|
||
.mag-link-label {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mag-link-row {
|
||
display: flex;
|
||
gap: 4px;
|
||
align-items: stretch;
|
||
}
|
||
.mag-link-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text);
|
||
padding: 7px 10px;
|
||
border-radius: 7px;
|
||
font-size: 0.72rem;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
outline: none;
|
||
}
|
||
.mag-link-input:focus {
|
||
border-color: var(--p-primary-color);
|
||
}
|
||
|
||
.mag-slug {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
padding-top: 8px;
|
||
border-top: 1px solid var(--m-border);
|
||
}
|
||
.mag-slug__label {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
.mag-slug__row {
|
||
display: flex;
|
||
align-items: stretch;
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 7px;
|
||
overflow: hidden;
|
||
background: var(--m-bg-soft);
|
||
}
|
||
.mag-slug__prefix {
|
||
padding: 7px 10px;
|
||
background: var(--m-bg-medium);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.72rem;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
border-right: 1px solid var(--m-border);
|
||
}
|
||
.mag-slug__input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--m-text);
|
||
padding: 7px 10px;
|
||
font-size: 0.78rem;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
outline: none;
|
||
}
|
||
|
||
.mag-upgrade {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: rgba(245, 158, 11, 0.10);
|
||
border: 1px dashed rgba(245, 158, 11, 0.40);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.74rem;
|
||
line-height: 1.4;
|
||
}
|
||
.mag-upgrade > i {
|
||
color: rgb(245, 158, 11);
|
||
font-size: 0.95rem;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.mag-empty-link {
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px dashed var(--m-border);
|
||
color: var(--m-text-muted);
|
||
font-size: 0.78rem;
|
||
text-align: center;
|
||
}
|
||
|
||
/* ═══════ Sidebar: Resumos ═══════ */
|
||
.mag-summary-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 8px 10px;
|
||
border-radius: 8px;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
text-align: left;
|
||
transition: background-color 120ms ease, border-color 120ms ease;
|
||
}
|
||
.mag-summary-item:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
border-color: var(--m-border-strong);
|
||
}
|
||
.mag-summary-item__icon {
|
||
width: 32px;
|
||
height: 32px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-summary-item__icon > i { font-size: 0.85rem; }
|
||
.mag-summary-item__text { flex: 1; min-width: 0; }
|
||
.mag-summary-item__label {
|
||
font-size: 0.82rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mag-summary-item__resumo {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.mag-summary-item__chev {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.7rem;
|
||
flex-shrink: 0;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
/* ═══════ Buttons ═══════ */
|
||
.mag-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 8px 14px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-size: 0.82rem;
|
||
font-weight: 600;
|
||
font-family: inherit;
|
||
transition: background-color 120ms ease, opacity 120ms ease;
|
||
}
|
||
.mag-btn:hover { background: var(--m-bg-soft-hover); }
|
||
.mag-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
||
.mag-btn--primary {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mag-btn--primary:hover {
|
||
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
||
}
|
||
.mag-btn--full { width: 100%; }
|
||
.mag-btn--sm { padding: 6px 10px; font-size: 0.78rem; }
|
||
|
||
.mag-icon-btn {
|
||
width: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
background: var(--m-bg-soft);
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 7px;
|
||
color: var(--m-text-muted);
|
||
cursor: pointer;
|
||
flex-shrink: 0;
|
||
transition: background-color 120ms ease, color 120ms ease;
|
||
}
|
||
.mag-icon-btn:hover {
|
||
background: var(--m-bg-soft-hover);
|
||
color: var(--m-text);
|
||
}
|
||
|
||
.mag-card-actions {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
padding-top: 4px;
|
||
border-top: 1px solid var(--m-border);
|
||
margin-top: 4px;
|
||
}
|
||
|
||
/* ═══════ Form ═══════ */
|
||
.mag-field {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
/* flex-shrink: 0 — em parent flex-col (.mag-w__body), garante que
|
||
fields nao colapsem. Originalmente adicionado pro PrimeVue Editor
|
||
(Quill) que media altura via JS e shrunk quando flex pai nao tinha
|
||
altura definida. Mantido apos troca pra Jodit por consistencia
|
||
(form fields nunca devem shrink em layout vertical). */
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-label {
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mag-label__opt {
|
||
font-weight: 400;
|
||
color: var(--m-text-muted);
|
||
opacity: 0.7;
|
||
}
|
||
.mag-hint {
|
||
font-size: 0.7rem;
|
||
color: var(--m-text-muted);
|
||
line-height: 1.3;
|
||
}
|
||
.mag-grid-2 {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 14px;
|
||
}
|
||
.mag-divider {
|
||
height: 1px;
|
||
background: var(--m-border);
|
||
margin: 4px 0;
|
||
}
|
||
.mag-group-title {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.06em;
|
||
color: var(--m-text-muted);
|
||
}
|
||
|
||
/* Color picker */
|
||
.mag-color {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.mag-color__input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||
}
|
||
.mag-color__swatch {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 7px;
|
||
border: 1px solid var(--m-border);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Uploads ═══════ */
|
||
.mag-uploads {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: 10px;
|
||
}
|
||
@media (min-width: 700px) {
|
||
.mag-uploads { grid-template-columns: repeat(3, 1fr); }
|
||
}
|
||
.mag-upload {
|
||
border: 1px solid var(--m-border);
|
||
border-radius: 9px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.mag-upload__head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 7px 10px;
|
||
background: var(--m-bg-medium);
|
||
border-bottom: 1px solid var(--m-border);
|
||
font-size: 0.78rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mag-upload__head > i { font-size: 0.78rem; opacity: 0.6; }
|
||
.mag-upload__body {
|
||
padding: 10px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.mag-upload__hint {
|
||
font-size: 0.68rem;
|
||
color: var(--m-text-muted);
|
||
opacity: 0.7;
|
||
}
|
||
.mag-upload__preview {
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
.mag-upload__img {
|
||
max-width: 100%;
|
||
height: 56px;
|
||
border-radius: 6px;
|
||
border: 1px solid var(--m-border);
|
||
object-fit: cover;
|
||
}
|
||
.mag-upload__img--circle {
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
}
|
||
.mag-upload__zone {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding: 10px;
|
||
border: 1.5px dashed var(--m-border);
|
||
border-radius: 7px;
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text-muted);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.74rem;
|
||
font-weight: 600;
|
||
transition: border-color 120ms ease, background-color 120ms ease;
|
||
}
|
||
.mag-upload__zone:hover {
|
||
border-color: var(--p-primary-color);
|
||
background: color-mix(in srgb, var(--p-primary-color) 6%, transparent);
|
||
}
|
||
|
||
/* ═══════ Radio cards ═══════ */
|
||
.mag-radio-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
.mag-radio {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
font-family: inherit;
|
||
transition: border-color 120ms ease, background-color 120ms ease;
|
||
}
|
||
.mag-radio:hover { border-color: var(--m-border-strong); }
|
||
.mag-radio.is-active {
|
||
border-color: var(--p-primary-color);
|
||
background: color-mix(in srgb, var(--p-primary-color) 6%, transparent);
|
||
}
|
||
.mag-radio__label {
|
||
font-size: 0.85rem;
|
||
font-weight: 700;
|
||
color: var(--m-text);
|
||
}
|
||
.mag-radio__desc {
|
||
font-size: 0.72rem;
|
||
color: var(--m-text-muted);
|
||
margin-top: 2px;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.mag-radio--mode { gap: 12px; }
|
||
.mag-radio__icon {
|
||
width: 36px;
|
||
height: 36px;
|
||
display: grid;
|
||
place-items: center;
|
||
border-radius: 9px;
|
||
background: var(--m-bg-medium);
|
||
color: var(--m-text-muted);
|
||
flex-shrink: 0;
|
||
}
|
||
.mag-radio__icon.is-active {
|
||
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
||
color: var(--p-primary-color);
|
||
}
|
||
.mag-radio__icon > i { font-size: 1rem; }
|
||
.mag-radio__text { flex: 1; min-width: 0; }
|
||
.mag-radio__check {
|
||
color: var(--p-primary-color);
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* ═══════ Chips (tipos de agendamento) ═══════ */
|
||
.mag-chips {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 6px;
|
||
}
|
||
.mag-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 5px;
|
||
padding: 6px 12px;
|
||
border-radius: 999px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
color: var(--m-text);
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
transition: all 120ms ease;
|
||
}
|
||
.mag-chip:hover { border-color: var(--m-border-strong); }
|
||
.mag-chip.is-active {
|
||
background: var(--p-primary-color);
|
||
border-color: var(--p-primary-color);
|
||
color: var(--p-primary-contrast-color, #fff);
|
||
}
|
||
.mag-chip > i { font-size: 0.7rem; }
|
||
|
||
/* ═══════ Methods (pagar na hora) ═══════ */
|
||
.mag-methods {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
.mag-method {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
padding: 10px 12px;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--m-border);
|
||
background: var(--m-bg-soft);
|
||
cursor: pointer;
|
||
font-size: 0.85rem;
|
||
transition: background-color 120ms ease, border-color 120ms ease;
|
||
}
|
||
.mag-method:hover { background: var(--m-bg-soft-hover); }
|
||
.mag-method.is-active {
|
||
border-color: var(--p-primary-color);
|
||
background: color-mix(in srgb, var(--p-primary-color) 6%, transparent);
|
||
}
|
||
.mag-method.is-disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
}
|
||
.mag-method > i {
|
||
color: var(--m-text-muted);
|
||
font-size: 0.92rem;
|
||
}
|
||
.mag-method__label { flex: 1; font-weight: 600; }
|
||
.mag-method__tag { font-size: 0.66rem !important; }
|
||
|
||
.mag-warn {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 8px;
|
||
padding: 10px 12px;
|
||
border-radius: 9px;
|
||
background: rgba(245, 158, 11, 0.10);
|
||
border: 1px solid rgba(245, 158, 11, 0.30);
|
||
color: rgb(180, 83, 9);
|
||
font-size: 0.78rem;
|
||
line-height: 1.4;
|
||
}
|
||
.mag-warn > i { font-size: 0.92rem; margin-top: 2px; flex-shrink: 0; }
|
||
|
||
/* PrimeVue Editor (Quill) trocado por JoditTextEditor — Jodit traz
|
||
CSS proprio (jodit/es2021/jodit.min.css importado no componente)
|
||
e suporta dark/light via theme prop. Sem override scoped aqui. */
|
||
|
||
/* ═══════ Mobile drawer ═══════ */
|
||
.mag-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;
|
||
}
|
||
.mag-mobile-drawer.is-open { transform: translateX(0); }
|
||
.mag-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;
|
||
}
|
||
.mag-mobile-drawer__scroll::-webkit-scrollbar { width: 5px; }
|
||
.mag-mobile-drawer__scroll::-webkit-scrollbar-thumb {
|
||
background: var(--m-border-strong);
|
||
border-radius: 3px;
|
||
}
|
||
/* No mobile a .mag-side e teleportada pra dentro do drawer scroll. */
|
||
.mag-mobile-drawer__scroll .mag-side {
|
||
width: 100%;
|
||
border-right: none;
|
||
}
|
||
|
||
.mag-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;
|
||
}
|
||
.mag-drawer-fade-enter-active,
|
||
.mag-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
||
.mag-drawer-fade-enter-from,
|
||
.mag-drawer-fade-leave-to { opacity: 0; }
|
||
|
||
/* ═══════ Mobile (<1024px) ═══════ */
|
||
@media (max-width: 1023px) {
|
||
.mag-body { flex-direction: column; padding: 0; }
|
||
.mag-body > .mag-side { display: none; }
|
||
.mag-main { width: 100%; padding: 8px; }
|
||
.mag-main .mag-w {
|
||
height: auto;
|
||
flex: 0 0 auto;
|
||
align-self: stretch;
|
||
}
|
||
.mag-grid-2 { grid-template-columns: 1fr; }
|
||
.mag-page__title > span:first-of-type { display: none; }
|
||
.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>
|