# Composable Sidebar with Accordion & Collapse **Issue:** [#112](https://gitea.siegeln.net/cameleer/cameleer-server/issues/112) **Date:** 2026-04-02 **Scope:** Design system refactor + server UI migration ## Problem The current `Sidebar` component in `@cameleer/design-system` is monolithic — it hardcodes three navigation sections (Applications, Agents, Routes), a starred section, bottom links (Admin, API Docs), and all tree-building logic. This makes it impossible for the consuming application to: 1. Add new sections (e.g., Admin sub-pages) without modifying the DS 2. Control section ordering or visibility based on application state 3. Implement accordion behavior (expanding one section collapses others) 4. Collapse the sidebar to an icon rail ## Solution Refactor the Sidebar into a **composable compound component** where the DS provides the shell and building blocks, and the consuming application controls all content. ## New DS API ### Compound Component Structure ```tsx setSidebarCollapsed(v => !v)} onSearchChange={setFilterQuery} > } title="cameleer" version="v3.2.1" /> } collapsed={appsCollapsed} onToggle={() => setAppsCollapsed(v => !v)} > } collapsed={agentsCollapsed} onToggle={() => setAgentsCollapsed(v => !v)} > } collapsed={routesCollapsed} onToggle={() => setRoutesCollapsed(v => !v)} > } collapsed={adminCollapsed} onToggle={() => setAdminCollapsed(v => !v)} > } label="API Docs" onClick={() => nav('/api-docs')} /> ``` ### Component Specifications #### `` The outer shell. Provides the sidebar frame, search input, scrollable content area, and collapse toggle. | Prop | Type | Default | Description | |------|------|---------|-------------| | `collapsed` | `boolean` | `false` | When true, renders as ~48px icon rail | | `onCollapseToggle` | `() => void` | - | Called when user clicks the collapse/expand toggle | | `onSearchChange` | `(query: string) => void` | - | Called on search input change. Omit to hide search. | | `children` | `ReactNode` | - | `Sidebar.Header`, `Sidebar.Section`, `Sidebar.Footer` | | `className` | `string` | - | Additional CSS class | **Search state ownership:** The DS renders the search input and calls `onSearchChange` on every keystroke. The consuming app owns the state (`filterQuery`) and passes it to each `SidebarTree`. This lets the app control filtering behavior (e.g., clear search on section switch, filter only certain sections). **Expanded layout (default):** ``` +---------------------------+ | [Header] [<<] | |---------------------------| | [Search...] | | | | [Section 1] | | [Section 2] | | [Section ...] | | | | [Footer] | +---------------------------+ ~260px ``` **Collapsed layout:** ``` +------+ | [>>] | |------| | [i1] | <- Section icons, centered | [i2] | <- Tooltip on hover shows label | [i3] | | [i4] | |------| | [f1] | <- Footer link icons +------+ ~48px ``` - `[<<]` / `[>>]` toggle button in top-right corner (chevron icon) - Width transition: CSS `width` + `transition: width 200ms ease` - When collapsed, search input is hidden - When collapsed, clicking a section icon fires both `onCollapseToggle` and that section's `onToggle` simultaneously. The sidebar expands and the section opens in one motion. No navigation occurs — the user clicks a tree item after the section is visible to navigate. #### `` Renders logo, title, and version. In collapsed mode, renders only the logo (centered). | Prop | Type | Default | Description | |------|------|---------|-------------| | `logo` | `ReactNode` | - | Logo icon/image (required) | | `title` | `string` | - | App name shown next to logo | | `version` | `string` | - | Version badge | #### `` An accordion section with a collapsible header and content area. | Prop | Type | Default | Description | |------|------|---------|-------------| | `label` | `string` | - | Section header text (rendered uppercase) | | `icon` | `ReactNode` | - | Icon shown in header and in collapsed rail | | `collapsed` | `boolean` | `false` | Whether content is hidden | | `onToggle` | `() => void` | - | Called when header is clicked | | `children` | `ReactNode` | - | Content rendered when expanded (typically `SidebarTree`) | | `active` | `boolean` | auto | Override active state highlight. Auto-detected if omitted (any descendant matches current path). | **Expanded state:** ``` v APPLICATIONS <- clickable header, chevron rotates > backend-app 3.4k > caller-app 891 > sample-app 9.6k ``` **Collapsed state:** ``` > APPLICATIONS <- single line, clickable ``` **In sidebar collapsed (icon rail) mode:** ``` [B] <- icon only, tooltip "Applications" ``` Header styling: uppercase label, muted color, chevron left of label, icon left of chevron. Active section gets amber accent (same as current active highlighting pattern). #### `` Pinned to the bottom of the sidebar. Renders children (typically `Sidebar.FooterLink` items). In collapsed mode, footer links render as centered icons. #### `` A single bottom link item. | Prop | Type | Default | Description | |------|------|---------|-------------| | `icon` | `ReactNode` | - | Link icon | | `label` | `string` | - | Link text (hidden when sidebar collapsed, shown as tooltip) | | `onClick` | `() => void` | - | Click handler | | `active` | `boolean` | `false` | Active state highlight | #### `` (unchanged, newly exported) The existing `SidebarTree` component stays as-is. It already accepts all its data via props (`nodes`, `selectedPath`, `filterQuery`, `onNavigate`, `persistKey`, `autoRevealPath`, `isStarred`, `onToggleStar`). It just needs to be exported from the package. The `SidebarTreeNode` type is also exported so consuming apps can build tree data. #### `useStarred` hook (unchanged, newly exported) The existing `useStarred` hook stays as-is. Export it so the consuming app can pass `isStarred`/`onToggleStar` to `SidebarTree`. ### What Gets Removed from DS The current monolithic `Sidebar` component contains ~300 lines of application-specific logic that moves to the server UI: 1. **Tree-building functions**: `buildAppTreeNodes()`, `buildRouteTreeNodes()`, `buildAgentTreeNodes()` — these transform `SidebarApp[]` into `SidebarTreeNode[]`. They move to the server UI. 2. **Starred section rendering**: `collectStarredItems()` and `StarredGroup` — starred items become a regular `Sidebar.Section` in the server UI (or inline within sections via `SidebarTree`'s existing star support). 3. **Hardcoded bottom links**: Admin and API Docs links — move to `Sidebar.Footer` + `Sidebar.FooterLink` in server UI. 4. **Section collapse state management**: localStorage persistence of `cameleer:sidebar:*-collapsed` — moves to server UI. 5. **Auto-reveal logic**: `sidebarRevealPath` effect that auto-expands sections — moves to server UI. 6. **`SidebarApp` / `SidebarRoute` / `SidebarAgent` types**: These are application-domain types, not DS types. Move to server UI. DS only exports `SidebarTreeNode`. ### New DS Exports ```typescript // layout/index.ts — updated exports export { Sidebar } from './Sidebar/Sidebar' export { SidebarTree } from './Sidebar/SidebarTree' export type { SidebarTreeNode } from './Sidebar/SidebarTree' export { useStarred } from './Sidebar/useStarred' ``` The `SidebarApp`, `SidebarRoute`, `SidebarAgent` type exports are removed (they move to the consuming application). ## Server UI Migration ### LayoutShell.tsx Changes The current `LayoutShell` already does most of the data preparation (building `sidebarApps`, handling `handleSidebarNavigate`). After migration: 1. Move tree-building functions (`buildAppTreeNodes`, etc.) from DS into a local `sidebar-utils.ts` 2. Manage section collapse states with localStorage persistence 3. Implement accordion logic: when on `/admin/*`, expand Admin section and collapse operational sections; when navigating away, restore previous states 4. Pass `filterQuery` from search to each `SidebarTree` 5. Compose the new `` with sections ### AdminLayout.tsx Changes Remove the `` navigation — sidebar now handles admin sub-page navigation. Keep just the content wrapper: ```tsx export default function AdminLayout() { return (
); } ``` ### ContentTabs Still hidden on admin pages (existing `isAdminPage` guard). The tab strip is irrelevant when the sidebar shows the admin context. ### TopBar No changes. Stays visible on all pages. ## Accordion Behavior (Server UI Logic) The accordion is not a DS concept — it's application logic in the server UI: ```typescript // When navigating to /admin/*: // 1. Remember current operational collapse states // 2. Collapse all operational sections // 3. Expand Admin section // When navigating away from /admin/*: // 1. Collapse Admin section // 2. Restore operational collapse states from memory ``` This means the DS sections are purely controlled components. The app decides which are open/closed based on current route. ## Visual States ### Operational Mode ``` +---------------------------+ | [C] cameleer v3.2.1 [<<] | |---------------------------| | [Search...] | | | | v APPLICATIONS | | > backend-app 3.4k | | > caller-app 891 | | > sample-app 9.6k | | | | v AGENTS | | > backend-app 3/3 live | | > caller-app 2/2 live | | | | > ROUTES | | > ADMIN | | | | [API Docs] | +---------------------------+ ``` ### Admin Mode (accordion) ``` +---------------------------+ | [C] cameleer v3.2.1 [<<] | |---------------------------| | [Search...] | | | | v ADMIN | | > User Management | | > Audit Log | | > OIDC | | > App Config | | > Database | | > ClickHouse | | | | > APPLICATIONS | | > AGENTS | | > ROUTES | | | | [API Docs] | +---------------------------+ ``` ### Collapsed (Icon Rail) ``` +------+ | [C] | | [>>] | |------| | [B] | <- Applications | [C] | <- Agents | [G] | <- Routes | [S] | <- Admin |------| | [F] | <- API Docs +------+ ``` ## Implementation Order This is a two-repo change: 1. **DS refactor** (design-system repo): - Create `Sidebar` compound component shell (`Sidebar`, `Sidebar.Header`, `Sidebar.Section`, `Sidebar.Footer`, `Sidebar.FooterLink`) - Extract `SidebarTree` and `useStarred` as standalone exports - Add `collapsed` / icon-rail mode - Remove application-specific logic (tree builders, hardcoded sections, bottom links) - Update barrel exports - Bump version 2. **Server UI migration** (cameleer-server repo): - Move tree-building functions to local utils - Rewrite sidebar composition in `LayoutShell` using new compound API - Add accordion logic for admin mode - Add sidebar collapse toggle with localStorage persistence - Simplify `AdminLayout` (remove tabs) ## Acceptance Criteria - [ ] DS: `Sidebar` accepts `collapsed` prop and renders as icon rail when true - [ ] DS: `Sidebar.Section` renders as accordion item with icon, label, chevron, and children - [ ] DS: `Sidebar.Footer` / `Sidebar.FooterLink` render bottom links - [ ] DS: `SidebarTree`, `SidebarTreeNode`, `useStarred` exported as standalone - [ ] DS: No application-specific logic remains in Sidebar (no hardcoded sections, no tree builders) - [ ] UI: Sidebar shows operational sections (Apps, Agents, Routes) on operational pages - [ ] UI: Clicking Admin expands Admin section, collapses operational sections - [ ] UI: Clicking an operational section header exits admin mode, restores previous state - [ ] UI: Sidebar collapse/expand toggle works with icon rail mode - [ ] UI: Admin tabs removed from AdminLayout (sidebar handles navigation) - [ ] UI: All icons passed by the consuming application, not hardcoded in DS