import { useEffect, useState } from "react" import { Link, NavLink, Outlet } from "react-router-dom" import { Boxes, ChevronDown, ChevronLeft, ChevronRight, Clock, Gauge, Globe, KeyRound, LayoutDashboard, Menu, Search, Server, Share2, SquareCode, Wrench, X, type LucideIcon, } from "lucide-react" import { cn } from "@/lib/utils" import { useEventStream } from "@/services/api/hooks" import { AssistantDock } from "@/components/AssistantDock" import { CommandPalette } from "@/components/CommandPalette" import { HostSwitcher } from "@/components/HostSwitcher" type NavLeaf = { to: string; label: string; icon: LucideIcon; end?: boolean } type NavGroup = { label: string; icon: LucideIcon; children: NavLeaf[] } // Services, Scheduled, and Tools are all deployment lenses — grouped under a // "Deployments" parent. Programs (the catalog) stays top-level. const NAV: (NavLeaf | NavGroup)[] = [ { to: "/", label: "Overview", icon: LayoutDashboard, end: true }, { to: "/map", label: "System", icon: Gauge }, { to: "/gateway", label: "Gateway", icon: Globe }, { label: "Deployments", icon: Boxes, children: [ { to: "/services", label: "Services", icon: Server }, { to: "/scheduled", label: "Scheduled", icon: Clock }, { to: "/tools", label: "Tools", icon: Wrench }, ], }, { to: "/programs", label: "Programs", icon: SquareCode }, { to: "/mesh", label: "Mesh", icon: Share2 }, { to: "/secrets", label: "Secrets", icon: KeyRound }, ] const COLLAPSE_KEY = "castle-nav-collapsed" function NavLeafLink({ leaf, collapsed, indent, onNavigate, }: { leaf: NavLeaf collapsed: boolean indent?: boolean onNavigate?: () => void }) { const Icon = leaf.icon return ( cn( "flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors", collapsed && "justify-center px-0", indent && !collapsed && "pl-9", isActive ? "bg-[var(--primary)]/10 text-[var(--foreground)] font-medium" : "text-[var(--muted)] hover:text-[var(--foreground)] hover:bg-[var(--card)]", ) } > {!collapsed && {leaf.label}} ) } function NavGroupItem({ group, collapsed, onNavigate, }: { group: NavGroup collapsed: boolean onNavigate?: () => void }) { const [open, setOpen] = useState(true) // On the icon rail there's no room for a group header — show the children flat. if (collapsed) { return ( <> {group.children.map((c) => ( ))} ) } const Icon = group.icon return (
{open && (
{group.children.map((c) => ( ))}
)}
) } function NavItems({ collapsed, onNavigate }: { collapsed: boolean; onNavigate?: () => void }) { return ( ) } function Brand({ collapsed, onClick }: { collapsed?: boolean; onClick?: () => void }) { return ( {collapsed ? "C" : "Castle"} ) } export function Layout() { useEventStream() const [collapsed, setCollapsed] = useState(() => localStorage.getItem(COLLAPSE_KEY) === "1") const [mobileOpen, setMobileOpen] = useState(false) useEffect(() => { localStorage.setItem(COLLAPSE_KEY, collapsed ? "1" : "0") }, [collapsed]) return (
{/* Mobile top bar */}
{/* Mobile drawer */} {mobileOpen && (
setMobileOpen(false)} />
)} {/* Desktop sidebar */} {/* Main content */}
{/* Global assistant — persists across navigation (Layout doesn't remount). */} {/* App-wide ⌘K launcher / command palette. */}
) }