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
+246
View File
@@ -0,0 +1,246 @@
<!-- src/layout/AppRailPanel.vue Painel expansível do Layout 2 -->
<script setup>
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useMenuStore } from '@/stores/menuStore'
import { useLayout } from './composables/layout'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
const menuStore = useMenuStore()
const { layoutState } = useLayout()
const entitlements = useEntitlementsStore()
const router = useRouter()
const route = useRoute()
// ── Seção ativa ──────────────────────────────────────────────
const currentSection = computed(() => {
const model = menuStore.model || []
return model.find(s => s.label === layoutState.railSectionKey) || null
})
// ── Items da seção (com suporte a children) ──────────────────
const sectionItems = computed(() => currentSection.value?.items || [])
function isLocked (item) {
if (!item.proBadge || !item.feature) return false
try { return !entitlements.has(item.feature) } catch { return false }
}
function isActive (item) {
const active = String(layoutState.activePath || route.path || '')
if (!item.to) return false
const p = typeof item.to === 'string' ? item.to : ''
return active === p || active.startsWith(p + '/')
}
function navigate (item) {
if (isLocked(item)) {
router.push({ name: 'upgrade', query: { feature: item.feature || '' } })
return
}
if (item.to) {
layoutState.activePath = typeof item.to === 'string' ? item.to : router.resolve(item.to).path
router.push(item.to)
}
}
function closePanel () {
layoutState.railPanelOpen = false
}
</script>
<template>
<Transition name="panel-slide">
<aside
v-if="layoutState.railPanelOpen && currentSection"
class="rp"
aria-label="Menu lateral"
>
<!-- Header -->
<div class="rp__head">
<span class="rp__title">{{ currentSection.label }}</span>
<button class="rp__close" aria-label="Fechar painel" @click="closePanel">
<i class="pi pi-times" />
</button>
</div>
<!-- Items -->
<nav class="rp__nav">
<template v-for="item in sectionItems" :key="item.to || item.label">
<!-- Item com filhos (sub-seção) -->
<div v-if="item.items?.length" class="rp__group">
<div class="rp__group-label">{{ item.label }}</div>
<button
v-for="child in item.items"
:key="child.to || child.label"
class="rp__item"
:class="{
'rp__item--active': isActive(child),
'rp__item--locked': isLocked(child)
}"
@click="navigate(child)"
>
<i v-if="child.icon" :class="child.icon" class="rp__item-icon" />
<span class="rp__item-label">{{ child.label }}</span>
<span v-if="isLocked(child)" class="rp__pro">PRO</span>
</button>
</div>
<!-- Item folha -->
<button
v-else
class="rp__item"
:class="{
'rp__item--active': isActive(item),
'rp__item--locked': isLocked(item)
}"
@click="navigate(item)"
>
<i v-if="item.icon" :class="item.icon" class="rp__item-icon" />
<span class="rp__item-label">{{ item.label }}</span>
<span v-if="isLocked(item)" class="rp__pro">PRO</span>
</button>
</template>
</nav>
</aside>
</Transition>
</template>
<style scoped>
/* ─── Panel ──────────────────────────────────────────────── */
.rp {
width: 260px;
flex-shrink: 0;
height: 100vh;
display: flex;
flex-direction: column;
border-right: 1px solid var(--surface-border);
background: var(--surface-card);
overflow: hidden;
}
/* ─── Header ─────────────────────────────────────────────── */
.rp__head {
height: 56px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
border-bottom: 1px solid var(--surface-border);
}
.rp__title {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--text-color);
}
.rp__close {
width: 28px;
height: 28px;
border-radius: 7px;
border: none;
background: transparent;
color: var(--text-color-secondary);
cursor: pointer;
display: grid;
place-items: center;
font-size: 0.75rem;
transition: background 0.15s, color 0.15s;
}
.rp__close:hover {
background: var(--surface-ground);
color: var(--text-color);
}
/* ─── Nav list ───────────────────────────────────────────── */
.rp__nav {
flex: 1;
overflow-y: auto;
padding: 10px 8px;
display: flex;
flex-direction: column;
gap: 2px;
scrollbar-width: thin;
scrollbar-color: var(--surface-border) transparent;
}
/* ─── Group ──────────────────────────────────────────────── */
.rp__group {
display: flex;
flex-direction: column;
gap: 1px;
margin-top: 12px;
}
.rp__group:first-child { margin-top: 0; }
.rp__group-label {
font-size: 0.62rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-color-secondary);
opacity: 0.55;
padding: 2px 10px 6px;
}
/* ─── Item ───────────────────────────────────────────────── */
.rp__item {
width: 100%;
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
border-radius: 9px;
border: none;
background: transparent;
color: var(--text-color-secondary);
cursor: pointer;
text-align: left;
font-size: 0.83rem;
font-weight: 500;
transition: background 0.13s, color 0.13s;
}
.rp__item:hover {
background: var(--surface-ground);
color: var(--text-color);
}
.rp__item--active {
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
color: var(--primary-color);
font-weight: 600;
}
.rp__item--locked {
opacity: 0.55;
}
.rp__item-icon {
font-size: 0.85rem;
flex-shrink: 0;
opacity: 0.75;
}
.rp__item-label { flex: 1; }
.rp__pro {
font-size: 0.58rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 1px 6px;
border-radius: 4px;
border: 1px solid var(--surface-border);
color: var(--text-color-secondary);
opacity: 0.7;
}
/* ─── Slide transition ───────────────────────────────────── */
.panel-slide-enter-active,
.panel-slide-leave-active {
transition: width 0.22s cubic-bezier(0.22, 1, 0.36, 1),
opacity 0.18s ease;
overflow: hidden;
}
.panel-slide-enter-from,
.panel-slide-leave-to {
width: 0 !important;
opacity: 0;
}
</style>