Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, migrations, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
369 lines
13 KiB
Markdown
369 lines
13 KiB
Markdown
# Composable Sidebar with Accordion & Collapse
|
|
|
|
**Issue:** [#112](https://gitea.siegeln.net/cameleer/cameleer-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 |
|
|
|
|
**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.
|
|
|
|
#### `<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** (cameleer-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
|