ZERADO
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user