Layout 100%, Notificações, SetupWizard
This commit is contained in:
+324
-168
@@ -1,10 +1,10 @@
|
||||
<!-- src/layout/AppRailPanel.vue — Painel expansível do Layout 2 -->
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch, nextTick, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import { useMenuStore } from '@/stores/menuStore'
|
||||
import { useLayout } from './composables/layout'
|
||||
import { useMenuStore } from '@/stores/menuStore'
|
||||
import { useLayout } from './composables/layout'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
|
||||
const menuStore = useMenuStore()
|
||||
@@ -19,9 +19,25 @@ const currentSection = computed(() => {
|
||||
return model.find(s => s.label === layoutState.railSectionKey) || null
|
||||
})
|
||||
|
||||
// ── Items da seção (com suporte a children) ──────────────────
|
||||
const sectionItems = computed(() => currentSection.value?.items || [])
|
||||
// Todos os grupos do menu
|
||||
const allSections = computed(() => {
|
||||
const model = menuStore.model || []
|
||||
return model.filter(s => s.label && Array.isArray(s.items) && s.items.length)
|
||||
})
|
||||
|
||||
// "Início" = chave especial __home__
|
||||
const isHome = computed(() => layoutState.railSectionKey === '__home__')
|
||||
|
||||
// Seções visíveis: tudo em Início, só a selecionada nos demais
|
||||
const visibleSections = computed(() =>
|
||||
isHome.value ? allSections.value : (currentSection.value ? [currentSection.value] : [])
|
||||
)
|
||||
|
||||
const panelTitle = computed(() =>
|
||||
isHome.value ? 'Início' : currentSection.value?.label || 'Menu'
|
||||
)
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────
|
||||
function isLocked (item) {
|
||||
if (!item.proBadge || !item.feature) return false
|
||||
try { return !entitlements.has(item.feature) } catch { return false }
|
||||
@@ -57,59 +73,324 @@ function navigate (item) {
|
||||
function closePanel () {
|
||||
layoutState.railPanelOpen = false
|
||||
}
|
||||
|
||||
// ── Busca (todo o menu) ──────────────────────────────────────
|
||||
const query = ref('')
|
||||
const showResults = ref(false)
|
||||
const activeIndex = ref(-1)
|
||||
const forcedOpen = ref(false)
|
||||
const searchEl = ref(null)
|
||||
const searchWrapEl = ref(null)
|
||||
|
||||
const RECENT_KEY = 'menu_search_recent'
|
||||
const recent = ref([])
|
||||
function loadRecent () {
|
||||
try { recent.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]') } catch { recent.value = [] }
|
||||
}
|
||||
function saveRecent (q) {
|
||||
const v = String(q || '').trim()
|
||||
if (!v) return
|
||||
const list = [v, ...recent.value.filter(x => x !== v)].slice(0, 8)
|
||||
recent.value = list
|
||||
localStorage.setItem(RECENT_KEY, JSON.stringify(list))
|
||||
}
|
||||
function clearRecent () {
|
||||
recent.value = []
|
||||
try { localStorage.removeItem(RECENT_KEY) } catch {}
|
||||
}
|
||||
loadRecent()
|
||||
|
||||
watch(query, (v) => {
|
||||
const hasText = !!v?.trim()
|
||||
if (hasText) { forcedOpen.value = false; showResults.value = true; return }
|
||||
showResults.value = forcedOpen.value
|
||||
})
|
||||
|
||||
function clearSearch () {
|
||||
query.value = ''
|
||||
activeIndex.value = -1
|
||||
showResults.value = false
|
||||
forcedOpen.value = false
|
||||
}
|
||||
|
||||
function norm (s) {
|
||||
return String(s || '').toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '').trim()
|
||||
}
|
||||
|
||||
function isVisibleItem (it) {
|
||||
const v = it?.visible
|
||||
if (typeof v === 'function') return !!v()
|
||||
if (v === undefined || v === null) return true
|
||||
return v !== false
|
||||
}
|
||||
|
||||
function flattenMenu (items, trail = []) {
|
||||
const out = []
|
||||
for (const it of (items || [])) {
|
||||
if (!isVisibleItem(it)) continue
|
||||
const nextTrail = [...trail, it?.label].filter(Boolean)
|
||||
if (it?.to && !it?.items?.length) {
|
||||
out.push({ label: it.label || it.to, to: it.to, icon: it.icon, trail: nextTrail, proBadge: !!(it.__showProBadge ?? it.proBadge), feature: it.feature || null })
|
||||
}
|
||||
if (it?.items?.length) out.push(...flattenMenu(it.items, nextTrail))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const allLinks = computed(() => flattenMenu(menuStore.model || []))
|
||||
|
||||
const results = computed(() => {
|
||||
const q = norm(query.value)
|
||||
if (!q) return []
|
||||
const wantPro = q === 'pro' || q.startsWith('pro ') || q.includes(' pro')
|
||||
return allLinks.value
|
||||
.filter(r => {
|
||||
const hay = `${norm(r.label)} ${norm(r.trail.join(' > '))} ${norm(r.to)}`
|
||||
if (hay.includes(q)) return true
|
||||
if (wantPro && (r.proBadge || r.feature)) return true
|
||||
return false
|
||||
})
|
||||
.slice(0, 12)
|
||||
})
|
||||
|
||||
watch(results, (list) => { activeIndex.value = list.length ? 0 : -1 })
|
||||
|
||||
function escapeHtml (s) {
|
||||
return String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
||||
}
|
||||
function highlight (text, q) {
|
||||
const queryNorm = norm(q)
|
||||
const raw = String(text || '')
|
||||
if (!queryNorm) return escapeHtml(raw)
|
||||
const rawNorm = norm(raw)
|
||||
const idx = rawNorm.indexOf(queryNorm)
|
||||
if (idx < 0) return escapeHtml(raw)
|
||||
const before = escapeHtml(raw.slice(0, idx))
|
||||
const mid = escapeHtml(raw.slice(idx, idx + queryNorm.length))
|
||||
const after = escapeHtml(raw.slice(idx + queryNorm.length))
|
||||
return `${before}<mark class="px-1 rounded bg-yellow-200/40">${mid}</mark>${after}`
|
||||
}
|
||||
|
||||
function onSearchKeydown (e) {
|
||||
if (e.key === 'Escape') { showResults.value = false; forcedOpen.value = false; return }
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault()
|
||||
if (!results.value.length) return
|
||||
showResults.value = true
|
||||
activeIndex.value = (activeIndex.value + 1) % results.value.length
|
||||
return
|
||||
}
|
||||
if (e.key === 'ArrowUp') {
|
||||
e.preventDefault()
|
||||
if (!results.value.length) return
|
||||
showResults.value = true
|
||||
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length
|
||||
return
|
||||
}
|
||||
if (e.key === 'Enter' && showResults.value && results.value.length && activeIndex.value >= 0) {
|
||||
e.preventDefault()
|
||||
goToResult(results.value[activeIndex.value])
|
||||
}
|
||||
}
|
||||
|
||||
function onSearchFocus () {
|
||||
if (!query.value?.trim()) { forcedOpen.value = true; showResults.value = true }
|
||||
}
|
||||
|
||||
function applyRecent (q) {
|
||||
query.value = q
|
||||
forcedOpen.value = true
|
||||
showResults.value = true
|
||||
activeIndex.value = 0
|
||||
nextTick(() => searchEl.value?.$el?.querySelector?.('input')?.focus?.())
|
||||
}
|
||||
|
||||
function onDocMouseDown (e) {
|
||||
if (!showResults.value) return
|
||||
if (!searchWrapEl.value?.contains(e.target)) { showResults.value = false; forcedOpen.value = false }
|
||||
}
|
||||
|
||||
onMounted(() => document.addEventListener('mousedown', onDocMouseDown))
|
||||
onBeforeUnmount(() => document.removeEventListener('mousedown', onDocMouseDown))
|
||||
|
||||
async function goToResult (r) {
|
||||
saveRecent(query.value)
|
||||
query.value = ''
|
||||
showResults.value = false
|
||||
activeIndex.value = -1
|
||||
forcedOpen.value = false
|
||||
layoutState.activePath = typeof r.to === 'string' ? r.to : router.resolve(r.to).path
|
||||
await router.push(r.to)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition name="panel-slide">
|
||||
<aside
|
||||
v-if="layoutState.railPanelOpen && currentSection"
|
||||
class="rp"
|
||||
v-if="layoutState.railPanelOpen"
|
||||
class="w-[260px] shrink-0 h-screen flex flex-col border-r border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden"
|
||||
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">
|
||||
<div class="h-14 shrink-0 flex items-center justify-between px-4 border-b border-[var(--surface-border)]">
|
||||
<span class="text-[0.9rem] font-bold tracking-tight text-[var(--text-color)]">
|
||||
{{ panelTitle }}
|
||||
</span>
|
||||
<button
|
||||
class="w-7 h-7 rounded-lg border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer grid place-items-center text-xs transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
|
||||
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>
|
||||
<!-- Busca — só no Início -->
|
||||
<div v-if="isHome" ref="searchWrapEl" class="shrink-0 px-2.5 pt-2.5 border-b border-[var(--surface-border)]">
|
||||
<!-- Campo -->
|
||||
<div class="relative">
|
||||
<div aria-hidden="true" style="position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;overflow:hidden;">
|
||||
<input type="email" name="email" autocomplete="username" tabindex="-1" disabled />
|
||||
<input type="password" name="password" autocomplete="current-password" tabindex="-1" disabled />
|
||||
</div>
|
||||
|
||||
<!-- Item folha -->
|
||||
<FloatLabel variant="on" class="w-full">
|
||||
<IconField class="w-full">
|
||||
<InputIcon class="pi pi-search" />
|
||||
<InputText
|
||||
ref="searchEl"
|
||||
id="rp_menu_search"
|
||||
name="rp_menu_search"
|
||||
type="text"
|
||||
inputmode="search"
|
||||
autocomplete="off"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
data-lpignore="true"
|
||||
data-1p-ignore="true"
|
||||
v-model="query"
|
||||
class="w-full pr-8"
|
||||
variant="filled"
|
||||
@focus="onSearchFocus"
|
||||
@keydown="onSearchKeydown"
|
||||
/>
|
||||
</IconField>
|
||||
<label for="rp_menu_search">Encontrar menu...</label>
|
||||
</FloatLabel>
|
||||
|
||||
<button
|
||||
v-else
|
||||
class="rp__item"
|
||||
:class="{
|
||||
'rp__item--active': isActive(item),
|
||||
'rp__item--locked': isLocked(item)
|
||||
}"
|
||||
@click="navigate(item)"
|
||||
v-if="query.trim()"
|
||||
type="button"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 bg-transparent border-none cursor-pointer opacity-60 hover:opacity-100 text-[var(--text-color-secondary)] text-xs grid place-items-center p-0.5"
|
||||
@mousedown.prevent="clearSearch"
|
||||
aria-label="Limpar busca"
|
||||
>
|
||||
<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>
|
||||
<i class="pi pi-times" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Recentes -->
|
||||
<div
|
||||
v-if="showResults && !query.trim() && recent.length"
|
||||
class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
|
||||
>
|
||||
<div class="flex items-center justify-between px-2.5 py-1.5 text-[1rem] opacity-65 text-[var(--text-color-secondary)]">
|
||||
<span>Recentes</span>
|
||||
<button type="button" class="bg-transparent border-none cursor-pointer opacity-70 hover:opacity-100 text-inherit text-[1rem]" @mousedown.prevent="clearRecent" aria-label="Limpar recentes">
|
||||
<i class="pi pi-trash" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-for="q in recent" :key="q"
|
||||
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors hover:bg-[var(--surface-hover)]"
|
||||
type="button"
|
||||
@click.stop.prevent="applyRecent(q)"
|
||||
>
|
||||
<i class="pi pi-history text-[0.8rem] opacity-70 shrink-0" />
|
||||
<span class="flex-1">{{ q }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Resultados de busca -->
|
||||
<div
|
||||
v-else-if="showResults && results.length"
|
||||
class="mt-1.5 mb-2.5 border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] shadow-sm overflow-hidden"
|
||||
>
|
||||
<button
|
||||
v-for="(r, i) in results" :key="String(r.to)"
|
||||
type="button"
|
||||
@mousedown.prevent="goToResult(r)"
|
||||
class="w-full flex items-center gap-2 px-2.5 py-1.5 bg-transparent border-none cursor-pointer text-[var(--text-color)] text-[1rem] text-left transition-colors"
|
||||
:class="i === activeIndex ? 'bg-[var(--surface-hover)]' : 'hover:bg-[var(--surface-hover)]'"
|
||||
>
|
||||
<i v-if="r.icon" :class="r.icon" class="text-[0.8rem] opacity-70 shrink-0" />
|
||||
<div class="flex flex-col flex-1">
|
||||
<span class="font-medium leading-tight" v-html="highlight(r.label, query)" />
|
||||
<small class="text-[0.68rem] opacity-60">{{ r.trail.join(' > ') }}</small>
|
||||
</div>
|
||||
<span v-if="r.proBadge" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="showResults && query && !results.length" class="py-2 pb-2.5 text-[1rem] opacity-60 text-[var(--text-color-secondary)]">
|
||||
Nenhum item encontrado.
|
||||
</div>
|
||||
|
||||
<div v-else class="pb-2.5" />
|
||||
</div>
|
||||
|
||||
<!-- Nav: todo o menu -->
|
||||
<nav class="flex-1 overflow-y-auto px-2 py-2.5 flex flex-col gap-0.5 [scrollbar-width:thin] [scrollbar-color:var(--surface-border)_transparent]">
|
||||
<template v-for="section in visibleSections" :key="section.label">
|
||||
|
||||
<!-- Label da seção — só exibe quando mostrando múltiplas seções -->
|
||||
<div
|
||||
v-if="visibleSections.length > 1"
|
||||
class="text-[0.62rem] font-extrabold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-55 px-2.5 pb-1.5 pt-3 first:pt-1"
|
||||
>
|
||||
{{ section.label }}
|
||||
</div>
|
||||
|
||||
<template v-for="item in section.items" :key="item.to || item.label">
|
||||
|
||||
<!-- Sub-grupo -->
|
||||
<div v-if="item.items?.length" class="flex flex-col gap-px">
|
||||
<div class="text-[0.6rem] font-bold uppercase tracking-widest text-[var(--text-color-secondary)] opacity-40 px-2.5 pb-1 pt-2">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
<button
|
||||
v-for="child in item.items"
|
||||
:key="child.to || child.label"
|
||||
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
|
||||
:class="{
|
||||
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(child),
|
||||
'opacity-55': isLocked(child)
|
||||
}"
|
||||
@click="navigate(child)"
|
||||
>
|
||||
<i v-if="child.icon" :class="child.icon" class="text-[1rem] shrink-0 opacity-75" />
|
||||
<span class="flex-1">{{ child.label }}</span>
|
||||
<span v-if="isLocked(child)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Item folha -->
|
||||
<button
|
||||
v-else
|
||||
class="w-full flex items-center gap-2.5 px-2.5 py-2 rounded-[9px] border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-left text-[0.83rem] font-medium transition-colors hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)]"
|
||||
:class="{
|
||||
'!bg-[color-mix(in_srgb,var(--primary-color)_10%,transparent)] !text-[var(--primary-color)] !font-semibold': isActive(item),
|
||||
'opacity-55': isLocked(item)
|
||||
}"
|
||||
@click="navigate(item)"
|
||||
>
|
||||
<i v-if="item.icon" :class="item.icon" class="text-[1rem] shrink-0 opacity-75" />
|
||||
<span class="flex-1">{{ item.label }}</span>
|
||||
<span v-if="isLocked(item)" class="text-[0.58rem] font-extrabold uppercase tracking-widest px-1.5 py-px rounded border border-[var(--surface-border)] text-[var(--text-color-secondary)] opacity-70">PRO</span>
|
||||
</button>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
</nav>
|
||||
</aside>
|
||||
@@ -117,134 +398,9 @@ function closePanel () {
|
||||
</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;
|
||||
transition: width 0.22s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.18s ease;
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel-slide-enter-from,
|
||||
@@ -252,4 +408,4 @@ function closePanel () {
|
||||
width: 0 !important;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user