This commit is contained in:
2026-07-21 11:55:40 +05:30
parent 03bc85b788
commit 4108062416
2 changed files with 125 additions and 92 deletions
@@ -105,18 +105,36 @@ function SidebarContent({
pathname: string
isMobile: boolean
}) {
const iconOnly = !isMobile && collapsed
// Which parent menus are open. Starts with the parent that owns the active
// route auto-expanded; user toggles are preserved across navigation.
const [expanded, setExpanded] = useState<Record<string, boolean>>({})
useEffect(() => {
const parent = items.find((i) =>
i.children?.some((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
)
if (parent) {
setExpanded((prev) => (prev[parent.code] ? prev : { ...prev, [parent.code]: true }))
}
}, [pathname, items])
const toggleExpand = (code: string) =>
setExpanded((prev) => ({ ...prev, [code]: !prev[code] }))
return (
<nav
className={cn(
"flex h-full flex-col rounded-3xl bg-white p-3 shadow-sm ring-1 ring-black/5 transition-[width] duration-200",
"flex h-full flex-col rounded-3xl bg-white p-3 shadow-sm ring-1 ring-black/5 transition-[width] duration-300 ease-in-out",
!isMobile && (collapsed ? "w-20" : "w-64")
)}
>
{/* Header */}
<div
className={cn(
"mb-10 flex items-center gap-2.5 px-4 py-3",
!isMobile && collapsed ? "flex-col-reverse justify-center gap-3 px-0" : "justify-between"
"mb-8 flex shrink-0 items-center gap-2.5 px-4 py-3",
iconOnly ? "flex-col-reverse justify-center gap-3 px-0" : "justify-between"
)}
>
<Link href="/dashboard" className="flex items-center gap-2.5" onClick={onClose}>
@@ -125,7 +143,7 @@ function SidebarContent({
<path d="M24 16c-3-8-11-12-16-8s-3 14 6 14c5 0 8.5-2.5 10-6 1.5 3.5 5 6 10 6 9 0 11-10 6-14s-13 0-16 8Z" />
</svg>
</div>
{(!collapsed || isMobile) && (
{!iconOnly && (
<span className="text-lg font-bold tracking-tight text-slate-900">Hexa ERP</span>
)}
</Link>
@@ -140,79 +158,116 @@ function SidebarContent({
</button>
</div>
{/* Nav items */}
<ul className="flex flex-col gap-1">
{/* Nav items — scrolls internally when it overflows, without a visible
scrollbar so the rounded panel stays clean. */}
<ul className="flex min-h-0 flex-1 flex-col gap-1 overflow-y-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
{items.map((item) => {
const isActive =
item.href === "/dashboard" ? pathname === item.href : pathname.startsWith(item.href)
const hasChildren = !!item.children?.length && !iconOnly
const isOpen = !!expanded[item.code]
return (
<li key={item.href}>
<Link
href={item.href}
title={!isMobile && collapsed ? item.title : undefined}
onClick={onClose}
<div
className={cn(
"flex items-center gap-3 rounded-2xl px-4 py-3 text-base font-semibold transition-colors",
!isMobile && collapsed && "justify-center px-0",
isActive
? "bg-indigo-50 text-indigo-600"
: "text-slate-700 hover:bg-slate-50"
"flex items-center rounded-2xl transition-colors",
isActive ? "bg-indigo-50" : "hover:bg-slate-50"
)}
>
<item.icon
className={cn("size-5 shrink-0", isActive ? "text-indigo-600" : "text-slate-400")}
/>
{(!collapsed || isMobile) && (
<>
<span className="flex-1">{item.title}</span>
{item.chevron && !item.children && !isActive && (
<ChevronRight className="size-4 shrink-0 text-slate-300" />
)}
</>
)}
</Link>
<Link
href={item.href}
title={iconOnly ? item.title : undefined}
onClick={onClose}
className={cn(
"flex flex-1 items-center gap-3 rounded-2xl px-4 py-3 text-base font-semibold",
iconOnly && "justify-center px-0",
isActive ? "text-indigo-600" : "text-slate-700"
)}
>
<item.icon
className={cn("size-5 shrink-0", isActive ? "text-indigo-600" : "text-slate-400")}
/>
{!iconOnly && (
<>
<span className="flex-1">{item.title}</span>
{item.chevron && !hasChildren && (
<ChevronRight
className={cn("size-4 shrink-0", isActive ? "text-indigo-400" : "text-slate-300")}
/>
)}
</>
)}
</Link>
{item.children && (!collapsed || isMobile) && (
<ul className="mt-1 flex flex-col gap-0.5 pl-11">
{(() => {
// Longest-matching href wins so a shared prefix (e.g. "Item" and
// "Category" both live under /dashboard/products) doesn't light up
// more than one sub-item at once.
const activeChild = [...item.children]
.filter((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
.sort((a, b) => b.href.length - a.href.length)[0]
return item.children.map((child) => {
const childActive = child.href === activeChild?.href
return (
<li key={child.href}>
<Link
href={child.href}
onClick={onClose}
className={cn(
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors",
childActive
? "bg-indigo-50 text-indigo-600"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
)}
>
<child.icon
className={cn("size-4 shrink-0", childActive ? "text-indigo-600" : "text-slate-400")}
/>
{child.title}
</Link>
</li>
)
})
})()}
</ul>
{hasChildren && (
<button
type="button"
onClick={() => toggleExpand(item.code)}
aria-label={isOpen ? `Collapse ${item.title}` : `Expand ${item.title}`}
aria-expanded={isOpen}
className={cn(
"mr-2 flex size-7 shrink-0 items-center justify-center rounded-lg transition-colors hover:bg-white/60",
isActive ? "text-indigo-500" : "text-slate-400"
)}
>
<ChevronRight
className={cn("size-4 transition-transform duration-300 ease-in-out", isOpen && "rotate-90")}
/>
</button>
)}
</div>
{hasChildren && (
<div
className={cn(
"grid transition-all duration-300 ease-in-out",
isOpen ? "mt-1 grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0"
)}
>
<div className="overflow-hidden">
<ul className="flex flex-col gap-0.5 pl-11">
{(() => {
// Longest-matching href wins so a shared prefix (e.g. "Item" and
// "Category" both live under /dashboard/products) doesn't light up
// more than one sub-item at once.
const activeChild = [...item.children!]
.filter((c) => pathname === c.href || pathname.startsWith(`${c.href}/`))
.sort((a, b) => b.href.length - a.href.length)[0]
return item.children!.map((child) => {
const childActive = child.href === activeChild?.href
return (
<li key={child.href}>
<Link
href={child.href}
onClick={onClose}
tabIndex={isOpen ? undefined : -1}
className={cn(
"flex items-center gap-2.5 rounded-xl px-3 py-2 text-sm font-medium transition-colors",
childActive
? "bg-indigo-50 text-indigo-600"
: "text-slate-500 hover:bg-slate-50 hover:text-slate-700"
)}
>
<child.icon
className={cn("size-4 shrink-0", childActive ? "text-indigo-600" : "text-slate-400")}
/>
{child.title}
</Link>
</li>
)
})
})()}
</ul>
</div>
</div>
)}
</li>
)
})}
</ul>
<div className="mt-auto flex items-center justify-center pt-6">
<div className="flex items-center justify-center pt-6">
<div className="flex size-12 items-center justify-center rounded-2xl bg-slate-50 ring-1 ring-black/5">
<svg viewBox="0 0 48 32" className="h-4 w-6 fill-slate-400">
<path d="M24 16c-3-8-11-12-16-8s-3 14 6 14c5 0 8.5-2.5 10-6 1.5 3.5 5 6 10 6 9 0 11-10 6-14s-13 0-16 8Z" />