This commit is contained in:
Leonardo
2026-03-06 06:37:13 -03:00
parent d58dc21297
commit f733db8436
146 changed files with 43436 additions and 12779 deletions

329
src/layout/AppRail.vue Normal file
View File

@@ -0,0 +1,329 @@
<!-- src/layout/AppRail.vue Mini icon rail (Layout 2) -->
<script setup>
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import Popover from 'primevue/popover'
import Button from 'primevue/button'
import { useMenuStore } from '@/stores/menuStore'
import { useLayout } from './composables/layout'
import { sessionUser } from '@/app/session'
import { supabase } from '@/lib/supabase/client'
const menuStore = useMenuStore()
const { layoutConfig, layoutState, isDesktop } = useLayout()
const router = useRouter()
// ── 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)
.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')
// ── 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
// verifica se algum filho está ativo
const active = String(layoutState.activePath || '')
return section.items.some(i => {
const p = typeof i.to === 'string' ? i.to : ''
return p && active.startsWith(p)
})
}
// ── Popover do usuário (rodapé) ───────────────────────────────
const userPop = ref(null)
function toggleUserPop (e) { userPop.value?.toggle(e) }
function goTo (path) {
try { userPop.value?.hide() } catch {}
router.push(path)
}
async function signOut () {
try { userPop.value?.hide() } catch {}
try { await supabase.auth.signOut() } catch {}
router.push('/auth/login')
}
</script>
<template>
<aside class="rail">
<!-- Brand -->
<div class="rail__brand">
<span class="rail__psi">Ψ</span>
</div>
<!-- Nav icons -->
<nav class="rail__nav" role="navigation" aria-label="Menu principal">
<button
v-for="section in railSections"
:key="section.key"
class="rail__btn"
:class="{ 'rail__btn--active': isActiveSectionOrChild(section) }"
v-tooltip.right="{ value: section.label, showDelay: 400 }"
:aria-label="section.label"
@click="selectSection(section)"
>
<i :class="section.icon" />
</button>
</nav>
<!-- Rodapé -->
<div class="rail__foot">
<!-- Configurações de layout -->
<button
class="rail__btn rail__btn--sm"
v-tooltip.right="{ value: 'Meu Perfil', showDelay: 400 }"
aria-label="Meu Perfil"
@click="goTo('/account/profile')"
>
<i class="pi pi-fw pi-cog" />
</button>
<!-- Avatar / user -->
<button
class="rail__av-btn"
v-tooltip.right="{ value: userName, showDelay: 400 }"
:aria-label="userName"
@click="toggleUserPop"
>
<img v-if="avatarUrl" :src="avatarUrl" class="rail__av-img" :alt="userName" />
<span v-else class="rail__av-init">{{ initials }}</span>
</button>
</div>
<!-- Popover usuário -->
<Popover ref="userPop" appendTo="body">
<div class="rail-pop">
<div class="rail-pop__user">
<div class="rail-pop__av">
<img v-if="avatarUrl" :src="avatarUrl" class="rail-pop__av-img" />
<span v-else class="rail-pop__av-init">{{ initials }}</span>
</div>
<div class="min-w-0">
<div class="rail-pop__name">{{ userName }}</div>
<div class="rail-pop__email">{{ sessionUser?.email }}</div>
</div>
</div>
<div class="rail-pop__divider" />
<Button label="Meu Perfil" icon="pi pi-user" text class="w-full justify-start" @click="goTo('/account/profile')" />
<Button label="Segurança" icon="pi pi-shield" text class="w-full justify-start" @click="goTo('/account/security')" />
<div class="rail-pop__divider" />
<Button label="Sair" icon="pi pi-sign-out" severity="danger" text class="w-full justify-start" @click="signOut" />
</div>
</Popover>
</aside>
</template>
<style scoped>
/* ─── Rail container ─────────────────────────────────────── */
.rail {
width: 60px;
flex-shrink: 0;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
border-right: 1px solid var(--surface-border);
background: var(--surface-card);
z-index: 50;
user-select: none;
}
/* ─── Brand ──────────────────────────────────────────────── */
.rail__brand {
width: 100%;
height: 56px;
display: grid;
place-items: center;
border-bottom: 1px solid var(--surface-border);
flex-shrink: 0;
}
.rail__psi {
font-size: 1.35rem;
font-weight: 800;
color: var(--primary-color);
text-shadow: 0 0 20px color-mix(in srgb, var(--primary-color) 40%, transparent);
line-height: 1;
}
/* ─── Nav ────────────────────────────────────────────────── */
.rail__nav {
flex: 1;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 0;
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
}
.rail__nav::-webkit-scrollbar { display: none; }
/* ─── Buttons ────────────────────────────────────────────── */
.rail__btn {
width: 40px;
height: 40px;
border-radius: 10px;
display: grid;
place-items: center;
border: none;
background: transparent;
color: var(--text-color-secondary);
cursor: pointer;
font-size: 1rem;
transition: background 0.15s, color 0.15s, transform 0.12s;
position: relative;
flex-shrink: 0;
}
.rail__btn:hover {
background: var(--surface-ground);
color: var(--text-color);
transform: scale(1.08);
}
.rail__btn--active {
background: color-mix(in srgb, var(--primary-color) 12%, transparent);
color: var(--primary-color);
}
.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);
}
.rail__btn--sm {
width: 36px;
height: 36px;
font-size: 0.875rem;
}
/* ─── Footer ─────────────────────────────────────────────── */
.rail__foot {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 8px 0 12px;
border-top: 1px solid var(--surface-border);
}
/* ─── Avatar button ──────────────────────────────────────── */
.rail__av-btn {
width: 36px;
height: 36px;
border-radius: 10px;
border: none;
cursor: pointer;
overflow: hidden;
transition: transform 0.12s, box-shadow 0.15s;
background: var(--surface-ground);
display: grid;
place-items: center;
flex-shrink: 0;
}
.rail__av-btn:hover {
transform: scale(1.08);
box-shadow: 0 0 0 2px var(--primary-color);
}
.rail__av-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.rail__av-init {
font-size: 0.78rem;
font-weight: 700;
color: var(--text-color);
}
/* ─── Popover ────────────────────────────────────────────── */
.rail-pop {
min-width: 210px;
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
}
.rail-pop__user {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px 10px;
}
.rail-pop__av {
width: 36px;
height: 36px;
border-radius: 9px;
overflow: hidden;
flex-shrink: 0;
background: var(--surface-ground);
display: grid;
place-items: center;
border: 1px solid var(--surface-border);
}
.rail-pop__av-img { width: 100%; height: 100%; object-fit: cover; }
.rail-pop__av-init { font-size: 0.78rem; font-weight: 700; color: var(--text-color); }
.rail-pop__name {
font-size: 0.83rem;
font-weight: 600;
color: var(--text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rail-pop__email {
font-size: 0.68rem;
color: var(--text-color-secondary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.rail-pop__divider {
height: 1px;
background: var(--surface-border);
margin: 2px 0;
}
</style>