feat: add workspace system and sidebar layout (UI design system Phase A+B)
Backend: Workspace model, migration (036), schemas, CRUD API endpoints. Adds workspace_id to trees and categories, seeds 4 default workspaces per account, auto-assigns existing trees by tree_type. Frontend: Complete AppLayout rewrite from top-nav to CSS Grid shell with persistent sidebar + topbar. New components: WorkspaceSwitcher, NavItem, CategoryList, TagCloud, TopBar, Sidebar. Dashboard components: QuickStats, FiltersBar, SectionGroup, TreeListItem, SessionsPanel. WorkspaceStore with localStorage persistence. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
frontend/src/components/layout/NavItem.tsx
Normal file
51
frontend/src/components/layout/NavItem.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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[]
|
||||
}
|
||||
|
||||
export function NavItem({ href, icon: Icon, label, badge, matchPaths }: NavItemProps) {
|
||||
const location = useLocation()
|
||||
const isActive = matchPaths
|
||||
? matchPaths.some(p => location.pathname.startsWith(p))
|
||||
: href === '/'
|
||||
? location.pathname === '/'
|
||||
: location.pathname.startsWith(href)
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user