Files
design-system/docs/superpowers/specs/2026-04-02-composable-sidebar-design.md
hsiegeln 4e2d5b2b2f docs: composable sidebar refactor spec
Compound component API replacing monolithic Sidebar. DS provides
shell (Sidebar, Sidebar.Header, Sidebar.Section, Sidebar.Footer,
Sidebar.FooterLink) + standalone SidebarTree and useStarred exports.
Application controls all content, icons, sections. Adds icon-rail
collapse mode. Breaking change — coordinate with server UI migration.

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

12 KiB

Composable Sidebar Refactor

Date: 2026-04-02 Upstream issue: cameleer3-server #112

Why

The current Sidebar component is monolithic. It hardcodes three navigation sections (Applications, Agents, Routes), a starred items section, bottom links (Admin, API Docs), and all tree-building logic (buildAppTreeNodes, buildRouteTreeNodes, buildAgentTreeNodes). The consuming application can only pass SidebarApp[] data — it cannot control what sections exist, what order they appear in, or add new sections without modifying this package.

This blocks two features the consuming application needs:

  1. Admin accordion — when the user enters admin context, the sidebar should expand an Admin section and collapse operational sections, all controlled by the application
  2. Icon-rail collapse — the sidebar should collapse to a narrow icon strip, like modern app sidebars (Linear, VS Code, etc.)

Goal

Refactor Sidebar into a composable compound component. The DS provides the frame and building blocks. The consuming application controls all content.

Current Exports (to be replaced)

// Current — monolithic
export { Sidebar } from './Sidebar/Sidebar'
export type { SidebarApp, SidebarRoute, SidebarAgent } from './Sidebar/Sidebar'

New Exports

// New — composable
export { Sidebar } from './Sidebar/Sidebar'
export { SidebarTree } from './Sidebar/SidebarTree'
export type { SidebarTreeNode } from './Sidebar/SidebarTree'
export { useStarred } from './Sidebar/useStarred'

SidebarApp, SidebarRoute, SidebarAgent types are removed — they are application-domain types that move to the consuming app.

Compound Component API

<Sidebar>

The outer shell. Renders the sidebar frame with an optional search input and collapse toggle.

<Sidebar
  collapsed={false}
  onCollapseToggle={() => {}}
  onSearchChange={(query) => {}}
  className=""
>
  <Sidebar.Header ... />
  <Sidebar.Section ... />
  <Sidebar.Section ... />
  <Sidebar.Footer ... />
</Sidebar>
Prop Type Default Description
collapsed boolean false Render as ~48px icon rail
onCollapseToggle () => void - Collapse/expand toggle clicked
onSearchChange (query: string) => void - Search input changed. Omit to hide search.
children ReactNode - Sidebar.Header, Sidebar.Section, Sidebar.Footer
className string - Additional CSS class

Rendering rules:

  • Expanded: full width (~260px), all content visible
  • Collapsed: ~48px wide, only icons visible, tooltips on hover
  • Width transition: transition: width 200ms ease
  • Collapse toggle button (<< / >> chevron) in top-right corner
  • Search input hidden when collapsed

<Sidebar.Header>

Logo, title, and version. In collapsed mode, renders only the logo centered.

<Sidebar.Header
  logo={<img src="..." />}
  title="cameleer"
  version="v3.2.1"
/>
Prop Type Default Description
logo ReactNode - Logo element
title string - App name (hidden when collapsed)
version string - Version text (hidden when collapsed)

<Sidebar.Section>

An accordion section with a collapsible header and content area.

<Sidebar.Section
  label="APPLICATIONS"
  icon={<Box size={14} />}
  collapsed={false}
  onToggle={() => {}}
  active={false}
>
  <SidebarTree nodes={nodes} ... />
</Sidebar.Section>
Prop Type Default Description
label string - Section header text (rendered uppercase via CSS)
icon ReactNode - Icon for header and collapsed rail
collapsed boolean false Whether children are hidden
onToggle () => void - Header clicked
children ReactNode - Content when expanded
active boolean - Override active highlight. If omitted, not highlighted.

Expanded rendering:

v [icon] APPLICATIONS
  (children rendered here)

Collapsed rendering:

> [icon] APPLICATIONS

In sidebar icon-rail mode:

[icon]     <- centered, tooltip shows label on hover

Header has: chevron (left), icon, label. Chevron rotates on collapse/expand. Active section gets the amber left-border accent (existing pattern). Clicking the header calls onToggle. In icon-rail mode, clicking the icon calls both onCollapseToggle (to expand the sidebar) and onToggle.

<Sidebar.Footer>

Pinned to the bottom of the sidebar. Container for Sidebar.FooterLink items.

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

In collapsed mode, footer links render as centered icons with tooltips.

A single bottom link.

Prop Type Default Description
icon ReactNode - Link icon
label string - Link text (hidden when collapsed, shown as tooltip)
onClick () => void - Click handler
active boolean false Active state highlight

<SidebarTree> (no changes, newly exported)

Already exists at Sidebar/SidebarTree.tsx. No modifications needed — it already accepts all data via props. Just export it from the package.

Current props (unchanged):

Prop Type Description
nodes SidebarTreeNode[] Tree data
selectedPath string Currently active path for highlighting
filterQuery string Search filter text
onNavigate (path: string) => void Navigation callback
persistKey string localStorage key for expand state
autoRevealPath string | null Path to auto-expand to
isStarred (id: string) => boolean Star state checker
onToggleStar (id: string) => void Star toggle callback

useStarred hook (no changes, newly exported)

Already exists at Sidebar/useStarred.ts. Export as-is.

Returns: { starredIds, isStarred, toggleStar }

What Gets Removed

All of this application-specific logic is deleted from the DS:

  1. buildAppTreeNodes() (~30 lines) — transforms SidebarApp[] into SidebarTreeNode[]
  2. buildRouteTreeNodes() (~20 lines) — transforms apps into route tree nodes
  3. buildAgentTreeNodes() (~25 lines) — transforms apps into agent tree nodes with live-count badges
  4. collectStarredItems() (~20 lines) — gathers starred items across types
  5. StarredGroup sub-component (~30 lines) — renders grouped starred items
  6. Hardcoded sections (~100 lines) — Applications, Agents, Routes section rendering with localStorage persistence
  7. Hardcoded bottom links (~30 lines) — Admin and API Docs links
  8. Auto-reveal effect (~20 lines) — sidebarRevealPath effect
  9. SidebarApp, SidebarRoute, SidebarAgent types — domain types, not DS types
  10. formatCount() helper — number formatting, moves to consuming app

Total: ~300 lines of application logic removed, replaced by ~150 lines of compound component shell.

CSS Changes

New styles needed

  • .sidebarCollapsed — narrow width (48px), centered icons
  • .collapseToggle<< / >> button positioning
  • .sectionIcon — icon rendering in section headers
  • .tooltip — hover tooltips for collapsed mode
  • Width transition: transition: width 200ms ease on .sidebar

Styles that stay

  • .sidebar (modified: width becomes conditional)
  • .searchWrap, .searchInput (unchanged)
  • .navArea (unchanged)
  • All tree styles in SidebarTree (unchanged)

Styles removed

  • .bottom, .bottomItem, .bottomItemActive — replaced by Sidebar.Footer / Sidebar.FooterLink styles
  • .starredSection, .starredGroup, .starredItem, .starredRemove — starred rendering moves to app
  • .section — replaced by Sidebar.Section styles

File Structure After Refactor

Sidebar/
├── Sidebar.tsx              # Compound component: Sidebar, Sidebar.Header,
│                            # Sidebar.Section, Sidebar.Footer, Sidebar.FooterLink
├── Sidebar.module.css       # Updated styles (shell + section + footer + collapsed)
├── SidebarTree.tsx           # Unchanged
├── SidebarTree.module.css    # Unchanged (if separate, otherwise stays in Sidebar.module.css)
├── useStarred.ts             # Unchanged
├── useStarred.test.ts        # Unchanged
└── Sidebar.test.tsx          # Updated for new compound API

Testing

Update Sidebar.test.tsx to test the compound component API:

  • Renders Header with logo, title, version
  • Renders Sections with labels and icons
  • Section toggle calls onToggle
  • Collapsed sections hide children
  • Sidebar collapsed mode renders icon rail
  • Collapse toggle calls onCollapseToggle
  • Footer links render with icons and labels
  • Collapsed mode hides labels, shows tooltips
  • Search input calls onSearchChange
  • Search hidden when sidebar collapsed
  • Section icon click in collapsed mode calls both onCollapseToggle and onToggle

SidebarTree tests are unaffected.

Usage Example (for reference)

This is how the consuming application (cameleer3-server) will use the new API. This code does NOT live in the design system — it's shown for context only.

// In LayoutShell.tsx (consuming app)
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [filterQuery, setFilterQuery] = useState('');
const [appsCollapsed, setAppsCollapsed] = useState(false);
const [agentsCollapsed, setAgentsCollapsed] = useState(false);
const [routesCollapsed, setRoutesCollapsed] = useState(true);
const [adminCollapsed, setAdminCollapsed] = useState(true);

// Accordion: entering admin expands admin, collapses others
useEffect(() => {
  if (isAdminPage) {
    setAdminCollapsed(false);
    setAppsCollapsed(true);
    setAgentsCollapsed(true);
    setRoutesCollapsed(true);
  } else {
    setAdminCollapsed(true);
    // restore previous operational states
  }
}, [isAdminPage]);

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

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

  <Sidebar.Section label="APPLICATIONS" icon={<Box size={14} />}
    collapsed={appsCollapsed} onToggle={() => { setAppsCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}>
    <SidebarTree nodes={appNodes} ... filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Section label="AGENTS" icon={<Cpu size={14} />}
    collapsed={agentsCollapsed} onToggle={() => { setAgentsCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}>
    <SidebarTree nodes={agentNodes} ... filterQuery={filterQuery} />
  </Sidebar.Section>

  <Sidebar.Section label="ROUTES" icon={<GitBranch size={14} />}
    collapsed={routesCollapsed} onToggle={() => { setRoutesCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}>
    <SidebarTree nodes={routeNodes} ... filterQuery={filterQuery} />
  </Sidebar.Section>

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

Breaking Change

This is a breaking change to the Sidebar API. The old <Sidebar apps={[...]} onNavigate={...} /> signature is removed entirely. The consuming application must migrate to the compound component API in the same release cycle.

Coordinate: bump DS version, update server UI, deploy together.