7c0c1b3528
Aplica o mesmo fix do MelissaPerfil/Negocio: - align-items: start no grid (cells nao stretch p/ row height) - Cards min-height 300px + max-height 100% (do container) - .mpl-w__body / .map-w__body com flex: 1 + min-height: 0 + overflow-y: auto (scrollbar fina) MelissaPlano: vale pros 2 cards do main (Recursos / Historico) e pros 2 cards da sidebar (Plano atual / Resumo) — quando o user tem features longas ou muitos eventos, scroll interno engata. MelissaAlterarPlano: aplicado SO na sidebar (Plano atual / Filtros). Os plan cards do main (.map-plan) ficam fora — sao product cards com layout proprio (preco grande + 3 CTAs). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1380 lines
47 KiB
Vue
1380 lines
47 KiB
Vue
<script setup>
|
|
/*
|
|
* MelissaPlano — Pagina nativa Melissa pra "Meu Plano".
|
|
*
|
|
* Substitui o embed cfg-plano que vivia dentro do MelissaConfiguracoes.
|
|
* Layout 2-col (espelha MelissaPerfil):
|
|
* - COL 1 (sidebar) — Card "Plano atual" (nome + status + valor +
|
|
* periodo + ID) + Card "Resumo" (mini-stats: recursos, eventos,
|
|
* proxima renovacao) + Footer com CTAs (Alterar plano + Atualizar)
|
|
* - COL 2 (main) — Card "Seu plano inclui" (features agrupadas por
|
|
* modulo) + Card "Historico" (subscription_events com badges).
|
|
*
|
|
* Logica de fetch espelhada de TherapistMeuPlanoPage.vue (subscriptions
|
|
* + plans + plan_prices + plan_features + features + subscription_events
|
|
* + profiles).
|
|
*/
|
|
import { ref, computed, onMounted } from 'vue';
|
|
import { useRouter } from 'vue-router';
|
|
import { useToast } from 'primevue/usetoast';
|
|
import { supabase } from '@/lib/supabase/client';
|
|
import { useTenantStore } from '@/stores/tenantStore';
|
|
// Tag/Skeleton: auto via PrimeVueResolver
|
|
|
|
const emit = defineEmits(['close']);
|
|
|
|
const router = useRouter();
|
|
const toast = useToast();
|
|
const tenantStore = useTenantStore();
|
|
|
|
// ── Breakpoints + drawer ───────────────────────────────────
|
|
const drawerOpen = ref(false);
|
|
const isMobile = ref(false);
|
|
let _mqMobile = null;
|
|
function _onMqMobileChange(e) {
|
|
isMobile.value = e.matches;
|
|
if (!e.matches) drawerOpen.value = false;
|
|
}
|
|
function toggleDrawer() { drawerOpen.value = !drawerOpen.value; }
|
|
function fecharDrawer() { drawerOpen.value = false; }
|
|
|
|
// ── Estado ─────────────────────────────────────────────────
|
|
const loading = ref(false);
|
|
const hasLoaded = ref(false);
|
|
const subscription = ref(null);
|
|
const plan = ref(null);
|
|
const price = ref(null);
|
|
const features = ref([]);
|
|
const events = ref([]);
|
|
const plans = ref([]);
|
|
const profiles = ref([]);
|
|
|
|
// ── Formatters ─────────────────────────────────────────────
|
|
function money(currency, amountCents) {
|
|
if (amountCents == null) return null;
|
|
const value = Number(amountCents) / 100;
|
|
try {
|
|
return new Intl.NumberFormat('pt-BR', {
|
|
style: 'currency',
|
|
currency: currency || 'BRL'
|
|
}).format(value);
|
|
} catch (_) {
|
|
return `${value.toFixed(2)} ${currency || ''}`.trim();
|
|
}
|
|
}
|
|
|
|
function fmtDate(iso) {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return String(iso);
|
|
return d.toLocaleString('pt-BR');
|
|
}
|
|
function fmtDateShort(iso) {
|
|
if (!iso) return '—';
|
|
const d = new Date(iso);
|
|
if (Number.isNaN(d.getTime())) return String(iso);
|
|
return d.toLocaleDateString('pt-BR');
|
|
}
|
|
|
|
function prettyMeta(meta) {
|
|
if (!meta) return null;
|
|
try {
|
|
if (typeof meta === 'string') return meta;
|
|
return JSON.stringify(meta, null, 2);
|
|
} catch (_) {
|
|
return String(meta);
|
|
}
|
|
}
|
|
|
|
function statusSeverity(st) {
|
|
const s = String(st || '').toLowerCase();
|
|
if (s === 'active') return 'success';
|
|
if (s === 'trialing') return 'info';
|
|
if (s === 'past_due') return 'warning';
|
|
if (s === 'canceled' || s === 'cancelled') return 'danger';
|
|
return 'secondary';
|
|
}
|
|
function statusLabel(st) {
|
|
const s = String(st || '').toLowerCase();
|
|
if (s === 'active') return 'Ativo';
|
|
if (s === 'trialing') return 'Trial';
|
|
if (s === 'past_due') return 'Atrasado';
|
|
if (s === 'canceled' || s === 'cancelled') return 'Cancelado';
|
|
return st || '—';
|
|
}
|
|
function eventSeverity(t) {
|
|
const k = String(t || '').toLowerCase();
|
|
if (k === 'plan_changed') return 'info';
|
|
if (k === 'canceled') return 'danger';
|
|
if (k === 'reactivated') return 'success';
|
|
return 'secondary';
|
|
}
|
|
function eventLabel(t) {
|
|
const k = String(t || '').toLowerCase();
|
|
if (k === 'plan_changed') return 'Plano alterado';
|
|
if (k === 'canceled') return 'Cancelada';
|
|
if (k === 'reactivated') return 'Reativada';
|
|
return t || '—';
|
|
}
|
|
|
|
// ── Computed ───────────────────────────────────────────────
|
|
const planName = computed(() => plan.value?.name || subscription.value?.plan_key || '—');
|
|
const intervalLabel = computed(() => {
|
|
const i = subscription.value?.interval;
|
|
if (i === 'month') return 'mês';
|
|
if (i === 'year') return 'ano';
|
|
return i || '—';
|
|
});
|
|
const priceLabel = computed(() => {
|
|
if (!price.value) return null;
|
|
return `${money(price.value.currency, price.value.amount_cents)} / ${intervalLabel.value}`;
|
|
});
|
|
const periodLabel = computed(() => {
|
|
const s = subscription.value;
|
|
if (!s?.current_period_start || !s?.current_period_end) return '—';
|
|
return `${fmtDateShort(s.current_period_start)} → ${fmtDateShort(s.current_period_end)}`;
|
|
});
|
|
const renewLabel = computed(() => {
|
|
const s = subscription.value;
|
|
if (!s?.current_period_end) return '—';
|
|
return fmtDateShort(s.current_period_end);
|
|
});
|
|
|
|
// ── Features agrupadas ─────────────────────────────────────
|
|
function moduleFromKey(key) {
|
|
const k = String(key || '').trim();
|
|
if (!k) return 'Outros';
|
|
if (k.includes('.')) return k.split('.')[0] || 'Outros';
|
|
if (k.includes('_')) return k.split('_')[0] || 'Outros';
|
|
return 'Outros';
|
|
}
|
|
function moduleLabel(m) {
|
|
const s = String(m || '').trim();
|
|
if (!s) return 'Outros';
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
const groupedFeatures = computed(() => {
|
|
const list = features.value || [];
|
|
const map = new Map();
|
|
for (const f of list) {
|
|
const mod = moduleFromKey(f.key);
|
|
if (!map.has(mod)) map.set(mod, []);
|
|
map.get(mod).push(f);
|
|
}
|
|
const modules = Array.from(map.keys()).sort((a, b) => {
|
|
if (a === 'Outros') return 1;
|
|
if (b === 'Outros') return -1;
|
|
return a.localeCompare(b);
|
|
});
|
|
return modules.map((mod) => {
|
|
const items = map.get(mod) || [];
|
|
items.sort((a, b) => String(a.key || '').localeCompare(String(b.key || '')));
|
|
return { module: mod, items };
|
|
});
|
|
});
|
|
|
|
// ── Historico ──────────────────────────────────────────────
|
|
const planById = computed(() => {
|
|
const m = new Map();
|
|
for (const p of plans.value || []) m.set(String(p.id), p);
|
|
return m;
|
|
});
|
|
function planKeyOrName(planId) {
|
|
if (!planId) return '—';
|
|
const p = planById.value.get(String(planId));
|
|
return p?.key || p?.name || String(planId);
|
|
}
|
|
const profileById = computed(() => {
|
|
const m = new Map();
|
|
for (const p of profiles.value || []) m.set(String(p.id), p);
|
|
return m;
|
|
});
|
|
function displayUser(userId) {
|
|
if (!userId) return '—';
|
|
const p = profileById.value.get(String(userId));
|
|
if (!p) return String(userId);
|
|
const name = p.nome || p.name || p.full_name || p.display_name || null;
|
|
const email = p.email || p.email_principal || null;
|
|
if (name && email) return `${name} <${email}>`;
|
|
return name || email || String(userId);
|
|
}
|
|
|
|
// ── Actions ────────────────────────────────────────────────
|
|
function goUpgrade() {
|
|
// Vai pra pagina nativa Melissa de alterar plano (substitui o
|
|
// /therapist/upgrade que virava redirect e perdia o overlay Melissa).
|
|
router.push({ name: 'Melissa', params: { secao: 'alterar-plano' } });
|
|
}
|
|
|
|
// ── Fetch ──────────────────────────────────────────────────
|
|
async function fetchMeuPlano() {
|
|
loading.value = true;
|
|
try {
|
|
const { data: authData, error: authError } = await supabase.auth.getUser();
|
|
if (authError) throw authError;
|
|
const uid = authData?.user?.id;
|
|
if (!uid) throw new Error('Sessão não encontrada.');
|
|
|
|
const sRes = await supabase
|
|
.from('subscriptions')
|
|
.select('*')
|
|
.eq('user_id', uid)
|
|
.order('created_at', { ascending: false })
|
|
.limit(10);
|
|
if (sRes.error) throw sRes.error;
|
|
|
|
const subList = sRes.data || [];
|
|
const priority = (st) => {
|
|
const s = String(st || '').toLowerCase();
|
|
if (s === 'active') return 1;
|
|
if (s === 'trialing') return 2;
|
|
if (s === 'past_due') return 3;
|
|
if (s === 'unpaid') return 4;
|
|
if (s === 'incomplete') return 5;
|
|
if (s === 'canceled' || s === 'cancelled') return 9;
|
|
return 8;
|
|
};
|
|
subscription.value = subList.length
|
|
? subList.slice().sort((a, b) => {
|
|
const pa = priority(a?.status);
|
|
const pb = priority(b?.status);
|
|
if (pa !== pb) return pa - pb;
|
|
return new Date(b?.created_at || 0) - new Date(a?.created_at || 0);
|
|
})[0]
|
|
: null;
|
|
|
|
if (!subscription.value) {
|
|
plan.value = null;
|
|
price.value = null;
|
|
features.value = [];
|
|
events.value = [];
|
|
plans.value = [];
|
|
profiles.value = [];
|
|
return;
|
|
}
|
|
|
|
const pRes = await supabase
|
|
.from('plans')
|
|
.select('id, key, name, description')
|
|
.eq('id', subscription.value.plan_id)
|
|
.maybeSingle();
|
|
if (pRes.error) throw pRes.error;
|
|
plan.value = pRes.data || null;
|
|
|
|
const nowIso = new Date().toISOString();
|
|
const ppRes = await supabase
|
|
.from('plan_prices')
|
|
.select('currency, interval, amount_cents, is_active, active_from, active_to')
|
|
.eq('plan_id', subscription.value.plan_id)
|
|
.eq('interval', subscription.value.interval)
|
|
.eq('is_active', true)
|
|
.lte('active_from', nowIso)
|
|
.or(`active_to.is.null,active_to.gte.${nowIso}`)
|
|
.order('active_from', { ascending: false })
|
|
.limit(1)
|
|
.maybeSingle();
|
|
if (ppRes.error) throw ppRes.error;
|
|
price.value = ppRes.data || null;
|
|
|
|
const pfRes = await supabase
|
|
.from('plan_features')
|
|
.select('feature_id')
|
|
.eq('plan_id', subscription.value.plan_id);
|
|
if (pfRes.error) throw pfRes.error;
|
|
const featureIds = (pfRes.data || []).map((r) => r.feature_id).filter(Boolean);
|
|
|
|
if (featureIds.length) {
|
|
const fRes = await supabase
|
|
.from('features')
|
|
.select('id, key, description, descricao')
|
|
.in('id', featureIds)
|
|
.order('key', { ascending: true });
|
|
if (fRes.error) throw fRes.error;
|
|
features.value = (fRes.data || []).map((f) => ({
|
|
key: f.key,
|
|
description: (f.descricao || f.description || '').trim()
|
|
}));
|
|
} else {
|
|
features.value = [];
|
|
}
|
|
|
|
const eRes = await supabase
|
|
.from('subscription_events')
|
|
.select('*')
|
|
.eq('subscription_id', subscription.value.id)
|
|
.order('created_at', { ascending: false })
|
|
.limit(50);
|
|
if (eRes.error) throw eRes.error;
|
|
events.value = eRes.data || [];
|
|
|
|
const planIds = new Set();
|
|
if (subscription.value?.plan_id) planIds.add(String(subscription.value.plan_id));
|
|
for (const ev of events.value) {
|
|
if (ev?.old_plan_id) planIds.add(String(ev.old_plan_id));
|
|
if (ev?.new_plan_id) planIds.add(String(ev.new_plan_id));
|
|
}
|
|
if (planIds.size) {
|
|
const { data: pAll, error: epAll } = await supabase
|
|
.from('plans')
|
|
.select('id,key,name')
|
|
.in('id', Array.from(planIds));
|
|
plans.value = epAll ? [] : pAll || [];
|
|
} else {
|
|
plans.value = [];
|
|
}
|
|
|
|
const userIds = new Set();
|
|
for (const ev of events.value) {
|
|
const by = String(ev.created_by || '').trim();
|
|
if (by) userIds.add(by);
|
|
}
|
|
if (userIds.size) {
|
|
const { data: pr, error: epr } = await supabase
|
|
.from('profiles')
|
|
.select('*')
|
|
.in('id', Array.from(userIds));
|
|
profiles.value = epr ? [] : pr || [];
|
|
} else {
|
|
profiles.value = [];
|
|
}
|
|
} catch (e) {
|
|
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || String(e), life: 5000 });
|
|
} finally {
|
|
loading.value = false;
|
|
hasLoaded.value = true;
|
|
}
|
|
}
|
|
|
|
// ── 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); }
|
|
}
|
|
await tenantStore.ensureLoaded();
|
|
await fetchMeuPlano();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<Transition name="mpl-drawer-fade">
|
|
<div
|
|
v-show="isMobile && drawerOpen"
|
|
class="mpl-mobile-drawer"
|
|
:class="{ 'is-open': drawerOpen }"
|
|
>
|
|
<div id="mpl-mobile-drawer-target" class="mpl-mobile-drawer__scroll" />
|
|
</div>
|
|
</Transition>
|
|
<Transition name="mpl-drawer-fade">
|
|
<div
|
|
v-show="isMobile && drawerOpen"
|
|
class="mpl-mobile-drawer__backdrop"
|
|
@click="fecharDrawer"
|
|
/>
|
|
</Transition>
|
|
|
|
<section class="mpl-page">
|
|
<header class="mpl-page__head">
|
|
<button
|
|
class="mpl-menu-btn mpl-menu-btn--mobile-only"
|
|
v-tooltip.bottom="'Plano & Acoes'"
|
|
@click="toggleDrawer"
|
|
>
|
|
<i class="pi pi-bars" />
|
|
<span>Menu Plano</span>
|
|
</button>
|
|
<div class="mpl-page__title">
|
|
<i class="pi pi-credit-card mpl-page__title-icon" />
|
|
<span>Meu Plano</span>
|
|
<Tag
|
|
v-if="subscription"
|
|
:value="statusLabel(subscription.status)"
|
|
:severity="statusSeverity(subscription.status)"
|
|
/>
|
|
</div>
|
|
<div class="mpl-page__actions">
|
|
<button
|
|
class="mpl-act-btn"
|
|
v-tooltip.bottom="'Atualizar'"
|
|
:disabled="loading"
|
|
@click="fetchMeuPlano"
|
|
>
|
|
<i :class="loading ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'" />
|
|
</button>
|
|
<button class="mpl-close" v-tooltip.bottom="'Voltar (Esc)'" @click="emit('close')">
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<div class="mpl-subheader">
|
|
<i class="pi pi-info-circle mpl-subheader__icon" />
|
|
<span class="mpl-subheader__text">
|
|
Sua assinatura, recursos inclusos e histórico de mudanças. Pra trocar
|
|
de plano, clique em <strong>Alterar plano</strong> na lateral.
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mpl-body">
|
|
<!-- ═══ COL 1: Sidebar ═══ -->
|
|
<Teleport to="#mpl-mobile-drawer-target" :disabled="!isMobile">
|
|
<aside class="mpl-side">
|
|
<div class="mpl-side__scroll">
|
|
<!-- Card: Plano atual -->
|
|
<div class="mpl-w mpl-w--side">
|
|
<div class="mpl-w__head">
|
|
<div class="mpl-w__icon"><i class="pi pi-credit-card" /></div>
|
|
<div class="mpl-w__title">
|
|
<div class="mpl-w__title-text">Plano atual</div>
|
|
<div class="mpl-w__sub">Assinatura ativa e ciclo de cobrança</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mpl-w__body">
|
|
<template v-if="loading && !hasLoaded">
|
|
<Skeleton width="60%" height="22px" class="mb-2" />
|
|
<Skeleton width="100%" height="14px" class="mb-1" />
|
|
<Skeleton width="80%" height="14px" />
|
|
</template>
|
|
|
|
<template v-else-if="!subscription">
|
|
<div class="mpl-empty">
|
|
<i class="pi pi-credit-card mpl-empty__icon" />
|
|
<div class="mpl-empty__title">Nenhuma assinatura</div>
|
|
<div class="mpl-empty__hint">
|
|
Escolha um plano pra começar.
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else>
|
|
<div class="mpl-plan">
|
|
<div class="mpl-plan__name">{{ planName }}</div>
|
|
<div class="mpl-plan__price">
|
|
{{ priceLabel || 'Preço não disponível' }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mpl-info">
|
|
<div class="mpl-info__row">
|
|
<span class="mpl-info__label">Status</span>
|
|
<Tag
|
|
:value="statusLabel(subscription.status)"
|
|
:severity="statusSeverity(subscription.status)"
|
|
/>
|
|
</div>
|
|
<div class="mpl-info__row">
|
|
<span class="mpl-info__label">Ciclo atual</span>
|
|
<span class="mpl-info__value">{{ periodLabel }}</span>
|
|
</div>
|
|
<div class="mpl-info__row">
|
|
<span class="mpl-info__label">
|
|
{{ subscription.cancel_at_period_end ? 'Encerra em' : 'Renova em' }}
|
|
</span>
|
|
<span class="mpl-info__value">{{ renewLabel }}</span>
|
|
</div>
|
|
<div class="mpl-info__row">
|
|
<span class="mpl-info__label">Renovação</span>
|
|
<Tag
|
|
v-if="subscription.cancel_at_period_end"
|
|
value="Cancelamento agendado"
|
|
severity="warning"
|
|
/>
|
|
<Tag
|
|
v-else
|
|
value="Automática"
|
|
severity="success"
|
|
/>
|
|
</div>
|
|
<div v-if="plan?.description" class="mpl-info__row mpl-info__row--full">
|
|
<span class="mpl-info__label">Descrição</span>
|
|
<span class="mpl-info__desc">{{ plan.description }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card: Resumo (mini stats) -->
|
|
<div v-if="subscription" class="mpl-w mpl-w--side">
|
|
<div class="mpl-w__head">
|
|
<div class="mpl-w__icon"><i class="pi pi-chart-pie" /></div>
|
|
<div class="mpl-w__title">
|
|
<div class="mpl-w__title-text">Resumo</div>
|
|
<div class="mpl-w__sub">Indicadores rápidos</div>
|
|
</div>
|
|
</div>
|
|
<div class="mpl-w__body">
|
|
<div class="mpl-stats">
|
|
<div class="mpl-stat">
|
|
<div class="mpl-stat__val">{{ features.length }}</div>
|
|
<div class="mpl-stat__lbl">Recursos</div>
|
|
</div>
|
|
<div class="mpl-stat">
|
|
<div class="mpl-stat__val">{{ events.length }}</div>
|
|
<div class="mpl-stat__lbl">Eventos</div>
|
|
</div>
|
|
<div class="mpl-stat">
|
|
<div class="mpl-stat__val mpl-stat__val--small">{{ renewLabel }}</div>
|
|
<div class="mpl-stat__lbl">{{ subscription.cancel_at_period_end ? 'Encerra' : 'Renova' }}</div>
|
|
</div>
|
|
</div>
|
|
<div class="mpl-id-row">
|
|
<span class="mpl-id-row__label">ID da assinatura</span>
|
|
<code class="mpl-id-row__code">{{ subscription.id }}</code>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mpl-side__footer">
|
|
<button class="mpl-btn mpl-btn--primary mpl-btn--full" @click="goUpgrade">
|
|
<i class="pi pi-arrow-up-right" />
|
|
<span>{{ subscription ? 'Alterar plano' : 'Ver planos' }}</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
</Teleport>
|
|
|
|
<!-- ═══ COL 2: Main ═══ -->
|
|
<div class="mpl-main">
|
|
<!-- Empty state -->
|
|
<div v-if="!loading && !subscription" class="mpl-w">
|
|
<div class="mpl-w__head">
|
|
<div class="mpl-w__icon"><i class="pi pi-info-circle" /></div>
|
|
<div class="mpl-w__title">
|
|
<div class="mpl-w__title-text">Sem assinatura ativa</div>
|
|
<div class="mpl-w__sub">Você ainda não tem um plano contratado</div>
|
|
</div>
|
|
</div>
|
|
<div class="mpl-w__body">
|
|
<div class="mpl-empty mpl-empty--big">
|
|
<i class="pi pi-credit-card mpl-empty__icon" />
|
|
<div class="mpl-empty__title">Escolha um plano pra começar</div>
|
|
<div class="mpl-empty__hint">
|
|
Ative todos os recursos do Agência PSI.
|
|
</div>
|
|
<button class="mpl-btn mpl-btn--primary" @click="goUpgrade">
|
|
<i class="pi pi-arrow-up-right" />
|
|
<span>Ver planos</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading skeleton -->
|
|
<template v-if="loading && !hasLoaded">
|
|
<div class="mpl-w" v-for="n in 2" :key="`sk-${n}`">
|
|
<div class="mpl-w__body">
|
|
<Skeleton width="40%" height="20px" class="mb-3" />
|
|
<Skeleton v-for="m in 4" :key="`sk-${n}-${m}`" width="100%" height="32px" class="mb-2" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template v-else-if="subscription">
|
|
<!-- ── Recursos do plano ── -->
|
|
<div class="mpl-w">
|
|
<div class="mpl-w__head">
|
|
<div class="mpl-w__icon"><i class="pi pi-check-circle" /></div>
|
|
<div class="mpl-w__title">
|
|
<div class="mpl-w__title-text">Seu plano inclui</div>
|
|
<div class="mpl-w__sub">Recursos liberados pelo plano contratado</div>
|
|
</div>
|
|
<span v-if="features.length" class="mpl-w__count">{{ features.length }}</span>
|
|
</div>
|
|
<div class="mpl-w__body">
|
|
<div v-if="!features.length" class="mpl-empty mpl-empty--small">
|
|
<i class="pi pi-info-circle" />
|
|
Nenhum recurso vinculado.
|
|
</div>
|
|
|
|
<div v-else class="mpl-features">
|
|
<div
|
|
v-for="g in groupedFeatures"
|
|
:key="g.module"
|
|
class="mpl-feat-group"
|
|
>
|
|
<div class="mpl-feat-group__head">
|
|
<span class="mpl-feat-group__label">{{ moduleLabel(g.module) }}</span>
|
|
<div class="mpl-feat-group__line" />
|
|
<span class="mpl-feat-group__count">{{ g.items.length }}</span>
|
|
</div>
|
|
<div class="mpl-feat-list">
|
|
<div
|
|
v-for="f in g.items"
|
|
:key="f.key"
|
|
class="mpl-feat"
|
|
:title="f.description || f.key"
|
|
>
|
|
<i class="pi pi-check-circle mpl-feat__check" />
|
|
<div class="mpl-feat__text">
|
|
<div class="mpl-feat__key">{{ f.key }}</div>
|
|
<div v-if="f.description" class="mpl-feat__desc">
|
|
{{ f.description }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── Historico ── -->
|
|
<div class="mpl-w">
|
|
<div class="mpl-w__head">
|
|
<div class="mpl-w__icon"><i class="pi pi-history" /></div>
|
|
<div class="mpl-w__title">
|
|
<div class="mpl-w__title-text">Histórico</div>
|
|
<div class="mpl-w__sub">Mudanças, cancelamentos e reativações</div>
|
|
</div>
|
|
<span v-if="events.length" class="mpl-w__count">{{ events.length }}</span>
|
|
</div>
|
|
<div class="mpl-w__body">
|
|
<div v-if="!events.length" class="mpl-empty mpl-empty--small">
|
|
<i class="pi pi-history" />
|
|
Sem eventos registrados.
|
|
</div>
|
|
|
|
<div v-else class="mpl-events">
|
|
<div v-for="ev in events" :key="ev.id" class="mpl-event">
|
|
<div class="mpl-event__head">
|
|
<Tag
|
|
:value="eventLabel(ev.event_type)"
|
|
:severity="eventSeverity(ev.event_type)"
|
|
/>
|
|
<span class="mpl-event__date">{{ fmtDate(ev.created_at) }}</span>
|
|
</div>
|
|
<div v-if="ev.old_plan_id || ev.new_plan_id" class="mpl-event__diff">
|
|
<span class="mpl-event__plan">{{ planKeyOrName(ev.old_plan_id) }}</span>
|
|
<i class="pi pi-arrow-right" />
|
|
<span class="mpl-event__plan">{{ planKeyOrName(ev.new_plan_id) }}</span>
|
|
</div>
|
|
<div class="mpl-event__by">
|
|
por <b>{{ displayUser(ev.created_by) }}</b>
|
|
</div>
|
|
<div v-if="ev.reason" class="mpl-event__reason">{{ ev.reason }}</div>
|
|
<pre v-if="ev.metadata" class="mpl-event__meta">{{ prettyMeta(ev.metadata) }}</pre>
|
|
</div>
|
|
<div class="mpl-events__hint">Mostrando até 50 eventos (mais recentes).</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ═══════ Page chrome (mesmo pattern do MelissaPerfil) ═══════ */
|
|
.mpl-page {
|
|
position: absolute;
|
|
inset: 6px 6px calc(var(--m-dock-h, 76px) + 6px) 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: mpl-page-enter 240ms cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
}
|
|
@keyframes mpl-page-enter {
|
|
from { opacity: 0; transform: scale(0.985); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
|
|
.mpl-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;
|
|
}
|
|
.mpl-page__title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 1rem;
|
|
font-weight: 700;
|
|
color: var(--m-text);
|
|
}
|
|
.mpl-page__title-icon {
|
|
color: var(--p-primary-color);
|
|
font-size: 1.05rem;
|
|
}
|
|
.mpl-page__actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.mpl-act-btn {
|
|
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;
|
|
}
|
|
.mpl-act-btn:hover {
|
|
background: var(--m-bg-soft-hover);
|
|
color: var(--m-text);
|
|
}
|
|
.mpl-act-btn:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
.mpl-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;
|
|
}
|
|
.mpl-close:hover {
|
|
background: var(--m-bg-soft-hover);
|
|
color: var(--m-text);
|
|
}
|
|
.mpl-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;
|
|
}
|
|
.mpl-menu-btn:hover { background: var(--m-bg-soft-hover); }
|
|
|
|
.mpl-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;
|
|
}
|
|
.mpl-subheader__icon {
|
|
color: var(--p-primary-color);
|
|
font-size: 0.85rem;
|
|
margin-top: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* ═══════ Body 2-col ═══════ */
|
|
.mpl-body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
.mpl-side {
|
|
width: 320px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--m-bg-soft);
|
|
border-right: 1px solid var(--m-border);
|
|
overflow: hidden;
|
|
}
|
|
.mpl-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;
|
|
}
|
|
.mpl-side__scroll::-webkit-scrollbar { width: 5px; }
|
|
.mpl-side__scroll::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
.mpl-side__footer {
|
|
flex-shrink: 0;
|
|
padding: 12px;
|
|
background: var(--m-bg-medium);
|
|
border-top: 1px solid var(--m-border);
|
|
}
|
|
|
|
.mpl-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;
|
|
}
|
|
.mpl-main::-webkit-scrollbar { width: 5px; }
|
|
.mpl-main::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Desktop (>=1024): main em grid 2-col, cards min-h 300 + max-h 100%
|
|
+ body com overflow-y: auto. Sidebar (Plano atual + Resumo) ganha
|
|
o mesmo cap pra nao crescer demais quando tem features/eventos longos. */
|
|
@media (min-width: 1024px) {
|
|
.mpl-main {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 12px;
|
|
align-items: start;
|
|
align-content: start;
|
|
}
|
|
.mpl-main > .mpl-w {
|
|
min-height: 300px;
|
|
max-height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mpl-side > .mpl-side__scroll > .mpl-w--side {
|
|
min-height: 300px;
|
|
max-height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mpl-main > .mpl-w > .mpl-w__body,
|
|
.mpl-side .mpl-w--side > .mpl-w__body {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--m-border-strong) transparent;
|
|
}
|
|
.mpl-main > .mpl-w > .mpl-w__body::-webkit-scrollbar,
|
|
.mpl-side .mpl-w--side > .mpl-w__body::-webkit-scrollbar { width: 5px; }
|
|
.mpl-main > .mpl-w > .mpl-w__body::-webkit-scrollbar-thumb,
|
|
.mpl-side .mpl-w--side > .mpl-w__body::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
}
|
|
|
|
/* ═══════ Card-base (estilo MelissaFinanceiro/MelissaPerfil) ═══════ */
|
|
.mpl-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;
|
|
}
|
|
.mpl-w--side {
|
|
background: var(--m-bg-medium);
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.16);
|
|
}
|
|
.mpl-w__head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 12px 14px;
|
|
border-bottom: 1px solid var(--m-border);
|
|
}
|
|
.mpl-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);
|
|
}
|
|
.mpl-w__icon > i { font-size: 0.95rem; }
|
|
.mpl-w__title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.mpl-w__title-text {
|
|
font-size: 0.92rem;
|
|
font-weight: 700;
|
|
color: var(--m-text);
|
|
line-height: 1.2;
|
|
}
|
|
.mpl-w__sub {
|
|
font-size: 0.74rem;
|
|
color: var(--m-text-muted);
|
|
margin-top: 2px;
|
|
line-height: 1.3;
|
|
}
|
|
.mpl-w__count {
|
|
flex-shrink: 0;
|
|
background: color-mix(in srgb, var(--p-primary-color) 18%, transparent);
|
|
color: var(--p-primary-color);
|
|
border-radius: 999px;
|
|
padding: 3px 10px;
|
|
font-size: 0.74rem;
|
|
font-weight: 700;
|
|
}
|
|
.mpl-w__body {
|
|
padding: 14px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* ═══════ Buttons ═══════ */
|
|
.mpl-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;
|
|
}
|
|
.mpl-btn:hover { background: var(--m-bg-soft-hover); }
|
|
.mpl-btn--primary {
|
|
background: var(--p-primary-color);
|
|
border-color: var(--p-primary-color);
|
|
color: var(--p-primary-contrast-color, #fff);
|
|
}
|
|
.mpl-btn--primary:hover {
|
|
background: color-mix(in srgb, var(--p-primary-color) 88%, black);
|
|
}
|
|
.mpl-btn--full { width: 100%; }
|
|
|
|
/* ═══════ Sidebar: Plano atual ═══════ */
|
|
.mpl-plan {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 12px;
|
|
border-radius: 9px;
|
|
background: color-mix(in srgb, var(--p-primary-color) 8%, transparent);
|
|
border: 1px solid color-mix(in srgb, var(--p-primary-color) 25%, transparent);
|
|
}
|
|
.mpl-plan__name {
|
|
font-size: 1.1rem;
|
|
font-weight: 800;
|
|
color: var(--p-primary-color);
|
|
line-height: 1.1;
|
|
}
|
|
.mpl-plan__price {
|
|
font-size: 0.78rem;
|
|
color: var(--m-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.mpl-info {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.mpl-info__row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
}
|
|
.mpl-info__row--full {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 3px;
|
|
}
|
|
.mpl-info__label {
|
|
font-size: 0.7rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--m-text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
.mpl-info__value {
|
|
font-size: 0.78rem;
|
|
font-weight: 600;
|
|
color: var(--m-text);
|
|
text-align: right;
|
|
word-break: break-word;
|
|
}
|
|
.mpl-info__desc {
|
|
font-size: 0.78rem;
|
|
color: var(--m-text);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* ═══════ Sidebar: Resumo (mini stats) ═══════ */
|
|
.mpl-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 8px;
|
|
}
|
|
.mpl-stat {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
padding: 10px 6px;
|
|
border-radius: 9px;
|
|
background: var(--m-bg-soft);
|
|
border: 1px solid var(--m-border);
|
|
text-align: center;
|
|
}
|
|
.mpl-stat__val {
|
|
font-size: 1.1rem;
|
|
font-weight: 800;
|
|
color: var(--m-text);
|
|
line-height: 1.1;
|
|
}
|
|
.mpl-stat__val--small {
|
|
font-size: 0.8rem;
|
|
}
|
|
.mpl-stat__lbl {
|
|
font-size: 0.66rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--m-text-muted);
|
|
}
|
|
|
|
.mpl-id-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
padding-top: 4px;
|
|
border-top: 1px solid var(--m-border);
|
|
}
|
|
.mpl-id-row__label {
|
|
font-size: 0.66rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: var(--m-text-muted);
|
|
}
|
|
.mpl-id-row__code {
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
font-size: 0.7rem;
|
|
color: var(--m-text-muted);
|
|
word-break: break-all;
|
|
user-select: all;
|
|
}
|
|
|
|
/* ═══════ Empty states ═══════ */
|
|
.mpl-empty {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 16px 8px;
|
|
text-align: center;
|
|
}
|
|
.mpl-empty--small {
|
|
flex-direction: row;
|
|
padding: 12px;
|
|
color: var(--m-text-muted);
|
|
font-size: 0.85rem;
|
|
}
|
|
.mpl-empty--small > i { color: var(--p-primary-color); }
|
|
.mpl-empty--big {
|
|
padding: 36px 24px;
|
|
gap: 10px;
|
|
}
|
|
.mpl-empty__icon {
|
|
font-size: 2rem;
|
|
color: var(--m-text-faint);
|
|
opacity: 0.6;
|
|
}
|
|
.mpl-empty--big .mpl-empty__icon { font-size: 2.5rem; }
|
|
.mpl-empty__title {
|
|
font-size: 0.92rem;
|
|
font-weight: 700;
|
|
color: var(--m-text);
|
|
}
|
|
.mpl-empty__hint {
|
|
font-size: 0.78rem;
|
|
color: var(--m-text-muted);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* ═══════ Recursos do plano (features grouped) ═══════ */
|
|
.mpl-features {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
.mpl-feat-group__head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
}
|
|
.mpl-feat-group__label {
|
|
font-size: 0.66rem;
|
|
font-weight: 700;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--p-primary-color);
|
|
}
|
|
.mpl-feat-group__line {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--m-border);
|
|
}
|
|
.mpl-feat-group__count {
|
|
font-size: 0.66rem;
|
|
font-weight: 700;
|
|
color: var(--m-text-muted);
|
|
background: var(--m-bg-medium);
|
|
border: 1px solid var(--m-border);
|
|
border-radius: 999px;
|
|
padding: 1px 7px;
|
|
}
|
|
.mpl-feat-list {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 4px;
|
|
}
|
|
@media (min-width: 600px) {
|
|
.mpl-feat-list { grid-template-columns: 1fr 1fr; }
|
|
}
|
|
.mpl-feat {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 8px;
|
|
padding: 6px 8px;
|
|
border-radius: 6px;
|
|
transition: background-color 120ms ease;
|
|
}
|
|
.mpl-feat:hover { background: var(--m-bg-medium); }
|
|
.mpl-feat__check {
|
|
color: rgb(22, 163, 74);
|
|
font-size: 0.85rem;
|
|
margin-top: 2px;
|
|
flex-shrink: 0;
|
|
}
|
|
.mpl-feat__text { min-width: 0; flex: 1; }
|
|
.mpl-feat__key {
|
|
font-size: 0.82rem;
|
|
font-weight: 600;
|
|
color: var(--m-text);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.mpl-feat__desc {
|
|
font-size: 0.7rem;
|
|
color: var(--m-text-muted);
|
|
line-height: 1.3;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* ═══════ Historico (events) ═══════ */
|
|
.mpl-events {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.mpl-event {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
padding: 10px 12px;
|
|
border-radius: 9px;
|
|
background: var(--m-bg-medium);
|
|
border: 1px solid var(--m-border);
|
|
}
|
|
.mpl-event__head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.mpl-event__date {
|
|
font-size: 0.7rem;
|
|
color: var(--m-text-muted);
|
|
}
|
|
.mpl-event__diff {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.78rem;
|
|
color: var(--m-text-muted);
|
|
flex-wrap: wrap;
|
|
}
|
|
.mpl-event__diff > i { font-size: 0.68rem; opacity: 0.6; }
|
|
.mpl-event__plan {
|
|
color: var(--m-text);
|
|
font-weight: 600;
|
|
}
|
|
.mpl-event__by {
|
|
font-size: 0.74rem;
|
|
color: var(--m-text-muted);
|
|
}
|
|
.mpl-event__by > b { color: var(--m-text); font-weight: 600; }
|
|
.mpl-event__reason {
|
|
font-size: 0.74rem;
|
|
color: var(--m-text-muted);
|
|
font-style: italic;
|
|
}
|
|
.mpl-event__meta {
|
|
margin: 4px 0 0;
|
|
padding: 6px 8px;
|
|
border-radius: 6px;
|
|
background: var(--m-bg-soft);
|
|
font-size: 0.68rem;
|
|
color: var(--m-text-muted);
|
|
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
max-height: 120px;
|
|
overflow-y: auto;
|
|
}
|
|
.mpl-events__hint {
|
|
font-size: 0.7rem;
|
|
color: var(--m-text-muted);
|
|
text-align: center;
|
|
padding-top: 4px;
|
|
}
|
|
|
|
/* ═══════ Mobile drawer ═══════ */
|
|
.mpl-mobile-drawer {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
height: 100vh;
|
|
height: 100dvh;
|
|
width: min(360px, 88vw);
|
|
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;
|
|
}
|
|
.mpl-mobile-drawer.is-open { transform: translateX(0); }
|
|
.mpl-mobile-drawer__scroll {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mpl-mobile-drawer__scroll .mpl-side {
|
|
flex: 1;
|
|
min-height: 0;
|
|
width: 100%;
|
|
overflow: hidden;
|
|
background: transparent;
|
|
border-right: none;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.mpl-mobile-drawer__scroll .mpl-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;
|
|
}
|
|
.mpl-mobile-drawer__scroll .mpl-side__scroll::-webkit-scrollbar { width: 5px; }
|
|
.mpl-mobile-drawer__scroll .mpl-side__scroll::-webkit-scrollbar-thumb {
|
|
background: var(--m-border-strong);
|
|
border-radius: 3px;
|
|
}
|
|
.mpl-mobile-drawer__scroll .mpl-w--side {
|
|
margin: 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.mpl-mobile-drawer__scroll .mpl-side__footer {
|
|
flex-shrink: 0;
|
|
margin: 0;
|
|
padding: 12px;
|
|
background: var(--m-bg-medium);
|
|
border-top: 1px solid var(--m-border);
|
|
}
|
|
|
|
.mpl-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;
|
|
}
|
|
.mpl-drawer-fade-enter-active,
|
|
.mpl-drawer-fade-leave-active { transition: opacity 200ms ease; }
|
|
.mpl-drawer-fade-enter-from,
|
|
.mpl-drawer-fade-leave-to { opacity: 0; }
|
|
|
|
/* ═══════ Mobile (<1024px) ═══════ */
|
|
@media (max-width: 1023px) {
|
|
.mpl-body {
|
|
flex-direction: column;
|
|
padding: 0;
|
|
}
|
|
.mpl-side { display: none; }
|
|
.mpl-main {
|
|
width: 100%;
|
|
padding: 8px;
|
|
}
|
|
.mpl-main .mpl-w {
|
|
height: auto;
|
|
flex: 0 0 auto;
|
|
align-self: stretch;
|
|
}
|
|
.mpl-page__title > span:first-of-type { display: none; }
|
|
.mpl-page__title-icon { display: none; }
|
|
.mpl-menu-btn--mobile-only { display: inline-flex; }
|
|
}
|
|
</style>
|