11import { useEffect , useState } from 'react' ;
22import { Link , useLocation } from 'react-router-dom' ;
3+ import { useTranslation } from 'react-i18next' ;
34import { Home , FileText , BarChart3 , Network , Settings , ChevronLeft , ChevronRight , BookOpen , X } from 'lucide-react' ;
45import { cn } from '../lib/utils' ;
56import { ProjectSwitcher } from './ProjectSwitcher' ;
67
7- const STORAGE_KEY = 'ui-vite- main-sidebar-collapsed' ;
8+ const STORAGE_KEY = 'main-sidebar-collapsed' ;
89
9- interface NavItem {
10- path : string ;
11- label : string ;
10+ interface SidebarLinkProps {
11+ to : string ;
1212 icon : typeof Home ;
13+ label : string ;
14+ description ?: string ;
15+ currentPath : string ;
16+ isCollapsed : boolean ;
1317}
1418
15- const navItems : NavItem [ ] = [
16- { path : '/' , label : 'Dashboard' , icon : Home } ,
17- { path : '/specs' , label : 'All Specifications' , icon : FileText } ,
18- { path : '/dependencies' , label : 'Dependency Graph' , icon : Network } ,
19- { path : '/stats' , label : 'Analytics' , icon : BarChart3 } ,
20- { path : '/context' , label : 'Project Context' , icon : BookOpen } ,
21- { path : '/settings' , label : 'Settings' , icon : Settings } ,
22- ] ;
19+ function SidebarLink ( { to, icon : Icon , label, description, currentPath, isCollapsed } : SidebarLinkProps ) {
20+ const normalize = ( value : string ) => value . replace ( / \/ $ / , '' ) || '/' ;
21+ const normalizedTo = normalize ( to ) ;
22+ const normalizedPath = normalize ( currentPath ) ;
23+ const isHome = normalizedTo === '/' ;
24+ const isActive = isHome ? normalizedPath === '/' : normalizedPath . startsWith ( normalizedTo ) ;
25+
26+ return (
27+ < Link
28+ to = { to }
29+ className = { cn (
30+ 'flex items-center gap-3 rounded-lg px-3 py-2 transition-colors' ,
31+ 'hover:bg-accent hover:text-accent-foreground' ,
32+ isActive && 'bg-accent text-accent-foreground font-medium' ,
33+ isCollapsed && 'justify-center px-2'
34+ ) }
35+ >
36+ < Icon className = { cn ( 'h-5 w-5 shrink-0' , isActive && 'text-primary' ) } />
37+ { ! isCollapsed && (
38+ < div className = "flex flex-col" >
39+ < span className = "text-sm" > { label } </ span >
40+ { description && < span className = "text-xs text-muted-foreground" > { description } </ span > }
41+ </ div >
42+ ) }
43+ </ Link >
44+ ) ;
45+ }
2346
2447interface MainSidebarProps {
2548 mobileOpen ?: boolean ;
@@ -28,97 +51,97 @@ interface MainSidebarProps {
2851
2952export function MainSidebar ( { mobileOpen = false , onMobileClose } : MainSidebarProps ) {
3053 const location = useLocation ( ) ;
31- const [ collapsed , setCollapsed ] = useState ( false ) ;
32-
33- useEffect ( ( ) => {
54+ const { t } = useTranslation ( 'common' ) ;
55+ const [ collapsed , setCollapsed ] = useState ( ( ) => {
56+ if ( typeof window === 'undefined' ) return false ;
3457 const stored = localStorage . getItem ( STORAGE_KEY ) ;
35- setCollapsed ( stored === 'true' ) ;
36- } , [ ] ) ;
58+ return stored === 'true' ;
59+ } ) ;
3760
3861 useEffect ( ( ) => {
39- document . documentElement . style . setProperty ( '--main-sidebar-width' , collapsed ? '64px' : '240px' ) ;
40- localStorage . setItem ( STORAGE_KEY , String ( collapsed ) ) ;
62+ document . documentElement . style . setProperty ( '--main-sidebar-width' , collapsed ? '60px' : '240px' ) ;
63+ if ( typeof window !== 'undefined' ) {
64+ localStorage . setItem ( STORAGE_KEY , String ( collapsed ) ) ;
65+ }
4166 } , [ collapsed ] ) ;
4267
4368 // Close mobile sidebar when route changes
4469 useEffect ( ( ) => {
4570 if ( mobileOpen && onMobileClose ) {
4671 onMobileClose ( ) ;
4772 }
48- } , [ location . pathname ] ) ;
73+ } , [ location . pathname , mobileOpen , onMobileClose ] ) ;
74+
75+ const navItems = [
76+ { path : '/' , label : t ( 'navigation.home' ) , description : t ( 'navigation.dashboard' ) , icon : Home } ,
77+ { path : '/specs' , label : t ( 'navigation.specs' ) , description : t ( 'navigation.allSpecifications' ) , icon : FileText } ,
78+ { path : '/dependencies' , label : t ( 'navigation.dependencies' ) , description : t ( 'navigation.dependencyGraph' ) , icon : Network } ,
79+ { path : '/stats' , label : t ( 'navigation.stats' ) , description : t ( 'navigation.analytics' ) , icon : BarChart3 } ,
80+ { path : '/context' , label : t ( 'navigation.context' ) , description : t ( 'navigation.projectContext' ) , icon : BookOpen } ,
81+ { path : '/settings' , label : t ( 'navigation.settings' ) , description : t ( 'navigation.settingsDescription' ) , icon : Settings } ,
82+ ] ;
4983
5084 return (
5185 < >
52- { /* Mobile overlay */ }
86+ { /* Mobile overlay backdrop */ }
5387 { mobileOpen && (
5488 < div
5589 className = "fixed inset-0 bg-black/50 z-40 lg:hidden"
5690 onClick = { onMobileClose }
5791 />
5892 ) }
5993
60- { /* Sidebar */ }
6194 < aside
6295 className = { cn (
63- 'flex flex-col border-r bg-background h-[calc(100vh-3.5rem)] transition-all duration-300' ,
64- // Desktop styles
65- 'hidden lg:flex sticky top-14' ,
66- collapsed ? 'w-16' : 'w-60' ,
67- // Mobile styles
68- 'lg:hidden fixed left-0 top-14 z-50 w-60' ,
69- mobileOpen ? 'translate-x-0' : '-translate-x-full' ,
70- // Show on desktop OR when mobile menu is open
71- 'lg:translate-x-0 lg:flex'
96+ 'border-r bg-background transition-all duration-300 flex-shrink-0' ,
97+ // Desktop behavior
98+ "hidden lg:flex lg:sticky lg:top-14 lg:h-[calc(100vh-3.5rem)]" ,
99+ collapsed ? "lg:w-[60px]" : "lg:w-[240px]" ,
100+ // Mobile behavior - show as overlay when open
101+ mobileOpen && "fixed inset-y-0 left-0 z-[60] flex w-[280px]"
72102 ) }
73103 >
74- { /* Mobile close button */ }
75- < div className = "lg:hidden flex justify-end p-2 border-b" >
76- < button
77- onClick = { onMobileClose }
78- className = "p-2 hover:bg-secondary rounded-md transition-colors"
79- aria-label = "Close menu"
80- >
81- < X className = "h-5 w-5" />
82- </ button >
83- </ div >
84-
85- < div className = "px-3 py-4" >
86- < ProjectSwitcher />
87- </ div >
88-
89- < nav className = "flex-1 px-2 space-y-1" >
90- { navItems . map ( ( item ) => {
91- const Icon = item . icon ;
92- const isActive = item . path === '/'
93- ? location . pathname === '/'
94- : location . pathname . startsWith ( item . path ) ;
104+ < div className = "flex flex-col h-full w-full" >
105+ { /* Mobile close button */ }
106+ < div className = "lg:hidden flex justify-end p-2 border-b" >
107+ < button
108+ onClick = { onMobileClose }
109+ className = "p-2 hover:bg-secondary rounded-md transition-colors"
110+ aria-label = { t ( 'navigation.closeMenu' ) }
111+ >
112+ < X className = "h-5 w-5" />
113+ </ button >
114+ </ div >
95115
96- return (
97- < Link
116+ < nav className = "flex-1 px-2 py-2 space-y-1" >
117+ < div className = "mb-4 flex items-center justify-center" >
118+ < ProjectSwitcher collapsed = { collapsed && ! mobileOpen } />
119+ </ div >
120+ { navItems . map ( ( item ) => (
121+ < SidebarLink
98122 key = { item . path }
99123 to = { item . path }
100- className = { cn (
101- 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors' ,
102- 'hover:bg-accent hover:text-accent-foreground' ,
103- isActive && 'bg-accent text-accent-foreground font-medium' ,
104- collapsed && 'justify-center px-2'
105- ) }
106- >
107- < Icon className = { cn ( 'h-5 w-5 shrink-0' , isActive && 'text-primary' ) } />
108- { ! collapsed && < span className = "truncate" > { item . label } </ span > }
109- </ Link >
110- ) ;
111- } ) }
112- </ nav >
124+ icon = { item . icon }
125+ label = { item . label }
126+ description = { ! collapsed || mobileOpen ? item . description : undefined }
127+ currentPath = { location . pathname }
128+ isCollapsed = { collapsed && ! mobileOpen }
129+ />
130+ ) ) }
131+ </ nav >
113132
114- < div className = "p-2 border-t hidden lg:block" >
115- < button
116- onClick = { ( ) => setCollapsed ( ( prev ) => ! prev ) }
117- className = { cn ( 'w-full flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-secondary transition-colors' , collapsed && 'px-2' ) }
118- >
119- { collapsed ? < ChevronRight className = "h-4 w-4" /> : < ChevronLeft className = "h-4 w-4" /> }
120- { ! collapsed && < span className = "text-xs" > Collapse</ span > }
121- </ button >
133+ < div className = "hidden lg:block p-2 border-t" >
134+ < button
135+ onClick = { ( ) => setCollapsed ( ( prev ) => ! prev ) }
136+ className = { cn (
137+ 'w-full flex items-center justify-center gap-2 rounded-md px-3 py-2 text-sm hover:bg-secondary transition-colors' ,
138+ collapsed && 'px-2'
139+ ) }
140+ >
141+ { collapsed ? < ChevronRight className = "h-4 w-4" /> : < ChevronLeft className = "h-4 w-4" /> }
142+ { ! collapsed && < span className = "text-xs" > { t ( 'navigation.collapse' ) } </ span > }
143+ </ button >
144+ </ div >
122145 </ div >
123146 </ aside >
124147 </ >
0 commit comments