c2c42a1620
Bot que coleta nome, motivo de busca e preferências ANTES do paciente
entrar no fluxo humano. Terapeuta abre a conversa e já encontra
resumo em conversation_notes.
Banco (migration 20260423000007):
- conversation_bots: config 1 por tenant. enabled, greeting/closing
messages, steps (JSONB array de {prompt, variable, type}), trigger_mode
(new_contact | all_unassigned | keyword), trigger_keywords[],
idle_timeout_minutes, respect_optout.
Defaults vêm com 4 perguntas úteis: nome, motivo, modalidade,
horário preferido.
- conversation_bot_sessions: estado por thread. current_step,
collected_data JSONB, status (active | completed | abandoned_idle |
abandoned_manual | opted_out). UNIQUE parcial garante 1 ativa por
(tenant, thread).
- RLS: leitura tenant/saas_admin, escrita admins (config) + service_role
(sessions, só edge altera).
Shared (_shared/whatsapp-hooks.ts):
- maybeProcessBot: carrega config, busca sessão ativa, avança step
com resposta, envia próxima pergunta via SendFn. Ao esgotar steps,
envia closing + cria conversation_notes com resumo das variáveis
coletadas. Se humano assume (conversation_assignments preenchido),
sessão marca 'abandoned_manual' e bot sai.
- Trigger modes:
- 'new_contact' (default): só inicia pra thread sem histórico bot
E sem paciente vinculado (lead real).
- 'all_unassigned': qualquer thread sem assignee.
- 'keyword': matched contra lista; normalizeForMatch já existe.
Integração nos inbound (ambos providers):
- evolution-whatsapp-inbound: chama maybeProcessBot após opt-in/opt-out,
ANTES do auto-reply. Se bot processou, skip auto-reply (senão duas
respostas sobrepostas).
- twilio-whatsapp-inbound: idem, usando makeTwilioCreditedSendFn pra
cobrar crédito de cada mensagem enviada pelo bot.
UI (/configuracoes/conversas-bots):
- Toggle enabled + Select trigger_mode + (se keyword) chips de keywords.
- Textareas greeting/closing.
- Editor de steps: reordenar (up/down), remover, add, editor com prompt
e variable (regex /^[a-z_][a-z0-9_]*$/).
- Botão "Padrão" restaura mensagens/steps default.
- InputNumber idle_timeout + toggle respect_optout.
- Card inferior: últimas 30 sessões (7 dias) com status, contato,
nome coletado (primeiro campo), progresso (step X/N), início.
- Entrada na landing de configurações + rota /configuracoes/conversas-bots.
Caveat conhecido: a resolução de conversation_notes.created_by usa
o primeiro admin ativo do tenant (pickAnyAdmin). Pra uma v2 seria
ideal ter um user "bot" sintético dedicado.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
556 lines
23 KiB
Vue
556 lines
23 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/layout/ConfiguracoesPage.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<script setup>
|
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useLayout } from '@/layout/composables/layout';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
|
|
const showMenu = ref(false);
|
|
const asideOpen = ref(false);
|
|
|
|
// ── Layout-aware left position (igual ao TherapistDashboard) ──────────────
|
|
const { effectiveVariant, layoutState, layoutConfig, isMobile, railPanelPushesLayout } = useLayout();
|
|
const isMobileLayout = computed(() => isMobile.value);
|
|
const asideLeft = computed(() => {
|
|
if (isMobileLayout.value) return undefined;
|
|
if (effectiveVariant.value !== 'rail') {
|
|
const isStaticActive = layoutConfig.menuMode === 'static' && !layoutState.staticMenuInactive;
|
|
return isStaticActive ? '20rem' : '0';
|
|
}
|
|
return railPanelPushesLayout.value ? 'calc(60px + 260px)' : '60px';
|
|
});
|
|
|
|
// ── Hero sticky ────────────────────────────────────────────
|
|
const headerEl = ref(null);
|
|
const headerSentinelRef = ref(null);
|
|
const headerStuck = ref(false);
|
|
let _observer = null;
|
|
|
|
const grupos = [
|
|
{
|
|
key: 'agenda',
|
|
label: 'Agenda',
|
|
desc: 'Horários, bloqueios e agendador público para pacientes.',
|
|
icon: 'pi pi-calendar',
|
|
items: [
|
|
{
|
|
key: 'agenda',
|
|
label: 'Agenda',
|
|
desc: 'Horários semanais, exceções, duração e intervalo padrão.',
|
|
icon: 'pi pi-calendar',
|
|
to: '/configuracoes/agenda'
|
|
},
|
|
{
|
|
key: 'bloqueios',
|
|
label: 'Bloqueios',
|
|
desc: 'Feriados nacionais, municipais e períodos bloqueados.',
|
|
icon: 'pi pi-ban',
|
|
to: '/configuracoes/bloqueios'
|
|
},
|
|
{
|
|
key: 'agendador',
|
|
label: 'Agendador Online',
|
|
desc: 'Link público para pacientes solicitarem horários.',
|
|
icon: 'pi pi-calendar-clock',
|
|
to: '/configuracoes/agendador'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'financeiro',
|
|
label: 'Financeiro',
|
|
desc: 'Formas de pagamento, valores, descontos e convênios.',
|
|
icon: 'pi pi-wallet',
|
|
items: [
|
|
{
|
|
key: 'pagamento',
|
|
label: 'Pagamento',
|
|
desc: 'Formas de pagamento: Pix, depósito, dinheiro, cartão, convênio.',
|
|
icon: 'pi pi-wallet',
|
|
to: '/configuracoes/pagamento'
|
|
},
|
|
{
|
|
key: 'precificacao',
|
|
label: 'Precificação',
|
|
desc: 'Valor padrão da sessão e preços por tipo de compromisso.',
|
|
icon: 'pi pi-tag',
|
|
to: '/configuracoes/precificacao'
|
|
},
|
|
{
|
|
key: 'descontos',
|
|
label: 'Descontos por Paciente',
|
|
desc: 'Descontos recorrentes aplicados automaticamente.',
|
|
icon: 'pi pi-percentage',
|
|
to: '/configuracoes/descontos'
|
|
},
|
|
{
|
|
key: 'excecoes-financeiras',
|
|
label: 'Exceções Financeiras',
|
|
desc: 'O que cobrar em faltas, cancelamentos e situações excepcionais.',
|
|
icon: 'pi pi-exclamation-triangle',
|
|
to: '/configuracoes/excecoes-financeiras'
|
|
},
|
|
{
|
|
key: 'convenios',
|
|
label: 'Convênios',
|
|
desc: 'Cadastre os convênios que você atende e seus valores.',
|
|
icon: 'pi pi-id-card',
|
|
to: '/configuracoes/convenios'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'whatsapp',
|
|
label: 'WhatsApp & Conversas',
|
|
desc: 'Canal, tags, auto-reply, lembretes e créditos de mensagens.',
|
|
icon: 'pi pi-whatsapp',
|
|
items: [
|
|
{
|
|
key: 'whatsapp',
|
|
label: 'Canal WhatsApp',
|
|
desc: 'Escolha o canal (oficial AgenciaPSI ou pessoal) e configure a integração.',
|
|
icon: 'pi pi-whatsapp',
|
|
to: '/configuracoes/whatsapp',
|
|
aliases: ['/configuracoes/whatsapp-pessoal', '/configuracoes/whatsapp-oficial']
|
|
},
|
|
{
|
|
key: 'whatsapp-templates',
|
|
label: 'Templates WhatsApp',
|
|
desc: 'Personalize os textos enviados por WhatsApp ou volte ao padrão da plataforma.',
|
|
icon: 'pi pi-file-edit',
|
|
to: '/configuracoes/whatsapp-templates'
|
|
},
|
|
{
|
|
key: 'conversas-tags',
|
|
label: 'Tags de Conversa',
|
|
desc: 'Etiquetas custom pra classificar threads no CRM (urgente, remarcação, etc).',
|
|
icon: 'pi pi-tag',
|
|
to: '/configuracoes/conversas-tags'
|
|
},
|
|
{
|
|
key: 'conversas-autoreply',
|
|
label: 'Auto-reply WhatsApp',
|
|
desc: 'Resposta automática quando paciente escreve fora do horário de atendimento.',
|
|
icon: 'pi pi-reply',
|
|
to: '/configuracoes/conversas-autoreply'
|
|
},
|
|
{
|
|
key: 'conversas-optouts',
|
|
label: 'Opt-outs (LGPD)',
|
|
desc: 'Números que pediram pra não receber mensagens automáticas. Direito de oposição LGPD.',
|
|
icon: 'pi pi-ban',
|
|
to: '/configuracoes/conversas-optouts'
|
|
},
|
|
{
|
|
key: 'conversas-sla',
|
|
label: 'SLA de resposta',
|
|
desc: 'Tempo máximo pra responder mensagens de pacientes. Alerta quando estourar.',
|
|
icon: 'pi pi-stopwatch',
|
|
to: '/configuracoes/conversas-sla'
|
|
},
|
|
{
|
|
key: 'conversas-bots',
|
|
label: 'Bot de triagem',
|
|
desc: 'Coleta nome e motivo do paciente via WhatsApp antes de entrar no fluxo humano.',
|
|
icon: 'pi pi-android',
|
|
to: '/configuracoes/conversas-bots'
|
|
},
|
|
{
|
|
key: 'lembretes-sessao',
|
|
label: 'Lembretes de Sessão',
|
|
desc: 'WhatsApp automático 24h e 2h antes das sessões agendadas.',
|
|
icon: 'pi pi-bell',
|
|
to: '/configuracoes/lembretes-sessao'
|
|
},
|
|
{
|
|
key: 'creditos-whatsapp',
|
|
label: 'Créditos WhatsApp',
|
|
desc: 'Compre pacotes de mensagens, veja saldo e extrato.',
|
|
icon: 'pi pi-credit-card',
|
|
to: '/configuracoes/creditos-whatsapp'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'comunicacao',
|
|
label: 'Comunicação',
|
|
desc: 'SMS e templates de e-mail enviados aos pacientes.',
|
|
icon: 'pi pi-send',
|
|
items: [
|
|
{
|
|
key: 'sms',
|
|
label: 'SMS',
|
|
desc: 'Gerencie créditos SMS e personalize as mensagens enviadas.',
|
|
icon: 'pi pi-comment',
|
|
to: '/configuracoes/sms'
|
|
},
|
|
{
|
|
key: 'email-templates',
|
|
label: 'Templates de E-mail',
|
|
desc: 'Personalize os e-mails enviados aos pacientes.',
|
|
icon: 'pi pi-envelope',
|
|
to: '/configuracoes/email-templates'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
key: 'plataforma',
|
|
label: 'Empresa & Plataforma',
|
|
desc: 'Dados da empresa, recursos extras e registro de auditoria.',
|
|
icon: 'pi pi-building',
|
|
items: [
|
|
{
|
|
key: 'empresa',
|
|
label: 'Minha Empresa',
|
|
desc: 'CNPJ, endereço, logomarca e redes sociais.',
|
|
icon: 'pi pi-building',
|
|
to: '/configuracoes/empresa'
|
|
},
|
|
{
|
|
key: 'recursos-extras',
|
|
label: 'Recursos Extras',
|
|
desc: 'Amplíe as funcionalidades com recursos adicionais e créditos.',
|
|
icon: 'pi pi-box',
|
|
to: '/configuracoes/recursos-extras'
|
|
},
|
|
{
|
|
key: 'auditoria',
|
|
label: 'Auditoria',
|
|
desc: 'Registro imutável de operações (LGPD Art. 37).',
|
|
icon: 'pi pi-shield',
|
|
to: '/configuracoes/auditoria'
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
// Flatten pra cálculos de item ativo (mesma lógica de antes)
|
|
const secoesFlat = computed(() => grupos.flatMap((g) => g.items));
|
|
|
|
const activeTo = computed(() => {
|
|
const p = route.path || '';
|
|
const hit = [...secoesFlat.value]
|
|
.sort((a, b) => b.to.length - a.to.length)
|
|
.find((s) => p === s.to || p.startsWith(s.to + '/') || (s.aliases || []).includes(p));
|
|
return hit?.to || '/configuracoes/agenda';
|
|
});
|
|
|
|
const activeSecao = computed(() => secoesFlat.value.find((s) => s.to === activeTo.value));
|
|
|
|
// Grupo que contém a seção ativa (pra auto-expandir no accordion)
|
|
const activeGrupoKey = computed(() => grupos.find((g) => g.items.some((i) => i.to === activeTo.value))?.key || grupos[0].key);
|
|
|
|
// Accordion multi-open: começa com o grupo ativo aberto
|
|
const openGroups = ref([activeGrupoKey.value]);
|
|
|
|
// Quando a rota mudar e o grupo ativo não estiver aberto, abre ele (sem fechar os outros)
|
|
watch(activeGrupoKey, (k) => {
|
|
if (k && !openGroups.value.includes(k)) {
|
|
openGroups.value = [...openGroups.value, k];
|
|
}
|
|
});
|
|
|
|
function ir(to) {
|
|
if (!to) return;
|
|
if (route.path !== to) router.push(to);
|
|
asideOpen.value = false;
|
|
}
|
|
|
|
onMounted(() => {
|
|
requestAnimationFrame(() => {
|
|
showMenu.value = true;
|
|
});
|
|
|
|
const rootMargin = `${document.querySelector('.l2-main') ? '0px' : '-56px'} 0px 0px 0px`;
|
|
_observer = new IntersectionObserver(
|
|
([entry]) => {
|
|
headerStuck.value = !entry.isIntersecting;
|
|
},
|
|
{ threshold: 0, rootMargin }
|
|
);
|
|
if (headerSentinelRef.value) _observer.observe(headerSentinelRef.value);
|
|
});
|
|
|
|
onBeforeUnmount(() => {
|
|
_observer?.disconnect();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex min-h-screen bg-[var(--surface-ground)]">
|
|
<!-- Overlay mobile -->
|
|
<div v-if="asideOpen" class="fixed inset-0 bg-black/40 backdrop-blur-sm z-[39] xl:hidden" @click="asideOpen = false" />
|
|
|
|
<!-- Aside drawer -->
|
|
<aside
|
|
class="cfg-aside-drawer flex flex-col overflow-y-auto shrink-0 bg-[var(--surface-card)] border-r border-[var(--surface-border)]"
|
|
:class="asideOpen ? 'translate-x-0 visible' : 'max-xl:-translate-x-full max-xl:invisible'"
|
|
:style="{ left: asideLeft }"
|
|
>
|
|
<!-- Cabeçalho da aside (sticky — igual à sidebar principal) -->
|
|
<div class="cfg-aside-header sticky top-0 z-10 flex items-center gap-2 px-4 h-[50px] border-b border-[var(--surface-border)] shrink-0 bg-[var(--surface-card)]">
|
|
<div class="grid place-items-center w-8 h-8 rounded-md bg-indigo-500/[0.12] text-[var(--p-primary-500,#6366f1)] shrink-0">
|
|
<i class="pi pi-cog text-sm" />
|
|
</div>
|
|
<span class="text-sm font-bold text-[var(--text-color)] tracking-tight">Configurações</span>
|
|
</div>
|
|
|
|
<!-- Label seções -->
|
|
<div class="flex items-center gap-1.5 px-4 pt-3 pb-1.5 text-[0.68rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-60">
|
|
<i class="pi pi-list text-[0.65rem]" />
|
|
<span>Seções</span>
|
|
</div>
|
|
|
|
<!-- Accordion de grupos -->
|
|
<div v-if="showMenu" class="cfg-menu-accordion px-2 pb-3">
|
|
<Accordion v-model:value="openGroups" multiple>
|
|
<AccordionPanel v-for="g in grupos" :key="g.key" :value="g.key">
|
|
<AccordionHeader class="cfg-group-header">
|
|
<div class="flex items-center gap-2.5 w-full min-w-0">
|
|
<div class="cfg-group-icon grid place-items-center w-9 h-9 rounded-md shrink-0 bg-[color-mix(in_srgb,var(--primary-color)_12%,transparent)] text-[var(--primary-color,#6366f1)]">
|
|
<i :class="g.icon" class="text-[0.95rem]" />
|
|
</div>
|
|
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
<span class="text-[0.9rem] font-bold leading-5 text-[var(--text-color)] truncate">{{ g.label }}</span>
|
|
<span class="text-[0.8rem] leading-[1.15rem] text-[var(--text-color-secondary)] opacity-75 whitespace-normal break-words">{{ g.desc }}</span>
|
|
</div>
|
|
<Badge :value="g.items.length" severity="contrast" class="cfg-group-badge shrink-0" />
|
|
</div>
|
|
</AccordionHeader>
|
|
<AccordionContent>
|
|
<div class="cfg-nav-list flex flex-col gap-2 py-1">
|
|
<button
|
|
v-for="s in g.items"
|
|
:key="s.key"
|
|
class="cfg-nav-item flex items-center gap-3 p-3 cursor-pointer w-full text-left rounded-[6px] border bg-[var(--surface-card)] transition-colors duration-[120ms]"
|
|
:class="activeTo === s.to ? 'cfg-nav-item--active' : 'border-[var(--surface-border)]'"
|
|
@click="ir(s.to)"
|
|
>
|
|
<div
|
|
class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0 transition-colors duration-[120ms]"
|
|
:class="activeTo === s.to
|
|
? 'bg-[color-mix(in_srgb,var(--primary-color)_15%,transparent)] text-[var(--primary-color,#6366f1)]'
|
|
: 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'"
|
|
>
|
|
<i :class="s.icon" class="text-lg" />
|
|
</div>
|
|
|
|
<div class="flex-1 min-w-0 flex flex-col gap-0.5">
|
|
<span
|
|
class="font-semibold leading-5 truncate"
|
|
:class="activeTo === s.to ? 'text-[var(--primary-color,#6366f1)]' : 'text-[var(--text-color)]'"
|
|
>{{ s.label }}</span>
|
|
<span class="text-sm leading-[1.15rem] text-[var(--text-color-secondary)] whitespace-normal break-words">{{ s.desc }}</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</AccordionContent>
|
|
</AccordionPanel>
|
|
</Accordion>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Área principal -->
|
|
<div class="flex-1 min-w-0 xl:pl-[320px]">
|
|
<!-- Sentinel -->
|
|
<div ref="headerSentinelRef" class="h-px" />
|
|
|
|
<!-- Hero compacto -->
|
|
<div
|
|
ref="headerEl"
|
|
class="sticky top-[var(--layout-sticky-top,56px)] z-20 overflow-hidden rounded-md border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-2.5 mx-3 md:mx-4 my-3"
|
|
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
|
|
>
|
|
<!-- Blobs decorativos -->
|
|
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
|
|
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-emerald-300/10" />
|
|
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-indigo-500/[0.09]" />
|
|
</div>
|
|
|
|
<div class="relative z-1 flex items-center gap-3">
|
|
<!-- Brand -->
|
|
<div class="flex items-center gap-2 shrink-0">
|
|
<div class="grid place-items-center w-9 h-9 rounded-md shrink-0 bg-indigo-500/[0.12] text-[var(--p-primary-500,#6366f1)]">
|
|
<i class="pi pi-cog text-base" />
|
|
</div>
|
|
<div class="min-w-0">
|
|
<!-- Título: Configurações · Seção -->
|
|
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)] flex items-center gap-1.5 flex-wrap">
|
|
<span>Configurações</span>
|
|
<template v-if="activeSecao">
|
|
<span class="text-[var(--text-color-secondary)] opacity-40 font-normal">·</span>
|
|
<span>{{ activeSecao.label }}</span>
|
|
</template>
|
|
</div>
|
|
<!-- Subtítulo: ícone + descrição da seção -->
|
|
<div class="text-xs text-[var(--text-color-secondary)] flex items-center gap-1 mt-px">
|
|
<template v-if="activeSecao">
|
|
<i :class="activeSecao.icon" class="opacity-50 shrink-0" />
|
|
<span>{{ activeSecao.desc }}</span>
|
|
</template>
|
|
<span v-else class="opacity-60">Configurações gerais do sistema</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ações -->
|
|
<div class="flex items-center gap-2 ml-auto shrink-0">
|
|
<!-- Slot de ações específicas da sub-página (via Teleport) -->
|
|
<div id="cfg-page-actions" class="flex items-center gap-1.5"></div>
|
|
|
|
<!-- Toggle aside — mobile/tablet apenas -->
|
|
<button
|
|
class="xl:hidden inline-flex items-center gap-1.5 h-9 px-3 rounded-full border border-[var(--surface-border)] bg-transparent text-xs font-semibold text-[var(--text-color)] cursor-pointer hover:bg-[var(--surface-hover)] transition-colors duration-150"
|
|
@click="asideOpen = !asideOpen"
|
|
>
|
|
<i class="pi pi-bars text-[0.75rem]" />
|
|
<span class="hidden sm:inline">Menu desta seção</span>
|
|
</button>
|
|
<Button icon="pi pi-arrow-left" severity="secondary" outlined class="h-9 w-9 rounded-full" v-tooltip.bottom="'Voltar'" @click="router.back()" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Conteúdo da seção -->
|
|
<div class="px-3 md:px-4 pb-5">
|
|
<router-view />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Aside drawer — comportamento responsivo */
|
|
.cfg-aside-drawer {
|
|
position: fixed;
|
|
top: calc(56px + var(--notice-banner-height, 0px));
|
|
left: 0;
|
|
height: calc(100dvh - 56px - var(--notice-banner-height, 0px));
|
|
width: min(320px, 90vw);
|
|
z-index: 40;
|
|
overflow-y: auto;
|
|
transition:
|
|
transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
|
|
visibility 0.25s;
|
|
box-shadow: 4px 0 24px rgba(0, 0, 0, 0.15);
|
|
}
|
|
@media (min-width: 1280px) {
|
|
.cfg-aside-drawer {
|
|
position: fixed;
|
|
top: calc(56px + var(--notice-banner-height, 0px));
|
|
height: calc(100vh - 56px - var(--notice-banner-height, 0px));
|
|
width: 320px;
|
|
transform: none;
|
|
visibility: visible;
|
|
box-shadow: none;
|
|
z-index: auto;
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
/* ── Lista de subitens (cards) ──────────────────────────────── */
|
|
.cfg-nav-list {
|
|
margin: 0.15rem 0.25rem 0.35rem 1.5rem;
|
|
}
|
|
|
|
/* ── Hover e estado ativo dos cards ─────────────────────────── */
|
|
.cfg-nav-item {
|
|
transition:
|
|
background-color 150ms ease,
|
|
border-color 150ms ease;
|
|
}
|
|
.cfg-nav-item:hover {
|
|
background: var(--surface-hover) !important;
|
|
border-color: color-mix(in srgb, var(--primary-color) 30%, var(--surface-border)) !important;
|
|
}
|
|
.cfg-nav-item--active {
|
|
background: color-mix(in srgb, var(--primary-color) 6%, var(--surface-card));
|
|
border-color: var(--primary-color, #6366f1) !important;
|
|
}
|
|
|
|
/* ── Accordion do menu — compacto, sem bordas grossas ───────────── */
|
|
.cfg-menu-accordion :deep(.p-accordion) {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionpanel) {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader) {
|
|
padding: 0.5rem 0.5rem;
|
|
border-radius: 8px;
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--text-color);
|
|
transition: background-color 120ms ease;
|
|
min-height: 52px;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader:hover) {
|
|
background: color-mix(in srgb, var(--primary-color) 8%, var(--surface-card)) !important;
|
|
color: var(--text-color) !important;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader:focus-visible) {
|
|
box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--primary-color) 30%, transparent);
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader-toggle-icon) {
|
|
color: var(--text-color-secondary);
|
|
opacity: 0.55;
|
|
font-size: 0.7rem;
|
|
margin-left: 0.25rem;
|
|
transition: color 150ms ease, opacity 150ms ease, transform 150ms ease;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader:hover) .p-accordionheader-toggle-icon {
|
|
color: var(--primary-color, #6366f1);
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Ícone do grupo ganha anel primary no hover (igual aos subitens) */
|
|
.cfg-group-icon {
|
|
transition: box-shadow 150ms ease, background-color 150ms ease;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordionheader:hover) .cfg-group-icon {
|
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary-color) 25%, transparent);
|
|
background: color-mix(in srgb, var(--primary-color) 18%, transparent);
|
|
}
|
|
|
|
/* Badge circular do contador de itens (igual ao p-badge-circle do pasteds.txt) */
|
|
.cfg-group-badge :deep(.p-badge),
|
|
.cfg-group-badge.p-badge {
|
|
min-width: 1.25rem;
|
|
height: 1.25rem;
|
|
padding: 0 0.35rem;
|
|
font-size: 0.68rem;
|
|
font-weight: 700;
|
|
border-radius: 9999px;
|
|
line-height: 1.25rem;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordioncontent) {
|
|
border: none;
|
|
background: transparent;
|
|
}
|
|
.cfg-menu-accordion :deep(.p-accordioncontent-content) {
|
|
padding: 0.15rem 0.2rem 0.35rem;
|
|
background: transparent;
|
|
border: none;
|
|
}
|
|
</style>
|