211 lines
9.7 KiB
Vue
211 lines
9.7 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/layout/AppRail.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<!-- Mini icon rail (Layout 2) -->
|
|
<script setup>
|
|
import { computed, ref } from 'vue';
|
|
|
|
import { useMenuStore } from '@/stores/menuStore';
|
|
import { useLayout } from './composables/layout';
|
|
import { sessionUser } from '@/app/session';
|
|
|
|
import AppMenuFooterPanel from './AppMenuFooterPanel.vue';
|
|
|
|
const menuStore = useMenuStore();
|
|
const { layoutState, layoutConfig, clearRailHoverClose, scheduleRailHoverClose } = useLayout();
|
|
|
|
// ── Hover com delay ──────────────────────────────────────────
|
|
let _hoverOpenTimer = null;
|
|
|
|
function onRailMouseLeave() {
|
|
if (layoutConfig.railOpenMode !== 'hover') return;
|
|
clearTimeout(_hoverOpenTimer);
|
|
scheduleRailHoverClose(200);
|
|
}
|
|
|
|
function onRailMouseEnter() {
|
|
if (layoutConfig.railOpenMode !== 'hover') return;
|
|
clearRailHoverClose();
|
|
}
|
|
|
|
function onSectionHover(section) {
|
|
if (layoutConfig.railOpenMode !== 'hover') return;
|
|
clearRailHoverClose();
|
|
clearTimeout(_hoverOpenTimer);
|
|
_hoverOpenTimer = setTimeout(() => {
|
|
layoutState.railSectionKey = section.key;
|
|
layoutState.railPanelOpen = true;
|
|
}, 120);
|
|
}
|
|
|
|
function onHomeHover() {
|
|
if (layoutConfig.railOpenMode !== 'hover') return;
|
|
clearRailHoverClose();
|
|
clearTimeout(_hoverOpenTimer);
|
|
_hoverOpenTimer = setTimeout(() => {
|
|
layoutState.railSectionKey = '__home__';
|
|
layoutState.railPanelOpen = true;
|
|
}, 120);
|
|
}
|
|
|
|
// ── Seções do rail (derivadas do model) ─────────────────────
|
|
const railSections = computed(() => {
|
|
const model = menuStore.model || [];
|
|
return model
|
|
.filter((s) => s.label && Array.isArray(s.items) && s.items.length)
|
|
.filter(
|
|
(s) =>
|
|
s.label
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/\p{Diacritic}/gu, '')
|
|
.trim() !== 'inicio'
|
|
)
|
|
.map((s) => ({
|
|
key: s.label,
|
|
label: s.label,
|
|
icon: s.icon || s.items.find((i) => i.icon)?.icon || 'pi pi-fw pi-circle',
|
|
items: s.items
|
|
}));
|
|
});
|
|
|
|
// ── Avatar / iniciais ────────────────────────────────────────
|
|
const avatarUrl = computed(() => sessionUser.value?.user_metadata?.avatar_url || null);
|
|
const initials = 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 userName = computed(() => sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || 'Conta');
|
|
|
|
// ── Início (fixo) ────────────────────────────────────────────
|
|
function selectHome() {
|
|
if (layoutState.railSectionKey === '__home__' && layoutState.railPanelOpen) {
|
|
layoutState.railPanelOpen = false;
|
|
} else {
|
|
layoutState.railSectionKey = '__home__';
|
|
layoutState.railPanelOpen = true;
|
|
}
|
|
}
|
|
|
|
const isHomeActive = computed(() => layoutState.railSectionKey === '__home__' && layoutState.railPanelOpen);
|
|
|
|
// ── Seleção de seção ─────────────────────────────────────────
|
|
function selectSection(section) {
|
|
if (layoutState.railSectionKey === section.key && layoutState.railPanelOpen) {
|
|
layoutState.railPanelOpen = false;
|
|
} else {
|
|
layoutState.railSectionKey = section.key;
|
|
layoutState.railPanelOpen = true;
|
|
}
|
|
}
|
|
|
|
function isActiveSectionOrChild(section) {
|
|
if (layoutState.railSectionKey === section.key && layoutState.railPanelOpen) return true;
|
|
const active = String(layoutState.activePath || '');
|
|
return section.items.some((i) => {
|
|
const p = typeof i.to === 'string' ? i.to : '';
|
|
return p && active.startsWith(p);
|
|
});
|
|
}
|
|
|
|
// ── Menu do usuário (rodapé) ─────────────────────────────────
|
|
const footerPanel = ref(null);
|
|
function toggleUserMenu(e) {
|
|
footerPanel.value?.toggle(e);
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<aside class="rail w-[60px] shrink-0 h-screen flex flex-col items-center border-r border-[var(--surface-border)] bg-[var(--surface-card)] z-50 select-none" @mouseenter="onRailMouseEnter" @mouseleave="onRailMouseLeave">
|
|
<!-- ── Brand ──────────────────────────────────────────── -->
|
|
<div class="w-full h-14 shrink-0 grid place-items-center border-b border-[var(--surface-border)]">
|
|
<span class="text-[1.35rem] font-extrabold leading-none text-[var(--primary-color)] [text-shadow:0_0_20px_color-mix(in_srgb,var(--primary-color)_40%,transparent)]">Ψ</span>
|
|
</div>
|
|
|
|
<!-- ── Nav icons ──────────────────────────────────────── -->
|
|
<nav class="flex-1 w-full flex flex-col items-center gap-1 py-2.5 overflow-y-auto overflow-x-hidden [scrollbar-width:none] [&::-webkit-scrollbar]:hidden" role="navigation" aria-label="Menu principal">
|
|
<!-- Início fixo -->
|
|
<button
|
|
class="rail-btn relative w-10 h-10 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-base shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
|
:class="isHomeActive ? 'rail-btn--active bg-[color-mix(in_srgb,var(--primary-color)_12%,transparent)] !text-[var(--primary-color)]' : ''"
|
|
v-tooltip.right="{ value: 'Início', showDelay: 0 }"
|
|
aria-label="Início"
|
|
@click="selectHome"
|
|
@mouseenter="onHomeHover"
|
|
>
|
|
<i class="pi pi-fw pi-home" />
|
|
</button>
|
|
|
|
<button
|
|
v-for="section in railSections"
|
|
:key="section.key"
|
|
class="rail-btn relative w-10 h-10 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-base shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
|
:class="isActiveSectionOrChild(section) ? 'rail-btn--active bg-[color-mix(in_srgb,var(--primary-color)_12%,transparent)] !text-[var(--primary-color)]' : ''"
|
|
v-tooltip.right="{ value: section.label, showDelay: 0 }"
|
|
:aria-label="section.label"
|
|
@click="selectSection(section)"
|
|
@mouseenter="onSectionHover(section)"
|
|
>
|
|
<i :class="section.icon" />
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- ── Rodapé ─────────────────────────────────────────── -->
|
|
<div class="w-full flex flex-col items-center gap-1.5 py-2 pb-3 border-t border-[var(--surface-border)]">
|
|
<button
|
|
class="w-9 h-9 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-[0.875rem] shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
|
v-tooltip.right="{ value: 'Configurações', showDelay: 0 }"
|
|
aria-label="Configurações"
|
|
@click="$router.push('/configuracoes')"
|
|
>
|
|
<i class="pi pi-fw pi-cog" />
|
|
</button>
|
|
|
|
<!-- Avatar — trigger do menu de usuário -->
|
|
<button
|
|
class="w-9 h-9 rounded-[10px] border-none cursor-pointer overflow-hidden shrink-0 bg-[var(--surface-ground)] grid place-items-center transition-[transform,box-shadow] duration-150 hover:scale-105 hover:shadow-[0_0_0_2px_var(--primary-color)]"
|
|
v-tooltip.right="{ value: userName, showDelay: 0 }"
|
|
:aria-label="userName"
|
|
@click="toggleUserMenu"
|
|
>
|
|
<img v-if="avatarUrl" :src="avatarUrl" class="w-full h-full object-cover" :alt="userName" />
|
|
<span v-else class="text-[1rem] font-bold text-[var(--text-color)]">{{ initials }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- ── Menu de usuário (popup via AppMenuFooterPanel) ── -->
|
|
<AppMenuFooterPanel ref="footerPanel" variant="rail" />
|
|
</aside>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Indicador lateral do botão ativo — pseudo-elemento não expressável em Tailwind */
|
|
.rail-btn--active::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -10px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 3px;
|
|
height: 22px;
|
|
border-radius: 0 3px 3px 0;
|
|
background: var(--primary-color);
|
|
}
|
|
</style>
|