Layout 100%, Notificações, SetupWizard
This commit is contained in:
@@ -20,6 +20,7 @@ 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,
|
||||
@@ -38,7 +39,21 @@ const initials = computed(() => {
|
||||
return (a + b).toUpperCase()
|
||||
})
|
||||
|
||||
const userName = computed(() => sessionUser.value?.user_metadata?.full_name || sessionUser.value?.email || 'Conta')
|
||||
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) {
|
||||
@@ -52,7 +67,6 @@ function selectSection (section) {
|
||||
|
||||
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 : ''
|
||||
@@ -77,21 +91,33 @@ async function signOut () {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="rail">
|
||||
<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">
|
||||
|
||||
<!-- ── Brand ──────────────────────────────────────────── -->
|
||||
<div class="rail__brand">
|
||||
<span class="rail__psi">Ψ</span>
|
||||
<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="rail__nav" role="navigation" aria-label="Menu principal">
|
||||
<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"
|
||||
>
|
||||
<i class="pi pi-fw pi-home" />
|
||||
</button>
|
||||
|
||||
<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 }"
|
||||
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)"
|
||||
>
|
||||
@@ -100,49 +126,48 @@ async function signOut () {
|
||||
</nav>
|
||||
|
||||
<!-- ── Rodapé ─────────────────────────────────────────── -->
|
||||
<div class="rail__foot">
|
||||
<!-- Configurações de layout -->
|
||||
<div class="w-full flex flex-col items-center gap-1.5 py-2 pb-3 border-t border-[var(--surface-border)]">
|
||||
<button
|
||||
class="rail__btn rail__btn--sm"
|
||||
v-tooltip.right="{ value: 'Meu Perfil', showDelay: 400 }"
|
||||
aria-label="Meu Perfil"
|
||||
@click="goTo('/account/profile')"
|
||||
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="goTo('/configuracoes')"
|
||||
>
|
||||
<i class="pi pi-fw pi-cog" />
|
||||
</button>
|
||||
|
||||
<!-- Avatar / user -->
|
||||
<button
|
||||
class="rail__av-btn"
|
||||
v-tooltip.right="{ value: userName, showDelay: 400 }"
|
||||
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="toggleUserPop"
|
||||
>
|
||||
<img v-if="avatarUrl" :src="avatarUrl" class="rail__av-img" :alt="userName" />
|
||||
<span v-else class="rail__av-init">{{ initials }}</span>
|
||||
<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>
|
||||
|
||||
<!-- ── 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 class="min-w-[210px] p-1 flex flex-col gap-0.5">
|
||||
<div class="flex items-center gap-2.5 px-2.5 py-2 pb-2.5">
|
||||
<div class="w-9 h-9 rounded-[9px] overflow-hidden shrink-0 bg-[var(--surface-ground)] grid place-items-center border border-[var(--surface-border)]">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" class="w-full h-full object-cover" />
|
||||
<span v-else class="text-[1rem] font-bold text-[var(--text-color)]">{{ initials }}</span>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="rail-pop__name">{{ userName }}</div>
|
||||
<div class="rail-pop__email">{{ sessionUser?.email }}</div>
|
||||
<div class="text-[0.83rem] font-semibold text-[var(--text-color)] truncate">{{ userName }}</div>
|
||||
<div class="text-[0.68rem] text-[var(--text-color-secondary)] truncate">{{ sessionUser?.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rail-pop__divider" />
|
||||
<div class="h-px bg-[var(--surface-border)] my-0.5" />
|
||||
|
||||
<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')" />
|
||||
<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" />
|
||||
<div class="h-px bg-[var(--surface-border)] my-0.5" />
|
||||
|
||||
<Button label="Sair" icon="pi pi-sign-out" severity="danger" text class="w-full justify-start" @click="signOut" />
|
||||
</div>
|
||||
@@ -151,78 +176,8 @@ async function signOut () {
|
||||
</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 {
|
||||
/* Indicador lateral do botão ativo — pseudo-elemento não expressável em Tailwind */
|
||||
.rail-btn--active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
@@ -233,97 +188,4 @@ async function signOut () {
|
||||
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>
|
||||
</style>
|
||||
Reference in New Issue
Block a user