Files
agenciapsilmno/src/layout/melissa/MelissaMenu.vue
T
Leonardo d8968d9aec MelissaMenu: hover/active primary nos itens do rodape
Hover e is-active dos .mm-foot-item agora usam --p-primary-color (text +
icone + bg color-mix 12-16%). Trocado "Meus Planos" -> "Meu Plano"
(singular). Cada item ganha is-active baseado em props.secaoAtiva
(perfil/plano/negocio/seguranca), Modo escuro fica is-active quando
isDarkTheme = true, Cores do Tema mantem is-active baseado em
themeViewActive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:24:53 -03:00

1398 lines
47 KiB
Vue

<script setup>
/*
* MelissaMenu — Menu float estilo "Start" do Win11
* --------------------------------------------------
* Modelo categoria → sub-itens em grupos:
* - LEFT (~280px): lista de categorias
* - RIGHT (~360px): grupos de sub-itens da categoria ativa
*
* Sub-item especial `tipo: 'link-cadastro'` renderiza inline
* (input com link + botão copiar) em vez de virar item navegável.
*
* Default: primeira categoria selecionada na abertura.
*
* Props:
* - secaoAtiva: string|null — destaca o sub-item correspondente
*
* Emit:
* - select(key) — sub-item clicado (parent decide navegação)
* - close — clique fora ou X
*/
import { ref, computed, watch } from 'vue';
import { useRouter } from 'vue-router';
import { sessionUser, sessionRole } from '@/app/session';
import { supabase } from '@/lib/supabase/client';
import { useRoleGuard } from '@/composables/useRoleGuard';
import { useLayout } from '@/layout/composables/layout';
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence';
import { primaryColors, surfaces, presetOptions, applyThemeEngine } from '@/theme/theme.options';
const props = defineProps({
secaoAtiva: { type: String, default: null }
});
const emit = defineEmits(['select', 'close']);
const router = useRouter();
const { role } = useRoleGuard();
const { layoutConfig, toggleDarkMode, isDarkTheme } = useLayout();
const { queuePatch } = useUserSettingsPersistence();
// ── Catálogo de categorias ────────────────────────────────────
// Quando promover Melissa pra produção, mover pra um composable que
// também filtra por entitlements/feature flags do tenant.
const CATEGORIAS = [
{
key: 'agenda-pacientes',
label: 'Agenda e Pacientes',
icon: 'pi pi-calendar-plus',
color: '#10b981',
groups: [
{
title: 'Principais',
items: [
{ key: 'agenda', label: 'Minha Agenda', icon: 'pi pi-calendar' },
{ key: 'pacientes', label: 'Meus Pacientes', icon: 'pi pi-users' },
{ key: 'cadastros-recebidos', label: 'Cadastros recebidos', icon: 'pi pi-inbox' },
{ key: 'agendamentos-recebidos', label: 'Agendamentos recebidos', icon: 'pi pi-bell' },
{ key: 'meu-link-cadastro', label: 'Meu link de cadastro', icon: 'pi pi-link', tipo: 'link-cadastro' }
]
},
{
title: 'Outros',
items: [
{ key: 'recorrencias', label: 'Recorrências', icon: 'pi pi-sync' },
{ key: 'compromissos', label: 'Compromissos determinados', icon: 'pi pi-flag' },
{ key: 'grupos', label: 'Grupos de pacientes', icon: 'pi pi-th-large' },
{ key: 'tags', label: 'Tags', icon: 'pi pi-tag' },
{ key: 'medicos', label: 'Médicos e referências', icon: 'pi pi-user-edit' },
{ key: 'online-scheduling', label: 'Agendador online', icon: 'pi pi-calendar-clock' },
{ key: 'link-externo', label: 'Link externo de cadastro', icon: 'pi pi-share-alt' }
]
}
]
},
{
key: 'whatsapp',
label: 'WhatsApp',
icon: 'pi pi-whatsapp',
color: '#22c55e',
groups: [
{
title: 'Atendimento',
items: [
{ key: 'conversas', label: 'Conversas', icon: 'pi pi-comments' },
{ key: 'notificacoes', label: 'Notificações enviadas', icon: 'pi pi-bell' }
]
},
{
title: 'Configuração',
items: [
{ key: 'wa-canal', label: 'Configurar canal', icon: 'pi pi-cog', route: { name: 'ConfiguracoesWhatsapp' } },
{ key: 'wa-templates', label: 'Templates de mensagem', icon: 'pi pi-file-edit', route: { name: 'ConfiguracoesWhatsappTemplates' } },
{ key: 'wa-creditos', label: 'Créditos', icon: 'pi pi-credit-card', route: { name: 'ConfiguracoesCreditosWhatsapp' } }
]
}
]
},
{
key: 'prontuarios',
label: 'Prontuários',
icon: 'pi pi-file',
color: '#0ea5e9',
groups: [
{
title: 'Acesso',
items: [
// Sem route — emit('select', 'pacientes') aciona o MelissaPacientes
// (lá o duplo-click no card abre PatientProntuario). Mantém o
// user dentro do Melissa em vez de jogar pra rota externa.
{ key: 'pacientes', label: 'Abrir por paciente', icon: 'pi pi-users' }
]
},
{
title: 'Documentos',
items: [
{ key: 'documentos', label: 'Documentos', icon: 'pi pi-file' },
{ key: 'documentos-templates', label: 'Templates de documentos', icon: 'pi pi-file-edit' }
]
}
]
},
{
key: 'financeiro',
label: 'Financeiro',
icon: 'pi pi-wallet',
color: '#f59e0b',
groups: [
{
title: 'Principais',
items: [
// Sem route — abre embedado via MelissaEmbed dentro do overlay Melissa
{ key: 'financeiro', label: 'Visão geral', icon: 'pi pi-chart-line' },
{ key: 'financeiro-lancamentos', label: 'Lançamentos', icon: 'pi pi-list' }
]
},
{
title: 'Análise',
items: [
{ key: 'relatorios', label: 'Relatórios', icon: 'pi pi-chart-bar' }
]
}
]
},
{
key: 'configuracoes',
label: 'Configurações',
icon: 'pi pi-cog',
color: '#94a3b8',
groups: [
{
title: 'Layout Melissa',
items: [
// Sem `route` — emit('select', 'aparencia') abre página interna do Melissa
{ key: 'aparencia', label: 'Aparência e cronômetro', icon: 'pi pi-palette' }
]
},
{
title: 'Agenda',
items: [
{ key: 'cfg-agenda', label: 'Agenda', icon: 'pi pi-calendar', route: { name: 'ConfiguracoesAgenda' } },
{ key: 'cfg-agendador', label: 'Agendador externo', icon: 'pi pi-link', route: { name: 'ConfiguracoesAgendador' } }
]
},
{
title: 'WhatsApp',
items: [
{ key: 'cfg-wa', label: 'Canal de WhatsApp', icon: 'pi pi-whatsapp', route: { name: 'ConfiguracoesWhatsapp' } },
{ key: 'cfg-wa-templates', label: 'Templates', icon: 'pi pi-file-edit', route: { name: 'ConfiguracoesWhatsappTemplates' } }
]
}
]
}
];
// ── Estado ──────────────────────────────────────────────────────
// Resolve a categoria que contem o sub-item ativo. Se nenhum casa,
// volta pra primeira (agenda-pacientes) — comportamento default.
function categoryKeyFor(itemKey) {
if (!itemKey) return null;
for (const cat of CATEGORIAS) {
for (const group of cat.groups) {
if (group.items.some((it) => it.key === itemKey)) return cat.key;
}
}
return null;
}
const selectedKey = ref(categoryKeyFor(props.secaoAtiva) || CATEGORIAS[0].key);
const copiado = ref(false);
// Sincroniza o destaque com a sessao ativa — se o user troca de
// "Lancamentos" pra "Agenda" sem fechar o menu, o selecionado
// acompanha a sessao corrente.
watch(
() => props.secaoAtiva,
(val) => {
const cat = categoryKeyFor(val);
if (cat) selectedKey.value = cat;
}
);
// Drill-down mobile: false = lista de categorias, true = sub-itens da
// categoria escolhida. CSS controla visibilidade via translateX em <lg.
// Em desktop o flag é ignorado (ambas colunas sempre visíveis).
const mobileSubView = ref(false);
const categoriaAtiva = computed(() =>
CATEGORIAS.find((c) => c.key === selectedKey.value) || CATEGORIAS[0]
);
// TODO: trocar por busca real do convite ativo (patient_invites)
// O AgendaTerapeutaPage faz isso ~ linha 919.
const linkCadastro = 'https://app.agenciapsi.com.br/cadastrar/abc123';
function selecionarCategoria(key) {
selectedKey.value = key;
copiado.value = false;
themeViewActive.value = false; // sai do tema ao mudar categoria
mobileSubView.value = true; // drill-down em mobile
}
function voltarParaCategorias() {
mobileSubView.value = false;
themeViewActive.value = false;
}
function clicarSubItem(item) {
if (item.tipo === 'link-cadastro') return; // inline, não navega
// Se item tem route definida, navega direto (rota externa ao Melissa).
// Senão, emite 'select' pro pai decidir (seções internas ao MelissaLayout).
if (item.route) {
emit('close');
safePush(item.route);
return;
}
emit('select', item.key);
}
async function copiarLink() {
try {
await navigator.clipboard.writeText(linkCadastro);
copiado.value = true;
setTimeout(() => (copiado.value = false), 1800);
} catch {
// Falha silenciosa (browser sem permissão de clipboard)
}
}
// ── User session (avatar, label, role) ─────────────────────────
// Pattern espelhado de src/layout/AppMenuFooterPanel.vue
const userInitials = computed(() => {
const name = sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || '';
const parts = String(name).trim().split(/\s+/).filter(Boolean);
const a = parts[0]?.[0] || 'U';
const b = parts.length > 1 ? parts[parts.length - 1][0] : '';
return (a + b).toUpperCase();
});
const userLabel = computed(
() => sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || 'Conta'
);
const userSublabel = computed(() => {
const r = role.value || sessionRole.value;
if (!r) return 'Sessão';
if (r === 'clinic_admin' || r === 'tenant_admin' || r === 'admin') return 'Administrador';
if (r === 'therapist') return 'Terapeuta';
if (r === 'supervisor') return 'Supervisor';
if (r === 'portal_user' || r === 'patient') return 'Portal';
return r;
});
const userAvatarUrl = computed(() => sessionUser.value?.user_metadata?.avatar_url || null);
// ── Footer actions ─────────────────────────────────────────────
async function safePush(target, fallback) {
try {
const r = router.resolve(target);
if (r?.matched?.length) return await router.push(target);
} catch {}
if (fallback) {
try { return await router.push(fallback); } catch {}
}
return router.push('/');
}
function navAndClose(target, fallback) {
emit('close');
safePush(target, fallback);
}
// Atalhos de Conta — abrem embedados dentro do MelissaConfiguracoes
// (em vez de navegar pra rota externa). Cada um vira uma section pré-
// selecionada na sidebar de configs.
function goPerfil() { emit('select', 'perfil'); emit('close'); }
function goPlano() { emit('select', 'plano'); emit('close'); }
function goNegocio() { emit('select', 'negocio'); emit('close'); }
function goSeguranca() { emit('select', 'seguranca'); emit('close'); }
async function toggleDarkAndPersist() {
try {
toggleDarkMode();
// requestAnimationFrame pra garantir que a classe já foi aplicada
await new Promise((r) => requestAnimationFrame(r));
const after = document.documentElement.classList.contains('app-dark');
const theme_mode = after ? 'dark' : 'light';
try { localStorage.setItem('ui_theme_mode', theme_mode); } catch {}
await queuePatch({ theme_mode }, { flushNow: true });
} catch (e) {
// eslint-disable-next-line no-console
console.warn('[MelissaMenu] toggleDark falhou:', e);
}
}
// ── Cores do Tema (embutido no aside, não em popover externo) ──
const themeViewActive = ref(false);
function toggleThemeView() {
themeViewActive.value = !themeViewActive.value;
if (themeViewActive.value) mobileSubView.value = true; // drill-down em mobile
}
function saveThemeToStorage() {
try {
localStorage.setItem('ui_theme_config', JSON.stringify({
preset: layoutConfig.preset,
primary: layoutConfig.primary,
surface: layoutConfig.surface,
menuMode: layoutConfig.menuMode
}));
} catch {}
}
function setPrimary(c) {
layoutConfig.primary = c.name;
applyThemeEngine(layoutConfig);
queuePatch?.({ primary_color: c.name });
saveThemeToStorage();
}
function setSurface(s) {
layoutConfig.surface = s.name;
applyThemeEngine(layoutConfig);
queuePatch?.({ surface_color: s.name });
saveThemeToStorage();
}
function setPreset(p) {
if (!p || p === layoutConfig.preset) return;
layoutConfig.preset = p;
applyThemeEngine(layoutConfig);
queuePatch?.({ preset: p });
saveThemeToStorage();
}
function surfaceIsActive(s) {
if (layoutConfig.surface) return layoutConfig.surface === s.name;
// fallback default por mode
return isDarkTheme.value ? s.name === 'zinc' : s.name === 'slate';
}
async function sair() {
try { await supabase.auth.signOut(); } catch {}
router.push('/auth/login');
}
</script>
<template>
<div class="mm-layer" @click.self="emit('close')">
<div class="mm-panel" :class="{ 'is-mobile-sub': mobileSubView }">
<!-- ESQUERDA: categorias -->
<nav class="mm-side">
<div class="mm-side__head">
<div class="mm-side__title">Menu</div>
<!-- Fechar (mobile only): em desktop o ψ continua visível
no canto inferior pra fechar; em mobile o menu cobre
tudo, então precisa de botão dedicado. -->
<button
class="mm-side__close mm-side__close--mobile-only"
title="Fechar menu"
aria-label="Fechar menu"
@click="emit('close')"
>
<i class="pi pi-times" />
</button>
</div>
<div class="mm-side__list">
<button
v-for="c in CATEGORIAS"
:key="c.key"
class="mm-cat"
:class="{ 'is-active': selectedKey === c.key }"
@click="selecionarCategoria(c.key)"
>
<span
class="mm-cat__icon"
:style="{
backgroundColor: c.color ? `${c.color}1f` : 'rgba(255,255,255,0.08)',
borderColor: c.color ? `${c.color}55` : 'rgba(255,255,255,0.15)'
}"
>
<i :class="c.icon" :style="{ color: c.color || 'rgba(255,255,255,0.85)' }" />
</span>
<span class="mm-cat__label">{{ c.label }}</span>
<i class="mm-cat__chevron pi pi-chevron-right" />
</button>
</div>
<!-- Rodapé: ações + usuário -->
<div class="mm-side__foot">
<!-- Itens grudados acima do usuário -->
<div class="mm-foot-actions">
<button
class="mm-foot-item"
:class="{ 'is-active': props.secaoAtiva === 'perfil' }"
@click="goPerfil"
>
<i class="pi pi-user" /><span>Meu Perfil</span>
</button>
<button
class="mm-foot-item"
:class="{ 'is-active': props.secaoAtiva === 'plano' }"
@click="goPlano"
>
<i class="pi pi-credit-card" /><span>Meu Plano</span>
</button>
<button
class="mm-foot-item"
:class="{ 'is-active': props.secaoAtiva === 'negocio' }"
@click="goNegocio"
>
<i class="pi pi-briefcase" /><span>Meu Negócio</span>
</button>
<button
class="mm-foot-item"
:class="{ 'is-active': props.secaoAtiva === 'seguranca' }"
@click="goSeguranca"
>
<i class="pi pi-shield" /><span>Segurança</span>
</button>
<button
class="mm-foot-item"
:class="{ 'is-active': isDarkTheme }"
:aria-pressed="isDarkTheme"
@click="toggleDarkAndPersist"
>
<i :class="isDarkTheme ? 'pi pi-sun' : 'pi pi-moon'" />
<span>Modo escuro</span>
<span
class="mm-toggle"
:class="{ 'is-on': isDarkTheme }"
aria-hidden="true"
/>
</button>
<button
class="mm-foot-item"
:class="{ 'is-active': themeViewActive }"
@click="toggleThemeView"
>
<i class="pi pi-palette" /><span>Cores do Tema</span>
</button>
</div>
<!-- User row com botão Sair à direita -->
<div class="mm-user">
<div class="mm-user__avatar">
<img v-if="userAvatarUrl" :src="userAvatarUrl" :alt="userLabel" />
<template v-else>{{ userInitials }}</template>
</div>
<div class="mm-user__info">
<div class="mm-user__name">{{ userLabel }}</div>
<div class="mm-user__role">{{ userSublabel }}</div>
</div>
<button class="mm-user__signout" title="Sair" @click="sair">
<i class="pi pi-sign-out" />
</button>
</div>
</div>
</nav>
<!-- DIREITA: sub-itens OU cores do tema -->
<aside class="mm-aside">
<div class="mm-aside__head">
<!-- Voltar (mobile only): aparece em <lg quando o
drill-down está em modo "sub-itens". Em desktop as
duas colunas convivem, voltar não faz sentido. -->
<button
class="mm-aside__back mm-aside__back--mobile-only"
title="Voltar"
aria-label="Voltar pra categorias"
@click="voltarParaCategorias"
>
<i class="pi pi-arrow-left" />
</button>
<div class="mm-aside__title">
{{ themeViewActive ? 'Cores do Tema' : categoriaAtiva.label }}
</div>
</div>
<div class="mm-aside__body">
<!-- View especial: Cores do Tema -->
<div v-if="themeViewActive" class="mm-theme">
<div class="mm-theme__group">
<div class="mm-theme__title">Cor primária</div>
<div class="mm-theme__swatches">
<button
v-for="c in primaryColors"
:key="c.name"
type="button"
class="mm-theme__swatch"
:class="{ 'is-active': layoutConfig.primary === c.name }"
:title="c.name"
:style="{ backgroundColor: c.name === 'noir' ? 'var(--text-color)' : c.palette['500'] }"
@click="setPrimary(c)"
/>
</div>
</div>
<div class="mm-theme__group">
<div class="mm-theme__title">Surface (cinzas)</div>
<div class="mm-theme__swatches">
<button
v-for="s in surfaces"
:key="s.name"
type="button"
class="mm-theme__swatch"
:class="{ 'is-active': surfaceIsActive(s) }"
:title="s.name"
:style="{ backgroundColor: s.palette['500'] }"
@click="setSurface(s)"
/>
</div>
</div>
<div class="mm-theme__group">
<div class="mm-theme__title">Preset</div>
<div class="mm-theme__presets">
<button
v-for="p in presetOptions"
:key="p"
type="button"
class="mm-theme__preset"
:class="{ 'is-active': layoutConfig.preset === p }"
@click="setPreset(p)"
>{{ p }}</button>
</div>
</div>
<div class="mm-theme__hint">
Mudanças se aplicam ao app inteiro e ficam salvas no seu perfil.
</div>
<!-- Cards informativos (sem interação ainda) -->
<div class="mm-info-card">
<div class="mm-info-card__head">
<i class="pi pi-window-maximize" />
<span>Layout Variant</span>
<span class="mm-info-card__badge">Em breve</span>
</div>
<div class="mm-info-card__body">
Estrutura principal do menu lateral.
<ul>
<li><strong>Rail</strong> sidebar fina (60px) que expande pra 260px no hover. <em>Padrão atual.</em></li>
<li><strong>Classic</strong> sidebar tradicional sempre visível.</li>
</ul>
<div class="mm-info-card__note">
No Melissa não se aplica (não tem sidebar). Configurável apenas no layout clássico.
</div>
</div>
</div>
<div class="mm-info-card">
<div class="mm-info-card__head">
<i class="pi pi-th-large" />
<span>Menu Mode</span>
<span class="mm-info-card__badge">Em breve</span>
</div>
<div class="mm-info-card__body">
Comportamento do menu (somente quando o variant é <strong>Classic</strong>).
<ul>
<li><strong>Static</strong> menu fica fixo ao lado do conteúdo, sempre visível.</li>
<li><strong>Overlay</strong> menu abre/fecha por cima do conteúdo (ganha mais espaço de trabalho).</li>
</ul>
<div class="mm-info-card__note">
Idem faz sentido no layout clássico.
</div>
</div>
</div>
</div>
<!-- Grupos de sub-itens -->
<template v-else-if="categoriaAtiva.groups.length">
<div
v-for="(g, gi) in categoriaAtiva.groups"
:key="g.title"
class="mm-group"
:class="{ 'mm-group--divided': gi > 0 }"
>
<div class="mm-group__title">{{ g.title }}</div>
<template v-for="item in g.items" :key="item.key">
<!-- Sub-item especial: link de cadastro inline -->
<div v-if="item.tipo === 'link-cadastro'" class="mm-link-row">
<div class="mm-link-row__head">
<i :class="item.icon" class="mm-link-row__icon" />
<span class="mm-link-row__label">{{ item.label }}</span>
</div>
<div class="mm-link-row__field">
<input
type="text"
:value="linkCadastro"
readonly
class="mm-link-row__input"
@click="$event.target.select()"
/>
<button
class="mm-link-row__copy"
:class="{ 'is-copiado': copiado }"
:title="copiado ? 'Copiado!' : 'Copiar link'"
@click="copiarLink"
>
<i :class="copiado ? 'pi pi-check' : 'pi pi-copy'" class="text-xs" />
</button>
</div>
</div>
<!-- Sub-item normal -->
<button
v-else
class="mm-sub"
:class="{ 'is-active': secaoAtiva === item.key }"
@click="clicarSubItem(item)"
>
<i :class="item.icon" class="mm-sub__icon" />
<span class="mm-sub__label">{{ item.label }}</span>
<i class="mm-sub__chevron pi pi-chevron-right" />
</button>
</template>
</div>
</template>
<!-- Categoria sem sub-itens (placeholder) -->
<div v-else class="mm-empty">
<i class="pi pi-th-large" />
<div class="mm-empty__text">
<strong>Em breve</strong>
<span>Os atalhos de {{ categoriaAtiva.label }} ainda serão definidos.</span>
</div>
</div>
</div>
</aside>
</div>
</div>
</template>
<style scoped>
/* ─── Layer (overlay full-screen com blur sutil) ─────────────
Aplica um leve escurecimento + blur-xs (2px) atrás do menu pra dar
sensação de "modal" e desfocar o conteúdo embaixo. Em mobile (<lg)
o media query mais embaixo aumenta a intensidade pra cobrir todo
o viewport com força. */
.mm-layer {
position: absolute;
inset: 0;
z-index: 50;
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
/* ─── Painel float ───────────────────────────────────────── */
.mm-panel {
position: absolute;
bottom: 6rem;
left: 1.75rem;
width: 640px;
height: 80vh;
max-height: calc(100vh - 8rem);
display: flex;
background: var(--m-bg-medium);
backdrop-filter: blur(32px) saturate(160%);
-webkit-backdrop-filter: blur(32px) saturate(160%);
border: 1px solid var(--m-border-strong);
border-radius: 18px;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
overflow: hidden;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
/* ═══ ESQUERDA: categorias ═══════════════════════════════════ */
.mm-side {
width: 280px;
flex-shrink: 0;
display: flex;
flex-direction: column;
border-right: 1px solid var(--m-border);
background: var(--m-bg-soft);
}
.mm-side__head {
padding: 18px 18px 8px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.mm-side__title {
text-transform: uppercase;
letter-spacing: 0.18em;
color: var(--m-text-faint);
font-size: 0.62rem;
font-weight: 600;
}
/* Botão fechar — só visível em mobile (≤lg). Vira display:flex no @media. */
.mm-side__close {
display: none;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 9px;
cursor: pointer;
font-family: inherit;
transition: background-color 140ms ease;
}
.mm-side__close:hover { background: var(--m-bg-soft-hover); }
.mm-side__list {
flex: 1;
overflow-y: auto;
padding: 4px 10px;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mm-side__list::-webkit-scrollbar { width: 5px; }
.mm-side__list::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mm-cat {
width: 100%;
display: flex;
align-items: center;
gap: 12px;
padding: 8px 10px;
background: transparent;
border: none;
border-radius: 10px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: 0.88rem;
transition: background-color 120ms ease, color 120ms ease;
}
.mm-cat:hover { background: var(--m-bg-soft); color: var(--m-text); }
.mm-cat.is-active {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mm-cat__icon {
width: 32px;
height: 32px;
display: grid;
place-items: center;
border-radius: 9px;
border: 1px solid var(--m-border-strong);
background: var(--m-bg-soft-hover);
flex-shrink: 0;
font-size: 0.85rem;
}
.mm-cat__label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-cat__chevron {
color: var(--m-text-faint);
font-size: 0.6rem;
opacity: 0;
transform: translateX(-4px);
transition: opacity 140ms ease, transform 140ms ease;
}
.mm-cat:hover .mm-cat__chevron,
.mm-cat.is-active .mm-cat__chevron {
opacity: 1;
transform: translateX(0);
}
/* Rodapé do menu (ações + usuário) */
.mm-side__foot {
padding: 8px;
border-top: 1px solid var(--m-border);
}
/* Lista de ações acima do user row */
.mm-foot-actions {
display: flex;
flex-direction: column;
gap: 1px;
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px solid var(--m-border);
}
.mm-foot-item {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: 0.78rem;
transition: background-color 120ms ease, color 120ms ease;
}
.mm-foot-item:hover {
background: color-mix(in srgb, var(--p-primary-color) 12%, transparent);
color: var(--p-primary-color);
}
.mm-foot-item:hover > i {
color: var(--p-primary-color);
}
.mm-foot-item.is-active {
background: color-mix(in srgb, var(--p-primary-color) 16%, transparent);
color: var(--p-primary-color);
}
.mm-foot-item.is-active > i {
color: var(--p-primary-color);
}
.mm-foot-item > i {
width: 14px;
text-align: center;
color: var(--m-text-muted);
font-size: 0.78rem;
transition: color 120ms ease;
}
.mm-foot-item > span {
flex: 1;
}
.mm-foot-item__hint {
flex: none !important;
font-size: 0.65rem;
color: var(--m-text-muted);
background: var(--m-bg-soft);
padding: 1px 6px;
border-radius: 999px;
}
/* Switch on/off (modo escuro) — usa as cores do tema */
.mm-toggle {
flex: none !important;
width: 32px;
height: 18px;
background: var(--m-border-strong, var(--m-border));
border-radius: 999px;
position: relative;
transition: background-color 200ms ease;
}
.mm-toggle::after {
content: '';
position: absolute;
top: 2px;
left: 2px;
width: 14px;
height: 14px;
border-radius: 50%;
background: white;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
transition: transform 200ms ease;
}
.mm-toggle.is-on {
background: var(--p-primary-color);
}
.mm-toggle.is-on::after {
transform: translateX(14px);
}
/* User row */
.mm-user {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border-radius: 10px;
transition: background-color 120ms ease;
}
.mm-user:hover { background: var(--m-bg-soft); }
.mm-user__avatar {
width: 32px; height: 32px;
border-radius: 50%;
background: var(--m-accent-strong);
border: 1px solid var(--m-accent);
color: var(--p-primary-contrast-color, white);
font-weight: 600;
font-size: 0.78rem;
display: grid;
place-items: center;
flex-shrink: 0;
overflow: hidden;
}
.mm-user__avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.mm-user__info { flex: 1; min-width: 0; }
.mm-user__name {
font-size: 0.82rem;
font-weight: 500;
color: var(--m-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-user__role {
font-size: 0.68rem;
color: var(--m-text-muted);
}
.mm-user__signout {
width: 32px; height: 32px;
display: grid; place-items: center;
background: transparent;
border: 1px solid rgba(239, 68, 68, 0.25);
color: rgba(239, 68, 68, 0.85);
border-radius: 8px;
cursor: pointer;
flex-shrink: 0;
transition: all 140ms ease;
font-family: inherit;
}
.mm-user__signout:hover {
background: rgba(239, 68, 68, 0.15);
border-color: rgba(239, 68, 68, 0.5);
color: rgb(239, 68, 68);
}
/* ═══ DIREITA: sub-itens da categoria ═══════════════════════ */
.mm-aside {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
padding: 18px;
}
.mm-aside__head {
margin-bottom: 14px;
display: flex;
align-items: center;
gap: 12px;
}
.mm-aside__title {
color: var(--m-text);
font-size: 1.15rem;
font-weight: 500;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Botão voltar — só visível em mobile (≤lg) com drill-down ativo. */
.mm-aside__back {
display: none;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
border-radius: 9px;
cursor: pointer;
flex-shrink: 0;
font-family: inherit;
transition: background-color 140ms ease, transform 140ms ease;
}
.mm-aside__back:hover { background: var(--m-bg-soft-hover); transform: translateX(-1px); }
.mm-aside__body {
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--m-border-strong) transparent;
}
.mm-aside__body::-webkit-scrollbar { width: 5px; }
.mm-aside__body::-webkit-scrollbar-thumb {
background: var(--m-border-strong);
border-radius: 3px;
}
.mm-group + .mm-group {
margin-top: 12px;
}
.mm-group--divided {
padding-top: 12px;
border-top: 1px solid var(--m-border);
}
.mm-group__title {
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--m-text-faint);
font-size: 0.62rem;
font-weight: 600;
padding: 0 10px 6px;
}
/* Sub-item normal */
.mm-sub {
width: 100%;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--m-text);
text-align: left;
cursor: pointer;
font-family: inherit;
font-size: 0.85rem;
transition: background-color 120ms ease, color 120ms ease;
}
.mm-sub:hover { background: var(--m-bg-soft); color: var(--m-text); }
.mm-sub.is-active {
background: var(--m-accent-soft);
border-left: 2px solid var(--m-accent);
padding-left: 8px;
color: var(--m-text);
}
.mm-sub__icon {
color: var(--m-text-muted);
width: 16px;
text-align: center;
flex-shrink: 0;
font-size: 0.85rem;
}
.mm-sub__label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mm-sub__chevron {
color: var(--m-text-faint);
font-size: 0.6rem;
opacity: 0;
transform: translateX(-3px);
transition: opacity 140ms ease, transform 140ms ease;
}
.mm-sub:hover .mm-sub__chevron { opacity: 1; transform: translateX(0); }
/* Sub-item especial: link de cadastro (inline) */
.mm-link-row {
padding: 8px 10px 12px;
}
.mm-link-row__head {
display: flex;
align-items: center;
gap: 10px;
color: var(--m-text);
font-size: 0.85rem;
margin-bottom: 8px;
}
.mm-link-row__icon {
color: var(--m-text-muted);
width: 16px;
text-align: center;
flex-shrink: 0;
font-size: 0.85rem;
}
.mm-link-row__label { font-weight: 500; }
.mm-link-row__field {
display: flex;
align-items: stretch;
gap: 6px;
margin-left: 26px; /* alinha com o label, depois do espaço do ícone */
}
.mm-link-row__input {
flex: 1;
min-width: 0;
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
padding: 6px 10px;
border-radius: 7px;
font-size: 0.72rem;
font-family: 'JetBrains Mono', ui-monospace, monospace;
outline: none;
transition: background-color 140ms ease, border-color 140ms ease;
}
.mm-link-row__input:focus {
background: var(--m-bg-soft-hover);
border-color: var(--m-border-strong);
}
.mm-link-row__copy {
width: 30px;
height: auto;
flex-shrink: 0;
display: grid;
place-items: center;
background: var(--m-bg-soft-hover);
border: 1px solid var(--m-border-strong);
color: var(--m-text);
border-radius: 7px;
cursor: pointer;
font-family: inherit;
transition: all 140ms ease;
}
.mm-link-row__copy:hover {
background: var(--m-bg-soft-hover);
}
.mm-link-row__copy.is-copiado {
background: var(--m-accent-strong);
border-color: var(--m-accent);
color: var(--p-primary-contrast-color, white);
}
/* ─── View "Cores do Tema" embutida ─────────────────────────── */
.mm-theme {
display: flex;
flex-direction: column;
gap: 18px;
padding: 4px 4px 12px;
}
.mm-theme__group {
display: flex;
flex-direction: column;
gap: 8px;
}
.mm-theme__title {
text-transform: uppercase;
letter-spacing: 0.16em;
color: var(--m-text-muted);
font-size: 0.62rem;
font-weight: 600;
}
.mm-theme__swatches {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.mm-theme__swatch {
width: 22px;
height: 22px;
border-radius: 50%;
border: 1px solid var(--m-border-strong);
cursor: pointer;
padding: 0;
transition: transform 140ms ease, box-shadow 140ms ease, border-color 140ms ease;
}
.mm-theme__swatch:hover {
transform: scale(1.12);
}
.mm-theme__swatch.is-active {
border-color: var(--m-text);
box-shadow: 0 0 0 2px var(--m-bg-soft-hover);
transform: scale(1.08);
}
.mm-theme__presets {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mm-theme__preset {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
color: var(--m-text);
padding: 5px 11px;
border-radius: 8px;
font-size: 0.72rem;
font-family: inherit;
text-transform: capitalize;
cursor: pointer;
transition: all 140ms ease;
}
.mm-theme__preset:hover {
background: var(--m-bg-soft-hover);
color: var(--m-text);
}
.mm-theme__preset.is-active {
background: var(--m-border-strong);
border-color: var(--m-border-strong);
color: var(--m-text);
}
.mm-theme__hint {
font-size: 0.7rem;
color: var(--m-text-faint);
line-height: 1.4;
padding-top: 6px;
border-top: 1px solid var(--m-border);
}
/* ─── Info cards (descritivos, sem interação ainda) ─────── */
.mm-info-card {
background: var(--m-bg-soft);
border: 1px solid var(--m-border);
border-radius: 12px;
padding: 12px 14px;
margin-top: 10px;
}
.mm-info-card__head {
display: flex;
align-items: center;
gap: 8px;
color: var(--m-text);
font-size: 0.78rem;
font-weight: 600;
margin-bottom: 8px;
}
.mm-info-card__head > i {
color: var(--m-text-muted);
font-size: 0.78rem;
}
.mm-info-card__badge {
margin-left: auto;
font-size: 0.6rem;
font-weight: 500;
padding: 1px 7px;
border-radius: 999px;
background: var(--m-bg-soft-hover);
border: 1px solid var(--m-border);
color: var(--m-text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.mm-info-card__body {
color: var(--m-text-muted);
font-size: 0.72rem;
line-height: 1.5;
}
.mm-info-card__body ul {
margin: 6px 0;
padding-left: 14px;
display: flex;
flex-direction: column;
gap: 3px;
}
.mm-info-card__body strong {
color: var(--m-text);
font-weight: 600;
}
.mm-info-card__body em {
color: var(--m-text-muted);
font-style: normal;
font-size: 0.66rem;
}
.mm-info-card__note {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid var(--m-border);
color: var(--m-text-faint);
font-size: 0.66rem;
font-style: italic;
}
/* Categoria vazia — placeholder */
.mm-empty {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: var(--m-text-muted);
gap: 16px;
padding: 24px;
}
.mm-empty > i {
font-size: 2.5rem;
color: var(--m-text-faint);
}
.mm-empty__text {
display: flex;
flex-direction: column;
gap: 4px;
max-width: 280px;
}
.mm-empty__text strong {
color: var(--m-text-muted);
font-weight: 500;
font-size: 0.85rem;
}
.mm-empty__text span {
font-size: 0.75rem;
line-height: 1.4;
}
/* ═══════════════════════════════════════════════════════════════
Responsivo <lg (≤1023px) — drawer da esquerda (paridade com Agenda)
───────────────────────────────────────────────────────────────
- .mm-layer vira backdrop fullscreen (escurece + blur), click fora fecha
- .mm-panel vira drawer 360px (mesmo tamanho do .ma-mobile-drawer),
desliza da esquerda
- .mm-side e .mm-aside viram camadas absolutas, alternam via
translateX controlado pelo modificador .is-mobile-sub
- Botão "fechar" no header da side, "voltar" no header do aside
- z-index do .mm-layer sobe pra 90 pra cobrir o ψ (70) e o dock (65)
═══════════════════════════════════════════════════════════════ */
@media (max-width: 1023px) {
/* Layer = backdrop. Click fora (no próprio layer) fecha via @click.self
que já existe no template. position:fixed garante cobertura mesmo
se algum ancestor estiver scrollado. */
.mm-layer {
position: fixed;
inset: 0;
z-index: 90;
background: rgba(0, 0, 0, 0.45);
backdrop-filter: blur(4px);
-webkit-backdrop-filter: blur(4px);
}
.mm-panel {
position: fixed;
top: 0;
left: 0;
bottom: auto;
width: min(360px, 88vw); /* paridade com .ma-mobile-drawer */
height: 100dvh;
max-height: 100dvh;
border-radius: 0;
border-top: none;
border-left: none;
border-bottom: none;
border-right: 1px solid var(--m-border);
overflow: hidden;
}
/* As duas colunas viram camadas full do painel, animadas via translateX. */
.mm-side,
.mm-aside {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
transition: transform 280ms cubic-bezier(0.4, 0, 0.2, 1);
will-change: transform;
}
.mm-side {
transform: translateX(0);
z-index: 1;
border-right: none;
}
.mm-aside {
transform: translateX(100%);
z-index: 2;
background: var(--m-bg-medium);
}
/* Modo "sub-itens" (drill-down ativo) */
.mm-panel.is-mobile-sub .mm-side {
transform: translateX(-12%); /* leve parallax pra dar profundidade */
}
.mm-panel.is-mobile-sub .mm-aside {
transform: translateX(0);
}
/* Botões mobile-only ganham display */
.mm-side__close--mobile-only { display: inline-flex; }
.mm-aside__back--mobile-only { display: inline-flex; }
/* Header da side fica um pouco mais aberto pra acomodar o close */
.mm-side__head {
padding-top: 14px;
padding-bottom: 14px;
}
.mm-side__title {
font-size: 0.7rem; /* lê melhor em mobile */
}
/* Aside head: o título fica MAIS espaçado no topo, e o aside ganha
padding lateral menor (telas pequenas precisam de cada pixel). */
.mm-aside {
padding: 14px 14px 18px;
}
.mm-aside__head {
margin-bottom: 12px;
}
/* Sub-itens com mais respiro vertical (toque tem que pegar) */
.mm-sub {
padding: 12px 12px;
font-size: 0.92rem;
}
.mm-cat {
padding: 12px 12px;
font-size: 0.95rem;
}
.mm-cat__icon { width: 36px; height: 36px; }
/* Footer continua na tela 1 (lista de categorias) */
}
</style>