feat: implement collapsible sidebar navigation with icons and active item detection

This commit is contained in:
mlogclub
2026-05-30 20:00:44 +08:00
parent 86ff71596d
commit 9cabee4d6d
6 changed files with 168 additions and 30 deletions
+1
View File
@@ -65,6 +65,7 @@ export function AppSidebar({ ...props }: ComponentProps<typeof Sidebar>) {
{navSections.map((section) => (
<NavMain
key={section.titleKey}
icon={section.icon}
title={t(section.titleKey)}
items={section.items.map((item) => ({
...item,
+50 -29
View File
@@ -1,21 +1,35 @@
"use client"
import { ChevronRightIcon } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { useEffect, useState } from "react"
import {
SidebarGroupLabel,
dashboardNavSectionHasActiveItem,
isDashboardNavItemActive,
} from "@/lib/navigation-active"
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible"
import {
SidebarGroup,
SidebarGroupContent,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from "@/components/ui/sidebar"
export function NavMain({
icon,
title,
items,
}: {
icon?: React.ReactNode
title: string
items: ReadonlyArray<{
title: string
@@ -24,38 +38,45 @@ export function NavMain({
}>
}) {
const pathname = usePathname()
const hasActiveItem = dashboardNavSectionHasActiveItem(items, pathname)
const [open, setOpen] = useState(true)
const isActive = (itemUrl: string) => {
const normalizePath = (path: string) =>
path !== "/" ? path.replace(/\/+$/, "") : path
const currentPath = normalizePath(pathname)
const targetPath = normalizePath(itemUrl)
if (targetPath === "/" || targetPath === "/dashboard") {
return currentPath === targetPath
useEffect(() => {
if (hasActiveItem) {
setOpen(true)
}
return currentPath === targetPath || currentPath.startsWith(targetPath + "/")
}
}, [hasActiveItem])
return (
<SidebarGroup>
<SidebarGroupLabel>{title}</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{items.map((item) => (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
tooltip={item.title}
render={<Link href={item.url} />}
isActive={isActive(item.url)}
>
{item.icon}
<span>{item.title}</span>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroupContent>
<SidebarMenu>
<Collapsible
open={open}
onOpenChange={setOpen}
className="group/collapsible"
render={<SidebarMenuItem />}
>
<CollapsibleTrigger render={<SidebarMenuButton tooltip={title} />}>
{icon}
<span>{title}</span>
<ChevronRightIcon className="ml-auto transition-transform duration-200 group-data-open/collapsible:rotate-90" />
</CollapsibleTrigger>
<CollapsibleContent>
<SidebarMenuSub>
{items.map((item) => (
<SidebarMenuSubItem key={item.title}>
<SidebarMenuSubButton
render={<Link href={item.url} />}
isActive={isDashboardNavItemActive(pathname, item.url)}
>
<span>{item.title}</span>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</CollapsibleContent>
</Collapsible>
</SidebarMenu>
</SidebarGroup>
)
}
+21
View File
@@ -0,0 +1,21 @@
"use client"
import { Collapsible as CollapsiblePrimitive } from "@base-ui/react/collapsible"
function Collapsible({ ...props }: CollapsiblePrimitive.Root.Props) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({ ...props }: CollapsiblePrimitive.Trigger.Props) {
return (
<CollapsiblePrimitive.Trigger data-slot="collapsible-trigger" {...props} />
)
}
function CollapsibleContent({ ...props }: CollapsiblePrimitive.Panel.Props) {
return (
<CollapsiblePrimitive.Panel data-slot="collapsible-content" {...props} />
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }