docs: composable sidebar design spec for #112
Replaces the previous "hide sidebar on admin" approach with a composable compound component design. DS provides shell + building blocks (Sidebar, Section, Footer, SidebarTree); consuming app controls all content, section ordering, accordion behavior, and icon-rail collapse. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
# Composable Sidebar with Accordion & Collapse
|
||||
|
||||
**Issue:** [#112](https://gitea.siegeln.net/cameleer/cameleer3-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
|
||||
<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 |
|
||||
|
||||
**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 calls `onCollapseToggle` (to expand) and that section's `onToggle` (to open it)
|
||||
|
||||
#### `<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.
|
||||
|
||||
#### `<Sidebar.FooterLink>`
|
||||
|
||||
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
|
||||
|
||||
```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 `<Sidebar>` with sections
|
||||
|
||||
### AdminLayout.tsx Changes
|
||||
|
||||
Remove the `<Tabs>` navigation — sidebar now handles admin sub-page navigation. Keep just the content wrapper:
|
||||
|
||||
```tsx
|
||||
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:
|
||||
|
||||
```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** (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
|
||||
Reference in New Issue
Block a user