- Sidebar collapse/expand toggle with icon-only rail mode (persisted) - Sidebar category/tag clicks navigate to /trees with URL params - TreeLibraryPage syncs filters from URL search params bidirectionally - Workspace create modal with icon picker and auto-slug generation - TopBar logo adapts to collapsed sidebar state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
78 lines
2.6 KiB
TypeScript
78 lines
2.6 KiB
TypeScript
import { Link, useLocation } from 'react-router-dom'
|
|
import type { LucideIcon } from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
interface NavItemProps {
|
|
href: string
|
|
icon: LucideIcon
|
|
label: string
|
|
badge?: number | 'dot'
|
|
matchPaths?: string[]
|
|
collapsed?: boolean
|
|
}
|
|
|
|
export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed }: NavItemProps) {
|
|
const location = useLocation()
|
|
const isActive = matchPaths
|
|
? matchPaths.some(p => location.pathname.startsWith(p))
|
|
: href === '/'
|
|
? location.pathname === '/'
|
|
: location.pathname.startsWith(href)
|
|
|
|
if (collapsed) {
|
|
return (
|
|
<Link
|
|
to={href}
|
|
className={cn(
|
|
'group relative flex items-center justify-center rounded-lg p-2 transition-all duration-120',
|
|
isActive
|
|
? 'bg-[hsl(var(--sidebar-active))] text-foreground'
|
|
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
|
)}
|
|
title={label}
|
|
>
|
|
{isActive && (
|
|
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
|
)}
|
|
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
|
|
{badge !== undefined && badge !== 0 && badge !== 'dot' && (
|
|
<span className="absolute -right-0.5 -top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-primary text-[0.5rem] font-bold text-primary-foreground">
|
|
{badge}
|
|
</span>
|
|
)}
|
|
</Link>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
to={href}
|
|
className={cn(
|
|
'group relative flex items-center gap-3 rounded-lg px-3 py-2 text-[0.8125rem] font-medium transition-all duration-120',
|
|
isActive
|
|
? 'bg-[hsl(var(--sidebar-active))] text-foreground'
|
|
: 'text-muted-foreground hover:bg-[hsl(var(--sidebar-hover))] hover:text-foreground'
|
|
)}
|
|
>
|
|
{/* Active indicator bar */}
|
|
{isActive && (
|
|
<div className="absolute left-0 top-1/2 h-6 w-[3px] -translate-y-1/2 rounded-r-full bg-gradient-brand" />
|
|
)}
|
|
|
|
<Icon size={18} className={cn('shrink-0', isActive ? 'opacity-100' : 'opacity-70')} />
|
|
<span className="truncate">{label}</span>
|
|
|
|
{/* Badge */}
|
|
{badge !== undefined && badge !== 0 && (
|
|
badge === 'dot' ? (
|
|
<span className="ml-auto h-1.5 w-1.5 shrink-0 rounded-full bg-brand-gradient-from" />
|
|
) : (
|
|
<span className="ml-auto shrink-0 rounded-full bg-card border border-border px-2 text-[0.6875rem] font-label text-muted-foreground">
|
|
{badge}
|
|
</span>
|
|
)
|
|
)}
|
|
</Link>
|
|
)
|
|
}
|