From 4e2d5b2b2fc8b9c3baade5a2370aa9df2c1eeaf5 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 2 Apr 2026 17:38:04 +0200 Subject: [PATCH] docs: composable sidebar refactor spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../2026-04-02-composable-sidebar-design.md | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-02-composable-sidebar-design.md diff --git a/docs/superpowers/specs/2026-04-02-composable-sidebar-design.md b/docs/superpowers/specs/2026-04-02-composable-sidebar-design.md new file mode 100644 index 0000000..0da57b4 --- /dev/null +++ b/docs/superpowers/specs/2026-04-02-composable-sidebar-design.md @@ -0,0 +1,317 @@ +# 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) + +```typescript +// Current — monolithic +export { Sidebar } from './Sidebar/Sidebar' +export type { SidebarApp, SidebarRoute, SidebarAgent } from './Sidebar/Sidebar' +``` + +## New Exports + +```typescript +// 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 + +### `` + +The outer shell. Renders the sidebar frame with an optional search input and collapse toggle. + +```tsx + {}} + onSearchChange={(query) => {}} + className="" +> + + + + + +``` + +| 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 + +### `` + +Logo, title, and version. In collapsed mode, renders only the logo centered. + +```tsx +} + 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) | + +### `` + +An accordion section with a collapsible header and content area. + +```tsx +} + collapsed={false} + onToggle={() => {}} + active={false} +> + + +``` + +| 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`. + +### `` + +Pinned to the bottom of the sidebar. Container for `Sidebar.FooterLink` items. + +```tsx + + } label="API Docs" onClick={() => {}} /> + +``` + +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 | + +### `` (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. + +```tsx +// 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]); + + setSidebarCollapsed(v => !v)} + onSearchChange={setFilterQuery} +> + } title="cameleer" version="v3.2.1" /> + + {isAdminPage && ( + } + collapsed={adminCollapsed} onToggle={() => setAdminCollapsed(v => !v)}> + + + )} + + } + collapsed={appsCollapsed} onToggle={() => { setAppsCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}> + + + + } + collapsed={agentsCollapsed} onToggle={() => { setAgentsCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}> + + + + } + collapsed={routesCollapsed} onToggle={() => { setRoutesCollapsed(v => !v); if (isAdminPage) nav('/exchanges'); }}> + + + + + } label="API Docs" onClick={() => nav('/api-docs')} /> + + +``` + +## Breaking Change + +This is a **breaking change** to the `Sidebar` API. The old `` 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.