Files
cameleer-server/docs/superpowers/specs/2026-04-02-admin-context-separation-design.md
hsiegeln d70ad91b33 docs: clarify search ownership and icon-rail click behavior
Search: DS renders dumb input, app owns filterQuery state and
passes it to each SidebarTree. Icon-rail click: fires both
onCollapseToggle and onToggle simultaneously, no navigation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 17:41:31 +02:00

13 KiB

Composable Sidebar with Accordion & Collapse

Issue: #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

<Sidebar
  collapsed={sidebarCollapsed}
  onCollapseToggle={() => setSidebarCollapsed(v => !v)}
  onSearchChange={setFilterQuery}
>
  <Sidebar.Header
    logo={<CameleerLogo />}
    title="cameleer"
    version="v3.2.1"
  />

  <Sidebar.Section
    label="APPLICATIONS"
    icon={<Box size={14} />}
    collapsed={appsCollapsed}
    onToggle={() => setAppsCollapsed(v => !v)}
  >
    <SidebarTree nodes={appNodes} selectedPath={path} onNavigate={nav} filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Section
    label="AGENTS"
    icon={<Cpu size={14} />}
    collapsed={agentsCollapsed}
    onToggle={() => setAgentsCollapsed(v => !v)}
  >
    <SidebarTree nodes={agentNodes} selectedPath={path} onNavigate={nav} filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Section
    label="ROUTES"
    icon={<GitBranch size={14} />}
    collapsed={routesCollapsed}
    onToggle={() => setRoutesCollapsed(v => !v)}
  >
    <SidebarTree nodes={routeNodes} selectedPath={path} onNavigate={nav} filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Section
    label="ADMIN"
    icon={<Settings size={14} />}
    collapsed={adminCollapsed}
    onToggle={() => setAdminCollapsed(v => !v)}
  >
    <SidebarTree nodes={adminNodes} selectedPath={path} onNavigate={nav} filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Footer>
    <Sidebar.FooterLink icon={<FileText size={14} />} label="API Docs" onClick={() => nav('/api-docs')} />
  </Sidebar.Footer>
</Sidebar>

Component Specifications

<Sidebar>

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.

<Sidebar.Header>

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

<Sidebar.Section>

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).

<Sidebar.Footer>

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

<SidebarTree> (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

// 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 <Sidebar> with sections

AdminLayout.tsx Changes

Remove the <Tabs> navigation — sidebar now handles admin sub-page navigation. Keep just the content wrapper:

export default function AdminLayout() {
  return (
    <div style={{ padding: '20px 24px 40px' }}>
      <Outlet />
    </div>
  );
}

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:

// 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 (cameleer3-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