Skip to content

Commit 54bd54a

Browse files
authored
Merge pull request #22 from Keon-Systems/feat/layout-shell
feat(layout): shell + sidebar + topbar + provider updates
2 parents 3382547 + 5137884 commit 54bd54a

File tree

1 file changed

+74
-136
lines changed

1 file changed

+74
-136
lines changed

src/components/layout/sidebar.tsx

Lines changed: 74 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,21 @@
33
import { cn } from "@/lib/utils";
44
import {
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";
2822
import Link from "next/link";
2923
import { 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

9576
export 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

Comments
 (0)