{label}
{items.map((item) => (
onNavigate(item.path)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onNavigate(item.path)
}}
>
{item.icon && (
{item.icon}
)}
{item.label}
{item.parentApp && (
{item.parentApp}
)}
))}
)
}
// ── localStorage-backed section collapse ────────────────────────────────────
function usePersistedCollapse(key: string, defaultValue: boolean): [boolean, () => void] {
const [value, setValue] = useState(() => {
try {
const raw = localStorage.getItem(key)
if (raw !== null) return raw === 'true'
} catch { /* ignore */ }
return defaultValue
})
const toggle = () => {
setValue((prev) => {
const next = !prev
try {
localStorage.setItem(key, String(next))
} catch { /* ignore */ }
return next
})
}
return [value, toggle]
}
// ── LayoutShell ─────────────────────────────────────────────────────────────
export function LayoutShell() {
const navigate = useNavigate()
const location = useLocation()
const { starredIds, isStarred, toggleStar } = useStarred()
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [filterQuery, setFilterQuery] = useState('')
// Section collapse state — persisted to localStorage
const [appsCollapsed, toggleAppsCollapsed] = usePersistedCollapse('cameleer:sidebar:apps-collapsed', false)
const [agentsCollapsed, toggleAgentsCollapsed] = usePersistedCollapse('cameleer:sidebar:agents-collapsed', false)
const [routesCollapsed, toggleRoutesCollapsed] = usePersistedCollapse('cameleer:sidebar:routes-collapsed', false)
// Tree data — static, so empty deps
const appNodes = useMemo(() => buildAppTreeNodes(SIDEBAR_APPS), [])
const agentNodes = useMemo(() => buildAgentTreeNodes(SIDEBAR_APPS), [])
const routeNodes = useMemo(() => buildRouteTreeNodes(SIDEBAR_APPS), [])
// Sidebar reveal from Cmd-K navigation
const sidebarRevealPath = (location.state as { sidebarReveal?: string } | null)?.sidebarReveal ?? null
// Auto-uncollapse matching sections when sidebarRevealPath changes
useEffect(() => {
if (!sidebarRevealPath) return
if (sidebarRevealPath.startsWith('/apps') && appsCollapsed) {
toggleAppsCollapsed()
}
if (sidebarRevealPath.startsWith('/agents') && agentsCollapsed) {
toggleAgentsCollapsed()
}
if (sidebarRevealPath.startsWith('/routes') && routesCollapsed) {
toggleRoutesCollapsed()
}
}, [sidebarRevealPath]) // eslint-disable-line react-hooks/exhaustive-deps
const effectiveSelectedPath = sidebarRevealPath ?? location.pathname
// Starred items — collected and grouped
const allStarred = useMemo(
() => collectStarredItems(SIDEBAR_APPS, starredIds),
[starredIds],
)
const starredApps = allStarred.filter((s) => s.type === 'application')
const starredRoutes = allStarred.filter((s) => s.type === 'route')
const starredAgents = allStarred.filter((s) => s.type === 'agent')
const starredRouteStats = allStarred.filter((s) => s.type === 'routestat')
const hasStarred = allStarred.length > 0
const camelLogo = (