1+ import { useMemo } from "react" ;
12import { NavLink } from "react-router-dom" ;
3+ import { useContainers } from "../../hooks/useQueries" ;
4+ import { useAppStore } from "../../store/useAppStore" ;
25
36/* ── SVG Icons (Lucide-sourced, inline for zero dependency) ──────────────── */
47
@@ -63,22 +66,54 @@ const SettingsIcon = () => (
6366
6467/* ── Navigation Definition ───────────────────────────────────────────────── */
6568
66- const NAV = [
67- { to : "/" , label : "Dashboard" , icon : DashboardIcon , shortcut : "⌘1" } ,
68- { to : "/environments" , label : "Environments" , icon : EnvIcon , shortcut : "⌘3" } ,
69- { to : "/images" , label : "Images" , icon : ImagesIcon , shortcut : "⌘6" } ,
70- { to : "/logs" , label : "Logs" , icon : LogsIcon , shortcut : "⌘2" } ,
71- { to : "/network" , label : "Network" , icon : NetworkIcon , shortcut : "⌘4" } ,
72- { to : "/volumes" , label : "Volumes" , icon : VolumesIcon , shortcut : "⌘7" } ,
73- { to : "/settings" , label : "Settings" , icon : SettingsIcon , shortcut : "⌘5" } ,
69+ const SECTIONS = [
70+ {
71+ title : "Workspace" ,
72+ items : [
73+ { to : "/" , label : "Dashboard" , icon : DashboardIcon } ,
74+ { to : "/logs" , label : "Logs" , icon : LogsIcon } ,
75+ ] ,
76+ } ,
77+ {
78+ title : "Development" ,
79+ items : [
80+ { to : "/environments" , label : "Environments" , icon : EnvIcon } ,
81+ ] ,
82+ } ,
83+ {
84+ title : "Infrastructure" ,
85+ items : [
86+ { to : "/images" , label : "Images" , icon : ImagesIcon } ,
87+ { to : "/volumes" , label : "Volumes" , icon : VolumesIcon } ,
88+ { to : "/network" , label : "Network" , icon : NetworkIcon } ,
89+ ] ,
90+ } ,
91+ {
92+ title : "System" ,
93+ items : [
94+ { to : "/settings" , label : "Settings" , icon : SettingsIcon } ,
95+ ] ,
96+ } ,
7497] ;
7598
7699/* ── Component ───────────────────────────────────────────────────────────── */
77100
78101export function SideNav ( ) {
102+ const { data : containers } = useContainers ( ) ;
103+ const { activeProject, setActiveProject } = useAppStore ( ) ;
104+
105+ const projects = useMemo ( ( ) => {
106+ if ( ! containers ) return [ ] ;
107+ const set = new Set < string > ( ) ;
108+ containers . forEach ( ( c ) => {
109+ if ( c . compose_project ) set . add ( c . compose_project ) ;
110+ } ) ;
111+ return Array . from ( set ) . sort ( ) ;
112+ } , [ containers ] ) ;
113+
79114 return (
80115 < nav
81- className = "w-[210px] bg-bg-deep border-r border-border flex flex-col shrink-0"
116+ className = "w-[210px] bg-bg-deep border-r border-border flex flex-col shrink-0 select-none "
82117 aria-label = "Main navigation"
83118 >
84119 { /* Logo / Wordmark */ }
@@ -88,56 +123,86 @@ export function SideNav() {
88123 style = { { boxShadow : "0 0 8px var(--green)" } }
89124 aria-hidden = "true"
90125 />
91- < span
92- className = "font-mono text-[14px] font-bold text-green tracking-tight select-none"
93- >
126+ < span className = "font-mono text-[14px] font-bold text-green tracking-tight select-none" >
94127 DevOpsLocal
95128 </ span >
96129 </ div >
97130
98- { /* Nav Items */ }
99- < div className = "flex flex-col py-2 flex-1" >
100- { NAV . map ( ( { to, label, icon : Icon } ) => (
101- < NavLink
102- key = { to }
103- to = { to }
104- end = { to === "/" }
105- aria-current = { undefined /* set by NavLink's isActive */ }
106- className = { ( { isActive } : { isActive : boolean } ) =>
107- [
108- "group flex items-center gap-2.5 px-3.5 py-2 mx-2 my-0.5 rounded-lg text-[13px] font-medium transition-all duration-150 no-underline relative" ,
109- isActive
110- ? "text-green bg-green/10 shadow-[inset_0_0_0_1px_rgba(var(--color-green)/0.2)]"
111- : "text-text-3 hover:text-text-2 hover:bg-surface-2" ,
112- ] . join ( " " )
113- }
114- >
115- { ( { isActive } : { isActive : boolean } ) => (
116- < >
117- { /* Active left accent bar */ }
118- { isActive && (
119- < span
120- className = "absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 rounded-r bg-green"
121- aria-hidden = "true"
122- />
131+ { /* Dynamic Project Workspace Selector */ }
132+ < div className = "px-3.5 py-3 border-b border-border-light flex flex-col gap-1.5 bg-bg-deep/50" >
133+ < label className = "text-[9px] font-mono font-bold uppercase tracking-wider text-text-3 select-none" >
134+ Project Workspace
135+ </ label >
136+ < select
137+ value = { activeProject }
138+ onChange = { ( e ) => setActiveProject ( e . target . value ) }
139+ className = "w-full bg-surface border border-border rounded px-2.5 py-1.5 text-[11px] text-text font-mono outline-none focus:border-green focus:ring-1 focus:ring-green/30 transition-all cursor-pointer hover:bg-surface-2"
140+ aria-label = "Select active project workspace"
141+ >
142+ < option value = "all" > 🌐 All Projects</ option >
143+ { projects . map ( ( p ) => (
144+ < option key = { p } value = { p } >
145+ 📦 { p }
146+ </ option >
147+ ) ) }
148+ </ select >
149+ </ div >
150+
151+ { /* Nav Items Grouped by Section */ }
152+ < div className = "flex flex-col py-3 flex-1 overflow-y-auto gap-4" >
153+ { SECTIONS . map ( ( section ) => (
154+ < div key = { section . title } className = "flex flex-col gap-0.5" >
155+ < span className = "px-4 text-[9px] font-mono font-bold uppercase tracking-wider text-text-3/65 mb-1 select-none" >
156+ { section . title }
157+ </ span >
158+ { section . items . map ( ( { to, label, icon : Icon } ) => (
159+ < NavLink
160+ key = { to }
161+ to = { to }
162+ end = { to === "/" }
163+ className = { ( { isActive } : { isActive : boolean } ) =>
164+ [
165+ "group flex items-center gap-2.5 px-3.5 py-2 mx-2 my-0.5 rounded-lg text-[13px] font-medium transition-all duration-150 no-underline relative" ,
166+ isActive
167+ ? "text-green bg-green/10 shadow-[inset_0_0_0_1px_rgba(var(--color-green)/0.2)] font-semibold"
168+ : "text-text-3 hover:text-text-2 hover:bg-surface-2" ,
169+ ] . join ( " " )
170+ }
171+ >
172+ { ( { isActive } : { isActive : boolean } ) => (
173+ < >
174+ { /* Active left accent bar */ }
175+ { isActive && (
176+ < span
177+ className = "absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 rounded-r bg-green"
178+ aria-hidden = "true"
179+ />
180+ ) }
181+ < span
182+ className = { `shrink-0 transition-colors ${
183+ isActive ? "text-green" : "text-text-3 group-hover:text-text-2"
184+ } `}
185+ aria-hidden = "true"
186+ >
187+ < Icon />
188+ </ span >
189+ < span className = "truncate" > { label } </ span >
190+ </ >
123191 ) }
124- < span
125- className = { `shrink-0 transition-colors ${ isActive ? "text-green" : "text-text-3 group-hover:text-text-2" } ` }
126- aria-hidden = "true"
127- >
128- < Icon />
129- </ span >
130- < span className = "truncate" > { label } </ span >
131- </ >
132- ) }
133- </ NavLink >
192+ </ NavLink >
193+ ) ) }
194+ </ div >
134195 ) ) }
135196 </ div >
136197
137198 { /* Footer hint */ }
138- < div className = "px-4 py-3 border-t border-border-light" >
199+ < div className = "px-4 py-3 border-t border-border-light bg-bg-deep " >
139200 < p className = "text-[10px] font-mono text-text-3 m-0" >
140- Press < kbd className = "bg-surface-3 border border-border px-1 rounded text-[9px]" > ?</ kbd > for shortcuts
201+ Press{ " " }
202+ < kbd className = "bg-surface-3 border border-border px-1 rounded text-[9px]" >
203+ ?
204+ </ kbd > { " " }
205+ for shortcuts
141206 </ p >
142207 </ div >
143208 </ nav >
0 commit comments