onNavigate(item.path)}
- role="button"
- tabIndex={0}
- onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') onNavigate(item.path) }}
- >
- {item.icon}
-
- {item.label}
- {item.parentApp && (
- {item.parentApp}
- )}
-
-
+
{ if (e.key === 'Enter' || e.key === ' ') onClick() } : undefined}
+ >
+ {logo}
+ {!collapsed && (
+
+ {title}
+ {version && {version}}
- ))}
+ )}
)
}
-// ── Sidebar ──────────────────────────────────────────────────────────────────
+function SidebarSection({
+ icon,
+ label,
+ open,
+ onToggle,
+ active,
+ children,
+ className,
+}: SidebarSectionProps) {
+ const { collapsed, onCollapseToggle } = useSidebarContext()
-export function Sidebar({ apps, className, onNavigate }: SidebarProps) {
- const [search, setSearch] = useState('')
- const [appsCollapsed, _setAppsCollapsed] = useState(() => localStorage.getItem('cameleer:sidebar:apps-collapsed') === 'true')
- const [agentsCollapsed, _setAgentsCollapsed] = useState(() => localStorage.getItem('cameleer:sidebar:agents-collapsed') === 'true')
- const [routesCollapsed, _setRoutesCollapsed] = useState(() => localStorage.getItem('cameleer:sidebar:routes-collapsed') === 'true')
-
- const setAppsCollapsed = (updater: (v: boolean) => boolean) => {
- _setAppsCollapsed((prev) => {
- const next = updater(prev)
- localStorage.setItem('cameleer:sidebar:apps-collapsed', String(next))
- return next
- })
- }
-
- const setAgentsCollapsed = (updater: (v: boolean) => boolean) => {
- _setAgentsCollapsed((prev) => {
- const next = updater(prev)
- localStorage.setItem('cameleer:sidebar:agents-collapsed', String(next))
- return next
- })
- }
-
- const setRoutesCollapsed = (updater: (v: boolean) => boolean) => {
- _setRoutesCollapsed((prev) => {
- const next = updater(prev)
- localStorage.setItem('cameleer:sidebar:routes-collapsed', String(next))
- return next
- })
- }
- const routerNavigate = useNavigate()
- const nav = onNavigate ?? routerNavigate
- const location = useLocation()
- const { starredIds, isStarred, toggleStar } = useStarred()
-
- // Build tree data
- const appNodes = useMemo(() => buildAppTreeNodes(apps), [apps])
- const agentNodes = useMemo(() => buildAgentTreeNodes(apps), [apps])
- const routeNodes = useMemo(() => buildRouteTreeNodes(apps), [apps])
-
- // Sidebar reveal from Cmd-K navigation (passed via location state)
- const sidebarRevealPath = (location.state as { sidebarReveal?: string } | null)?.sidebarReveal ?? null
-
- useEffect(() => {
- if (!sidebarRevealPath) return
-
- // Uncollapse Applications section if reveal path matches an apps tree node
- const matchesAppTree = appNodes.some((node) =>
- node.path === sidebarRevealPath || node.children?.some((child) => child.path === sidebarRevealPath),
+ // In icon-rail (collapsed) mode, render a centered icon with tooltip
+ if (collapsed) {
+ return (
+
{
+ // Expand sidebar and open the section
+ onCollapseToggle?.()
+ onToggle()
+ }}
+ role="button"
+ tabIndex={0}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ onCollapseToggle?.()
+ onToggle()
+ }
+ }}
+ >
+ {icon}
+
)
- if (matchesAppTree && appsCollapsed) {
- _setAppsCollapsed(false)
- localStorage.setItem('cameleer:sidebar:apps-collapsed', 'false')
- }
-
- // Uncollapse Agents section if reveal path matches an agents tree node
- const matchesAgentTree = agentNodes.some((node) =>
- node.path === sidebarRevealPath || node.children?.some((child) => child.path === sidebarRevealPath),
- )
- if (matchesAgentTree && agentsCollapsed) {
- _setAgentsCollapsed(false)
- localStorage.setItem('cameleer:sidebar:agents-collapsed', 'false')
- }
- }, [sidebarRevealPath]) // eslint-disable-line react-hooks/exhaustive-deps
-
- // Build starred items
- const starredItems = useMemo(
- () => collectStarredItems(apps, starredIds),
- [apps, starredIds],
- )
-
- const starredApps = starredItems.filter((i) => i.type === 'application')
- const starredRoutes = starredItems.filter((i) => i.type === 'route')
- const starredAgents = starredItems.filter((i) => i.type === 'agent')
- const starredRouteStats = starredItems.filter((i) => i.type === 'routestat')
- const hasStarred = starredItems.length > 0
-
- // When a sidebar reveal path is provided (e.g. via Cmd-K navigation),
- // use it for sidebar selection so the correct item is highlighted
- const effectiveSelectedPath = sidebarRevealPath ?? location.pathname
+ }
return (
-
-
+
+ {/* Detail panel (portals itself) */}
+ {selectedInstance && (
+