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

<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>