feat: add sidebar collapse, category/tag filtering, and workspace CRUD (Phase D)

- 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>
This commit is contained in:
Michael Chihlas
2026-02-15 05:05:25 -05:00
parent 09cd05e143
commit f334ba861b
9 changed files with 301 additions and 40 deletions

View File

@@ -8,9 +8,10 @@ interface NavItemProps {
label: string
badge?: number | 'dot'
matchPaths?: string[]
collapsed?: boolean
}
export function NavItem({ href, icon: Icon, label, badge, matchPaths }: NavItemProps) {
export function NavItem({ href, icon: Icon, label, badge, matchPaths, collapsed }: NavItemProps) {
const location = useLocation()
const isActive = matchPaths
? matchPaths.some(p => location.pathname.startsWith(p))
@@ -18,6 +19,31 @@ export function NavItem({ href, icon: Icon, label, badge, matchPaths }: NavItemP
? 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}