33import { cn } from "@/lib/utils" ;
44import {
55 Activity ,
6- Archive ,
76 BookOpen ,
8- CheckCircle ,
97 ChevronLeft ,
8+ Gavel ,
9+ Home ,
1010 Cpu ,
1111 CreditCard ,
1212 FileCheck2 ,
13- GitBranch ,
14- Gavel ,
13+ GitBranch ,
1514 KeyRound ,
16- LayoutDashboard ,
17- Link2 ,
1815 MessageSquare ,
19- PlaySquare ,
2016 Scale ,
2117 Settings ,
22- Shield ,
23- Sparkles ,
18+ ShieldCheck ,
2419 Users ,
2520 Waves ,
26- Zap
2721} from "lucide-react" ;
2822import Link from "next/link" ;
2923import { usePathname } from "next/navigation" ;
@@ -41,79 +35,62 @@ interface NavItem {
4135 icon : React . ComponentType < { className ?: string } > ;
4236}
4337
44- interface NavGroup {
45- label : string ;
38+ interface NavSection {
39+ title ? : string ;
4640 items : NavItem [ ] ;
4741}
4842
49- const navGroups : NavGroup [ ] = [
43+ const navSections : NavSection [ ] = [
44+ {
45+ title : "Core" ,
46+ items : [
47+ { label : "Control" , href : "/control" , icon : Home } ,
48+ { label : "Receipts" , href : "/receipts" , icon : KeyRound } ,
49+ { label : "Policies" , href : "/policies" , icon : ShieldCheck } ,
50+ { label : "Tenants" , href : "/tenants" , icon : Users } ,
51+ { label : "Integrations" , href : "/integrations" , icon : Waves } ,
52+ { label : "Collective" , href : "/collective" , icon : BookOpen } ,
53+ { label : "System State" , href : "/cockpit" , icon : Activity } ,
54+ ] ,
55+ } ,
5056 {
51- label : "CONTROL PLANE " ,
57+ title : "Operator " ,
5258 items : [
53- { label : "Overview" , href : "/" , icon : LayoutDashboard } ,
54- { label : "Get Started" , href : "/get-started" , icon : Sparkles } ,
5559 { label : "Usage" , href : "/usage" , icon : Waves } ,
5660 { label : "API Keys" , href : "/api-keys" , icon : KeyRound } ,
57- { label : "Subscription" , href : "/admin/subscription" , icon : CreditCard } ,
58- { label : "Tenant" , href : "/tenants" , icon : Users } ,
5961 { label : "Settings" , href : "/settings" , icon : Settings } ,
6062 ] ,
6163 } ,
6264 {
63- label : "GOVERNANCE " ,
65+ title : "Collective Detail " ,
6466 items : [
65- { label : "Overview" , href : "/collective" , icon : BookOpen } ,
66- { label : "Submit Run" , href : "/collective/submit" , icon : PlaySquare } ,
67- { label : "Recent Runs" , href : "/collective/runs" , icon : BookOpen } ,
6867 { label : "Deliberations" , href : "/collective/deliberations" , icon : MessageSquare } ,
6968 { label : "Reforms" , href : "/collective/reforms" , icon : Gavel } ,
7069 { label : "Legitimacy" , href : "/collective/legitimacy" , icon : Scale } ,
71- { label : "Pulse" , href : "/collective" , icon : Activity } ,
72- { label : "Decisions" , href : "/collective/decisions" , icon : Scale } ,
73- { label : "Executions" , href : "/collective/executions" , icon : Cpu } ,
74- { label : "Evidence" , href : "/collective/evidence" , icon : FileCheck2 } ,
75- { label : "Policies" , href : "/collective/policies" , icon : BookOpen } ,
76- { label : "Receipts" , href : "/collective/receipts" , icon : Link2 } ,
77- { label : "Correlation" , href : "/collective/correlation" , icon : GitBranch } ,
78- { label : "Compliance" , href : "/collective/compliance" , icon : Shield } ,
79- ] ,
80- } ,
81- {
82- label : "AUTHORITY" ,
83- items : [
84- { label : "Delegations" , href : "/collective/authority/delegations" , icon : Shield } ,
85- { label : "Permissions" , href : "/collective/authority/permissions" , icon : KeyRound } ,
86- { label : "Activations" , href : "/collective/authority/activations" , icon : Zap } ,
87- { label : "Prepared Effects" , href : "/collective/effects/prepared" , icon : Archive } ,
88- { label : "Reforms" , href : "/collective/reforms/adoption" , icon : CheckCircle } ,
8970 ] ,
9071 } ,
9172] ;
9273
93- const allItems = navGroups . flatMap ( ( g ) => g . items ) ;
74+ const navItems : NavItem [ ] = navSections . flatMap ( ( section ) => section . items ) ;
9475
9576export function Sidebar ( { collapsed = false , onCollapse, className } : SidebarProps ) {
9677 const pathname = usePathname ( ) ;
9778 const [ selectedIndex , setSelectedIndex ] = React . useState ( 0 ) ;
9879
99- // Keyboard navigation (J/K keys)
10080 React . useEffect ( ( ) => {
10181 const handleKeyDown = ( e : KeyboardEvent ) => {
102- if (
103- document . activeElement ?. tagName === "INPUT" ||
104- document . activeElement ?. tagName === "TEXTAREA"
105- ) {
82+ if ( document . activeElement ?. tagName === "INPUT" || document . activeElement ?. tagName === "TEXTAREA" ) {
10683 return ;
10784 }
10885
10986 if ( e . key === "j" ) {
11087 e . preventDefault ( ) ;
111- setSelectedIndex ( ( prev ) => ( prev + 1 ) % allItems . length ) ;
88+ setSelectedIndex ( ( prev ) => ( prev + 1 ) % navItems . length ) ;
11289 } else if ( e . key === "k" ) {
11390 e . preventDefault ( ) ;
114- setSelectedIndex ( ( prev ) => ( prev - 1 + allItems . length ) % allItems . length ) ;
91+ setSelectedIndex ( ( prev ) => ( prev - 1 + navItems . length ) % navItems . length ) ;
11592 } else if ( e . key === "Enter" && selectedIndex >= 0 ) {
116- const item = allItems [ selectedIndex ] ;
93+ const item = navItems [ selectedIndex ] ;
11794 if ( item ) {
11895 window . location . href = item . href ;
11996 }
@@ -124,27 +101,19 @@ export function Sidebar({ collapsed = false, onCollapse, className }: SidebarPro
124101 return ( ) => document . removeEventListener ( "keydown" , handleKeyDown ) ;
125102 } , [ selectedIndex ] ) ;
126103
127- // Update selected index based on pathname
128104 React . useEffect ( ( ) => {
129- const index = allItems . findIndex ( ( item ) =>
130- item . href === "/"
131- ? pathname === "/"
132- : pathname === item . href || pathname . startsWith ( item . href + "/" )
133- ) ;
105+ const index = navItems . findIndex ( ( item ) => item . href === pathname ) ;
134106 if ( index !== - 1 ) {
135107 setSelectedIndex ( index ) ;
136108 }
137109 } , [ pathname ] ) ;
138110
139- // Save collapsed state to localStorage
140111 React . useEffect ( ( ) => {
141112 if ( typeof window !== "undefined" ) {
142113 localStorage . setItem ( "sidebar-collapsed" , JSON . stringify ( collapsed ) ) ;
143114 }
144115 } , [ collapsed ] ) ;
145116
146- let flatIndex = 0 ;
147-
148117 return (
149118 < aside
150119 className = { cn (
@@ -153,77 +122,55 @@ export function Sidebar({ collapsed = false, onCollapse, className }: SidebarPro
153122 className
154123 ) }
155124 >
156- { /* Navigation Items */ }
157125 < nav className = "flex-1 space-y-1 overflow-y-auto p-3" >
158- { navGroups . map ( ( group , groupIdx ) => (
159- < div key = { group . label } >
160- { /* Group divider */ }
161- { groupIdx > 0 && (
162- < div className = "my-3 border-t border-[--tungsten]" />
163- ) }
164- { ! collapsed && (
165- < div className = "mb-2 px-3 pt-1" >
166- < span className = "font-mono text-[10px] uppercase tracking-widest text-[--tungsten]" >
167- { group . label }
168- </ span >
169- </ div >
170- ) }
171-
172- { /* Group items */ }
173- { group . items . map ( ( item ) => {
174- const Icon = item . icon ;
175- const isActive =
176- item . href === "/"
177- ? pathname === "/"
178- : pathname === item . href || pathname . startsWith ( item . href + "/" ) ;
179- const currentFlatIndex = flatIndex ;
180- const isSelected = currentFlatIndex === selectedIndex ;
181- flatIndex ++ ;
182-
183- return (
184- < Link
185- key = { item . href }
186- href = { item . href }
187- className = { cn (
188- "group relative flex items-center gap-3 rounded px-3 py-2.5 transition-all" ,
189- "text-[#C5C6C7] hover:bg-[#384656] hover:text-[#66FCF1]" ,
190- isActive && "border-l-2 border-[#66FCF1] bg-[#384656] text-[#66FCF1]" ,
191- isSelected && ! isActive && "ring-1 ring-[#66FCF1] ring-opacity-50"
192- ) }
193- onMouseEnter = { ( ) => setSelectedIndex ( currentFlatIndex ) }
194- >
195- { /* Active indicator glow */ }
196- { isActive && (
197- < div className = "absolute inset-0 rounded bg-[#66FCF1] opacity-5" > </ div >
198- ) }
199-
200- { /* Icon */ }
201- < Icon
126+ { navSections . map ( ( section , sectionIdx ) => {
127+ let globalOffset = 0 ;
128+ for ( let i = 0 ; i < sectionIdx ; i += 1 ) {
129+ globalOffset += navSections [ i ] . items . length ;
130+ }
131+
132+ return (
133+ < div key = { section . title ?? sectionIdx } className = { cn ( sectionIdx > 0 && "mt-4" ) } >
134+ { section . title && ! collapsed && (
135+ < div className = "mb-2 px-3 font-mono text-[10px] uppercase tracking-widest text-[#66FCF1] opacity-60" >
136+ { section . title }
137+ </ div >
138+ ) }
139+ { sectionIdx > 0 && < div className = "mb-2 border-t border-[#384656]" /> }
140+ { section . items . map ( ( item , itemIdx ) => {
141+ const globalIndex = globalOffset + itemIdx ;
142+ const Icon = item . icon ;
143+ const isActive = pathname === item . href ;
144+ const isSelected = globalIndex === selectedIndex ;
145+
146+ return (
147+ < Link
148+ key = { item . href }
149+ href = { item . href }
202150 className = { cn (
203- "h-5 w-5 shrink-0 transition-colors" ,
204- isActive && "text-[#66FCF1]"
151+ "group relative flex items-center gap-3 rounded px-3 py-2.5 transition-all" ,
152+ "text-[#C5C6C7] hover:bg-[#384656] hover:text-[#66FCF1]" ,
153+ isActive && "border-l-2 border-[#66FCF1] bg-[#384656] text-[#66FCF1]" ,
154+ isSelected && ! isActive && "ring-1 ring-[#66FCF1] ring-opacity-50"
205155 ) }
206- />
207-
208- { /* Label */ }
209- { ! collapsed && (
210- < span className = "font-mono text-sm font-medium" > { item . label } </ span >
211- ) }
212-
213- { /* Tooltip for collapsed state */ }
214- { collapsed && (
215- < div className = "absolute left-full top-1/2 ml-2 hidden -translate-y-1/2 whitespace-nowrap rounded border border-[#384656] bg-[#1F2833] px-3 py-2 font-mono text-sm text-[#C5C6C7] group-hover:block" >
216- { item . label }
217- </ div >
218- ) }
219- </ Link >
220- ) ;
221- } ) }
222- </ div >
223- ) ) }
156+ onMouseEnter = { ( ) => setSelectedIndex ( globalIndex ) }
157+ >
158+ { isActive && < div className = "absolute inset-0 rounded bg-[#66FCF1] opacity-5" /> }
159+ < Icon className = { cn ( "h-5 w-5 shrink-0 transition-colors" , isActive && "text-[#66FCF1]" ) } />
160+ { ! collapsed && < span className = "font-mono text-sm font-medium" > { item . label } </ span > }
161+ { collapsed && (
162+ < div className = "absolute left-full top-1/2 ml-2 hidden -translate-y-1/2 whitespace-nowrap rounded border border-[#384656] bg-[#1F2833] px-3 py-2 font-mono text-sm text-[#C5C6C7] group-hover:block" >
163+ { section . title ? `${ section . title } : ${ item . label } ` : item . label }
164+ </ div >
165+ ) }
166+ </ Link >
167+ ) ;
168+ } ) }
169+ </ div >
170+ ) ;
171+ } ) }
224172 </ nav >
225173
226- { /* Collapse Toggle */ }
227174 < div className = "border-t border-[#384656] p-3" >
228175 < button
229176 onClick = { ( ) => onCollapse ?.( ! collapsed ) }
@@ -233,25 +180,16 @@ export function Sidebar({ collapsed = false, onCollapse, className }: SidebarPro
233180 ) }
234181 aria-label = { collapsed ? "Expand sidebar" : "Collapse sidebar" }
235182 >
236- < ChevronLeft
237- className = { cn (
238- "h-5 w-5 transition-transform" ,
239- collapsed && "rotate-180"
240- ) }
241- />
183+ < ChevronLeft className = { cn ( "h-5 w-5 transition-transform" , collapsed && "rotate-180" ) } />
242184 { ! collapsed && < span className = "font-mono text-sm" > Collapse</ span > }
243185 </ button >
244186 </ div >
245187
246- { /* Keyboard hint */ }
247188 { ! collapsed && (
248189 < div className = "border-t border-[#384656] p-3" >
249190 < div className = "rounded bg-[#0B0C10] p-2 text-center" >
250- < p className = "font-mono text-xs text-[#C5C6C7] opacity-50" >
251- Press{ " " }
252- < kbd className = "rounded bg-[#384656] px-1.5 py-0.5 text-[#66FCF1]" > J</ kbd > /{ " " }
253- < kbd className = "rounded bg-[#384656] px-1.5 py-0.5 text-[#66FCF1]" > K</ kbd > { " " }
254- to navigate
191+ < p className = "font-mono text-[11px] text-[#C5C6C7] opacity-40" >
192+ Guided setup lives outside the control plane and returns here only after activation is complete.
255193 </ p >
256194 </ div >
257195 </ div >
0 commit comments