You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
3.7 KiB
180 lines
3.7 KiB
<script setup lang="ts">
|
|
export interface NavItem {
|
|
label: string
|
|
to?: string
|
|
icon?: string
|
|
children?: NavItem[]
|
|
}
|
|
|
|
const props = defineProps<{
|
|
nav: NavItem[]
|
|
}>()
|
|
|
|
const route = useRoute()
|
|
const expandedMenus = ref<Set<string>>(new Set())
|
|
|
|
const toggleMenu = (label: string) => {
|
|
if (expandedMenus.value.has(label)) {
|
|
expandedMenus.value.delete(label)
|
|
} else {
|
|
expandedMenus.value.add(label)
|
|
}
|
|
}
|
|
|
|
const isExpanded = (label: string) => expandedMenus.value.has(label)
|
|
|
|
const hasMatchingChild = (item: NavItem): boolean => {
|
|
if (!item.children) return false
|
|
return item.children.some(child => child.to === route.path || hasMatchingChild(child))
|
|
}
|
|
|
|
const expandMatchingParents = () => {
|
|
for (const item of props.nav) {
|
|
if (item.children && hasMatchingChild(item)) {
|
|
expandedMenus.value.add(item.label)
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
expandMatchingParents()
|
|
})
|
|
|
|
watch(() => route.path, () => {
|
|
expandMatchingParents()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<nav class="sidebar-nav">
|
|
<template v-for="item in nav" :key="item.label">
|
|
<div v-if="item.children" class="nav-group">
|
|
<button
|
|
class="sidebar-link nav-group-header"
|
|
:class="{ expanded: isExpanded(item.label) }"
|
|
@click="toggleMenu(item.label)"
|
|
>
|
|
<Icon v-if="item.icon" :name="item.icon" class="sidebar-icon" />
|
|
<span class="sidebar-link-label">{{ item.label }}</span>
|
|
<Icon class="nav-chevron" name="lucide:chevron-down" />
|
|
</button>
|
|
<div v-show="isExpanded(item.label)" class="nav-children">
|
|
<NuxtLink
|
|
v-for="child in item.children"
|
|
:key="child.to"
|
|
:to="child.to"
|
|
class="sidebar-link sidebar-link-child"
|
|
active-class="active"
|
|
>
|
|
<span class="sidebar-link-label">{{ child.label }}</span>
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
<NuxtLink
|
|
v-else
|
|
:to="item.to"
|
|
class="sidebar-link"
|
|
active-class="active"
|
|
>
|
|
<Icon v-if="item.icon" :name="item.icon" class="sidebar-icon" />
|
|
<span class="sidebar-link-label">{{ item.label }}</span>
|
|
</NuxtLink>
|
|
</template>
|
|
</nav>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.sidebar-nav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding: 16px 12px;
|
|
flex: 1;
|
|
}
|
|
|
|
.sidebar-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
text-decoration: none;
|
|
color: var(--color-on-dark-soft);
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
transition: all 0.15s ease;
|
|
}
|
|
|
|
.sidebar-link:hover {
|
|
background: rgba(255, 255, 255, 0.06);
|
|
color: var(--color-on-dark);
|
|
}
|
|
|
|
.sidebar-link.active {
|
|
background: var(--color-primary);
|
|
color: var(--color-on-primary);
|
|
}
|
|
|
|
.sidebar-icon {
|
|
width: 18px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.sidebar-link:hover .sidebar-icon,
|
|
.sidebar-link.active .sidebar-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
.nav-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.nav-group-header {
|
|
width: 100%;
|
|
justify-content: flex-start;
|
|
text-align: left;
|
|
}
|
|
|
|
.nav-chevron {
|
|
width: 14px;
|
|
height: 14px;
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
opacity: 0.6;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.nav-group-header.expanded .nav-chevron {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.nav-children {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
padding-left: 38px;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.sidebar-link-child {
|
|
padding: 8px 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.sidebar-link-child::before {
|
|
content: '';
|
|
width: 4px;
|
|
height: 4px;
|
|
border-radius: 50%;
|
|
background: var(--color-on-dark-soft);
|
|
margin-right: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.sidebar-link-child.active::before {
|
|
background: var(--color-on-primary);
|
|
}
|
|
</style>
|
|
|