8 changed files with 226 additions and 121 deletions
@ -0,0 +1,158 @@ |
|||
<script setup lang="ts"> |
|||
export interface NavItem { |
|||
label: string |
|||
to?: string |
|||
icon?: string |
|||
children?: NavItem[] |
|||
} |
|||
|
|||
defineProps<{ |
|||
nav: NavItem[] |
|||
}>() |
|||
|
|||
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) |
|||
</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> |
|||
Binary file not shown.
Loading…
Reference in new issue