Files
design-system/docs/superpowers/specs/2026-03-18-sidebar-redesign.md
hsiegeln e69e5ab5fe feat: redesign Sidebar with hierarchical trees, starring, and collapsible sections
Replace flat app/route/agent lists with expandable tree navigation.
Apps contain their routes and agents hierarchically. Add localStorage-
backed starring with composite keys for uniqueness. Persist expand
state to sessionStorage across page navigations. Add collapsible
section headers, remove button on starred items, and parent app
context labels. Create stub pages for /apps/:id, /agents/:id,
/admin, /api-docs. Consolidate duplicated sidebar data into
shared mock. Widen sidebar from 220px to 260px.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 17:50:41 +01:00

14 KiB

Sidebar Redesign — Hierarchical Navigation with Starring

Context

The Cameleer3 Sidebar currently renders flat lists of applications, routes, and agents as separate sections. This doesn't reflect the hierarchical relationship in Apache Camel where routes and agents belong to applications. The redesign restructures navigation into expandable trees, adds a starring/favorites system, and adds bottom navigation links (Admin, API Docs).

This project is primarily a design system with a mock application showcasing how all pieces come together. Stub pages will be created for new routes to demonstrate the full navigation flow.

Sidebar Structure

┌─────────────────────────┐
│  cameleer  v3.2.1       │  Logo (fixed)
├─────────────────────────┤
│  Filter...              │  Search (fixed)
├─────────────────────────┤
│  NAVIGATION             │  Section header
│                         │
│  ▾ Applications         │  Tree (expandable)
│    ● order-svc    1.2k  │    App (health dot + exchangeCount)
│      ▶ ingest      540  │    Route (arrow + exchangeCount)
│      ▶ notify      320  │
│    ● payment-svc   800  │
│      ▶ validate    800  │
│                         │
│  ▾ Agents               │  Tree (expandable)
│    ● order-svc  2/3 live│    App (health dot + live count)
│      agent-1    42 tps  │    Agent (name + tps)
│      agent-2    38 tps  │
│    ● payment-svc 1/1 ...│
│      agent-3    12 tps  │
│                         │
│  ▦ Dashboards           │  Standalone nav link (not a tree node)
│         ↕ scrollable    │
├─────────────────────────┤
│  ★ STARRED              │  Section header (hidden when empty)
│  Applications           │  Group label
│    ● order-svc          │
│  Routes                 │  Group label
│    ▶ ingest             │
│  Agents                 │  Group label
│    agent-prod-1         │
│         ↕ scrollable    │
├─────────────────────────┤
│  ⚙ Admin                │  Bottom link → /admin (fixed)
│  📄 API Docs            │  Bottom link → /api-docs (fixed)
└─────────────────────────┘

Width: 220px (unchanged)

Scroll regions:

  • Navigation: flex: 1; overflow-y: auto; min-height: 0
  • Starred: max-height: 30vh; overflow-y: auto (entire section hidden when no items are starred)
  • Logo, Search, Bottom: fixed (flex-shrink: 0)

Data Model

New interfaces (replace current flat App/Route/Agent)

interface SidebarApp {
  id: string
  name: string
  health: 'live' | 'stale' | 'dead'
  exchangeCount: number
  routes: SidebarRoute[]
  agents: SidebarAgent[]
}

interface SidebarRoute {
  id: string
  name: string
  exchangeCount: number
}

interface SidebarAgent {
  id: string
  name: string
  status: 'live' | 'stale' | 'dead'
  tps: string
}

interface SidebarProps {
  apps: SidebarApp[]
  activeItemId?: string   // highlighted item; derived from URL if omitted
  className?: string
}

Key changes from current:

  • Routes and agents nested inside apps (was: three flat arrays)
  • onItemClick removed — Sidebar navigates internally via useNavigate()
  • Active item derived from useLocation().pathname if activeItemId not provided
  • Route health is out of scope — routes show a static arrow icon (no StatusDot)

Agent type migration

The current src/mocks/agents.ts has AgentHealth extends Agent where Agent is imported from Sidebar. Since SidebarAgent is now a minimal type (just id, name, status, tps), AgentHealth must become self-contained:

// src/mocks/agents.ts — no longer extends Sidebar's Agent type
export interface AgentHealth {
  id: string
  name: string
  service: string
  version: string
  tps: string
  lastSeen: string
  status: 'live' | 'stale' | 'dead'
  errorRate?: string
  uptime: string
  memoryUsagePct: number
  cpuUsagePct: number
  activeRoutes: number
  totalRoutes: number
}

Components

SidebarTree (src/design-system/layout/Sidebar/SidebarTree.tsx)

A sidebar-specific tree component. NOT the generic TreeView — optimized for sidebar rendering with health dots, count badges, and star buttons.

Props:

interface SidebarTreeNode {
  id: string
  label: string
  icon?: ReactNode         // StatusDot, arrow icon, etc.
  badge?: string           // right-aligned text ("1.2k", "42 tps", "2/3 live")
  path?: string            // navigation path on click
  starrable?: boolean      // whether star toggle appears (default false)
  children?: SidebarTreeNode[]
}

interface SidebarTreeProps {
  nodes: SidebarTreeNode[]
  selectedId?: string
  isStarred: (id: string) => boolean
  onToggleStar: (id: string) => void
  onNavigate?: (path: string) => void
  className?: string
  filterQuery?: string     // when set, auto-expands matching parents
}

Styling: All SidebarTree styles go in Sidebar.module.css (SidebarTree is a private component of Sidebar, not independently used).

Behaviors:

  • Expand/collapse: Chevron click only. Row click navigates (if path set).
  • Parent nodes without path: Click does nothing (only chevron toggles). The "Applications" and "Agents" root headers are not clickable or starrable — they're just section labels with a toggle chevron.
  • Star toggle: Star icon appears on hover (right side, after badge). Filled star always visible on starred items. Click stops propagation (doesn't navigate).
  • Leaf nodes with no children: Render without chevron. App nodes with empty routes[] or agents[] render as leaf nodes (no expand arrow).
  • Search filter: When filterQuery is set, only nodes matching the query (or with matching descendants) are shown. Matching parents auto-expand. When no matches, the tree is empty (the Sidebar shows a "No results" message).
  • Keyboard nav: ArrowUp/Down to move focus, ArrowRight to expand, ArrowLeft to collapse, Enter to navigate/select, Home/End to jump.
  • ARIA: role="tree", role="treeitem", aria-expanded, aria-selected.
  • Styling: Uses --sidebar-* CSS tokens. Active item gets amber highlight.

useStarred (src/design-system/layout/Sidebar/useStarred.ts)

function useStarred(): {
  starredIds: Set<string>
  isStarred: (id: string) => boolean
  toggleStar: (id: string) => void
}
  • Reads/writes localStorage key "cameleer:starred" (JSON string array)
  • Returns a Set<string> for O(1) lookups
  • toggleStar updates both React state and localStorage
  • Error handling: Wraps localStorage read/write in try/catch — falls back to in-memory state if localStorage is unavailable (private browsing, quota exceeded)
  • Cross-tab sync is out of scope for now

Sidebar (rewritten)

The Sidebar assembles the SidebarTree data from SidebarApp[] props:

  1. Applications tree: Maps each app to a parent node with route children. App nodes show StatusDot + name + exchangeCount badge. Route nodes show arrow icon + name + exchangeCount badge. Both apps and routes are starrable: true.

  2. Agents tree: Maps each app to a parent node with agent children. App nodes show StatusDot + name + "X/Y live" badge (computed: count agents where status === 'live' / total agents). Agent nodes show name + tps badge. Both apps and agents are starrable: true.

  3. Dashboards: A standalone nav link (not part of any tree), rendered as a simple clickable row below the trees. Uses icon. Note: the current top-level Metrics and Agents nav links are intentionally removed — Metrics will be displayed contextually within app/route/agent detail views, and Agents are accessible via the Agents tree.

  4. Starred section: Reads starredIds from useStarred(), looks up items in the apps data, groups by type (Applications, Routes, Agents in that order). Orphaned IDs (starred item no longer in data) are silently ignored. If a starred app appears in both the Applications and Agents trees, it shows only under "Applications" in the starred section. The entire section (header + content) is hidden when no items are starred.

  5. Search: Filters both trees. When query matches a route/agent name, its parent app stays visible and auto-expands. Placeholder text: "Filter...". On clear, restore previous expand/collapse state.

Star Icon

Inline SVG (no icon library). Outline star for unstarred, filled star for starred. Amber color (var(--amber)).

<!-- outline (unstarred) -->
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>

<!-- filled (starred) -->
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2">
  <path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>

Visibility rules:

  • Unstarred items: star appears on row :hover only
  • Starred items: filled star always visible
  • Star button: opacity: 0opacity: 1 on hover transition (0.15s)

Navigation Paths

Item type Click navigates to Route exists?
App (in Applications tree) /apps/{id} New stub page
Route /routes/{id} Existing
App (in Agents tree) /apps/{id} Same stub page
Agent /agents/{id} New — currently only /agents (list) exists
Dashboards / Existing (Dashboard)
Admin /admin New stub page
API Docs /api-docs New stub page

Stub pages needed

Minimal placeholder pages using the design system's EmptyState component:

  • AppDetail (src/pages/AppDetail/AppDetail.tsx) — route /apps/:id, shows app name from URL param + EmptyState "App detail coming soon"
  • AgentDetail (src/pages/AgentDetail/AgentDetail.tsx) — route /agents/:id, shows agent name + EmptyState
  • Admin (src/pages/Admin/Admin.tsx) — route /admin, EmptyState "Admin panel coming soon"
  • ApiDocs (src/pages/ApiDocs/ApiDocs.tsx) — route /api-docs, EmptyState "API documentation coming soon"

All stub pages use <AppShell> with <Sidebar> to demonstrate the full navigation flow.

Search Behavior

  • Case-insensitive substring match on node labels
  • When a child matches, its parent node is shown and auto-expanded
  • When no matches, show "No results" text in the navigation area
  • Search clears when input is emptied
  • On clear, restore previous expand/collapse state
  • Search filters both the Applications and Agents trees simultaneously

Mock Data

New file: src/mocks/sidebar.ts

Exports SIDEBAR_APPS: SidebarApp[] — consolidates the duplicated APPS, SIDEBAR_ROUTES, and agents data currently scattered across 6 page files. Each app contains its routes and agents nested inside.

All consumer pages + Inventory demo will import from this single source.

Files to Create

File Purpose
src/design-system/layout/Sidebar/SidebarTree.tsx Tree component with health dots, badges, star toggle
src/design-system/layout/Sidebar/useStarred.ts localStorage hook for starred item IDs
src/mocks/sidebar.ts Shared hierarchical sidebar mock data
src/pages/AppDetail/AppDetail.tsx Stub page for /apps/:id
src/pages/AgentDetail/AgentDetail.tsx Stub page for /agents/:id
src/pages/Admin/Admin.tsx Stub page for /admin
src/pages/ApiDocs/ApiDocs.tsx Stub page for /api-docs
src/design-system/layout/Sidebar/Sidebar.test.tsx Tests for Sidebar
src/design-system/layout/Sidebar/useStarred.test.ts Tests for useStarred hook

Files to Modify

File Changes
src/design-system/layout/Sidebar/Sidebar.tsx Rewrite: new props, SidebarTree, starred section, bottom links
src/design-system/layout/Sidebar/Sidebar.module.css Add tree styles, starred section, star button, bottom links, SidebarTree styles
src/design-system/layout/index.ts Export new types (SidebarApp, SidebarRoute, SidebarAgent)
src/App.tsx Add routes: /apps/:id, /agents/:id, /admin, /api-docs
src/pages/Dashboard/Dashboard.tsx Simplify to <Sidebar apps={SIDEBAR_APPS} />
src/pages/RouteDetail/RouteDetail.tsx Same simplification
src/pages/ExchangeDetail/ExchangeDetail.tsx Same simplification
src/pages/AgentHealth/AgentHealth.tsx Same simplification
src/pages/Metrics/Metrics.tsx Same simplification
src/pages/Inventory/sections/LayoutSection.tsx Update demo data to hierarchical shape
src/mocks/agents.ts Make AgentHealth self-contained (no longer extends Sidebar's Agent)
COMPONENT_GUIDE.md Update Sidebar and navigation descriptions

Implementation Order

  1. Mock data (src/mocks/sidebar.ts) — needed for development and testing
  2. Agent type fix (src/mocks/agents.ts) — make AgentHealth self-contained
  3. useStarred hook + tests — standalone, no dependencies
  4. SidebarTree component — depends on useStarred interface
  5. Sidebar rewrite + CSS — composes SidebarTree + useStarred
  6. Stub pages — AppDetail, AgentDetail, Admin, ApiDocs
  7. App.tsx routes — register new routes
  8. Consumer migration — update all 6 pages to use <Sidebar apps={SIDEBAR_APPS} />
  9. Inventory update — update LayoutSection demo data
  10. Type exports — update layout barrel export
  11. Sidebar tests — integration tests for Sidebar component
  12. Docs — update COMPONENT_GUIDE.md

Verification

  1. npx tsc --noEmit — zero TypeScript errors
  2. npx vitest run — all tests pass (existing + new)
  3. npm run build — clean Vite build
  4. Manual: open /inventory → Layout section → verify Sidebar demo renders correctly
  5. Manual: open / → verify sidebar trees expand/collapse, starring persists across refresh, search filters both trees, navigation to all routes works
  6. Manual: verify starred section hides when empty, shows grouped items when populated
  7. Manual: verify dark theme rendering (sidebar uses --sidebar-* tokens)
  8. grep -r "onItemClick\|routes={.*SIDEBAR\|agents={.*agents" src/design-system/layout/Sidebar/ — zero hits (old API removed)