- Move MultiSelect to composites (depends on portal, not a primitive) - MultiSelect manages own positioning instead of wrapping Popover - Add loading prop and info variant to ConfirmDialog - Drop forwardRef from InlineEdit (input conditionally exists) - Change InlineEdit blur to cancel (not save) - Add router integration, barrel export, and accessibility details - Add sidebar integration strategy (admin sub-nav, not sidebar clutter) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
11 KiB
Admin Pages + New Components Design
Date: 2026-03-18 Scope: 3 new design system components + 3 admin example pages
Overview
Transfer the admin section from cameleer3-server UI to the design system project as example pages. Add three new reusable components to the design system that are needed by these pages and useful generally.
New Design System Components
1. MultiSelect (composite)
Dropdown trigger that opens a positioned panel with searchable checkbox list and "Apply" action.
Props:
interface MultiSelectOption {
value: string
label: string
}
interface MultiSelectProps {
options: MultiSelectOption[]
value: string[]
onChange: (value: string[]) => void
placeholder?: string // default: "Select..."
searchable?: boolean // default: true
disabled?: boolean
className?: string
}
Behavior:
- Click trigger → panel opens below with search input + checkbox list + "Apply (N)" footer
- Search filters options by label (case-insensitive)
- Checkboxes toggle selection; changes are local until "Apply" is clicked
- Apply calls
onChangewith selected values and closes panel - Click outside or Escape closes without applying (discards pending changes)
- Trigger shows count: "2 selected" or placeholder when empty
- Arrow keys navigate checkbox list, Space toggles focused item, Tab moves between search/list/apply
- Panel has max-height with scroll for long option lists
Accessibility:
- Trigger:
role="combobox",aria-expanded,aria-haspopup="listbox" - Option list:
role="listbox", options haverole="option"witharia-selected - Search input:
aria-label="Filter options"
Implementation:
- New directory:
src/design-system/composites/MultiSelect/ - Manages its own open/close state and positioning (does NOT wrap Popover — needs controlled close behavior to distinguish apply vs. discard)
- Uses portal for the dropdown panel to avoid overflow clipping
- CSS Modules with design tokens
2. ConfirmDialog (composite)
Modal dialog requiring the user to type a confirmation string before a destructive action proceeds.
Props:
interface ConfirmDialogProps {
open: boolean
onClose: () => void
onConfirm: () => void
title?: string // default: "Confirm Deletion"
message: string // e.g., "Delete user 'alice'? This cannot be undone."
confirmText: string // text the user must type to enable confirm button (must be non-empty)
confirmLabel?: string // default: "Delete"
cancelLabel?: string // default: "Cancel"
variant?: 'danger' | 'warning' | 'info' // default: 'danger'
loading?: boolean // default: false — disables buttons, shows pending state
className?: string
}
Behavior:
- Built on Modal (size="sm")
- Shows title, message, text input with label "Type '{confirmText}' to confirm"
- Confirm button disabled until input matches
confirmTextexactly - Input clears on open
- Enter submits when enabled; Escape closes
- Confirm button uses danger/warning/info variant styling (matches AlertDialog pattern)
- When
loadingis true, both buttons are disabled
Implementation:
- New directory:
src/design-system/composites/ConfirmDialog/ - Reuses Modal internally (same pattern as AlertDialog)
- Auto-focuses input on open
3. InlineEdit (primitive)
Click-to-edit text field that toggles between display and edit modes.
Props:
interface InlineEditProps {
value: string
onSave: (value: string) => void
placeholder?: string // shown when value is empty in display mode
disabled?: boolean
className?: string
}
Behavior:
- Display mode: Shows value as text with subtle edit icon (pencil). Clicking text or icon enters edit mode.
- Edit mode: Input field with current value. Enter saves. Escape cancels (reverts to original value). Blur cancels (same as Escape — prevents accidental saves when clicking away).
- If saved value is empty and placeholder exists, display mode shows placeholder in muted style.
- No save/cancel buttons — Enter saves, Escape/blur cancels (lightweight inline pattern).
Implementation:
- New directory:
src/design-system/primitives/InlineEdit/ - No forwardRef — the component manages its own input internally (the input only exists in edit mode, so a forwarded ref would be null in display mode)
- Manages internal editing state with useState
Admin Pages
Route Structure
/admin → redirects to /admin/rbac
/admin/audit → AuditLog page
/admin/oidc → OidcConfig page
/admin/rbac → UserManagement page (tabs: Users | Groups | Roles)
All pages use the standard AppShell + Sidebar + TopBar layout with breadcrumbs.
Router Integration
Update App.tsx to replace the single /admin route with nested routes:
<Route path="/admin" element={<Navigate to="/admin/rbac" replace />} />
<Route path="/admin/audit" element={<AuditLog />} />
<Route path="/admin/oidc" element={<OidcConfig />} />
<Route path="/admin/rbac" element={<UserManagement />} />
Sidebar Integration
Keep the existing single "Admin" bottom link in the Sidebar. The admin pages handle their own sub-navigation internally via a secondary nav bar at the top of each admin page (links to Audit Log, OIDC, User Management). This avoids cluttering the main sidebar with admin sub-entries.
Barrel Export Updates
- Add
MultiSelectandMultiSelectOptiontype tosrc/design-system/composites/index.ts - Add
ConfirmDialogandConfirmDialogPropstype tosrc/design-system/composites/index.ts - Add
InlineEditandInlineEditPropstype tosrc/design-system/primitives/index.ts
Page: Audit Log (src/pages/Admin/AuditLog/)
Layout: Full-width content area (no split pane).
Sections:
- Header — "Audit Log" title + event count badge
- Filter bar — DateRangePicker (from/to), Input (user), Select (category: INFRA/AUTH/USER_MGMT/CONFIG), Input (search)
- Data table — DataTable with columns: Timestamp (monospace), User, Category (Badge), Action, Target, Result (Badge with success/error color)
- Expandable rows — Clicking a row reveals detail section with IP address, user agent, and JSON detail (CodeBlock)
- Pagination — Pagination component below table
Mock data: ~30 audit events with varied categories, actions, results.
Page: OIDC Config (src/pages/Admin/OidcConfig/)
Layout: Single-column form layout, max-width constrained.
Sections:
- Header — "OIDC Configuration" + Save/Test/Delete buttons
- Behavior — Two Toggle fields (Enabled, Auto Sign-Up) wrapped in FormField
- Provider Settings — FormField-wrapped Inputs for Issuer URI, Client ID, Client Secret (type=password)
- Claim Mapping — FormField-wrapped Inputs for Roles Claim, Display Name Claim with hint text
- Default Roles — Tag list (removable) + Input + Button to add new roles
- Delete — Button (danger) that opens ConfirmDialog
Mock data: Pre-filled form state with sample OIDC config.
Page: User Management (src/pages/Admin/UserManagement/)
Layout: Tabs component at top (Users | Groups | Roles). Each tab has a CSS grid split-pane (roughly 52/48).
Users Tab
- Left pane: Input search + "Add user" Button + scrollable user list. Each item: Avatar + name + provider Badge + meta (email, group path) + Tag list (roles: amber, groups: green, inherited: dashed Badge).
- Inline create form: Appears at top of list when "Add user" clicked. Input fields for username, display, email, password. Cancel/Create buttons.
- Right pane (detail): Large Avatar + InlineEdit (display name) + email + Delete Button. Metadata fields (Status, ID as MonoText, Created). SectionHeader "Group membership" + Tag list (removable) + MultiSelect to add groups. SectionHeader "Effective roles" + Tag list (direct: solid, inherited: dashed with source label) + MultiSelect to add roles.
- Delete: ConfirmDialog (type username to confirm).
Groups Tab
- Left pane: Same pattern — search + "Add group" + group list. Each item: Avatar (square) + name + meta (parent, child count, member count) + role Tags.
- Inline create form: Name input + parent Select (top-level or existing group).
- Right pane: Avatar + InlineEdit (name) + hierarchy label. Metadata (ID, Parent — editable via Select). SectionHeader "Members" + Tag list. SectionHeader "Child groups" + Tag list. SectionHeader "Assigned roles" + removable Tags + MultiSelect. SectionHeader "Hierarchy" with indented tree display.
- Delete: ConfirmDialog.
Roles Tab
- Left pane: Search + "Add role" + role list. Each item: Avatar (square) + name + lock icon if system + meta (description, assignment count) + Tags.
- Inline create form: Name, Description, Scope inputs.
- Right pane: Avatar + role name (non-editable for system roles) + description. Metadata (ID, Scope, Type). SectionHeader "Assigned to groups" (read-only list). SectionHeader "Assigned to users (direct)" (read-only). SectionHeader "Effective principals" with inherited entries in dashed style.
- Delete: ConfirmDialog (only for non-system roles).
Mock data: ~8 users, ~4 groups (with nesting), ~6 roles (including system roles ADMIN, USER). Realistic role/group assignments with inheritance.
Inventory Updates
Add demos for all three new components:
- MultiSelect → CompositesSection: Demo showing multi-select with sample options, displaying selected count
- InlineEdit → PrimitivesSection: Demo showing display/edit toggle with a sample name
- ConfirmDialog → CompositesSection: Demo with a "Delete item" button that opens the dialog
File Structure
src/design-system/composites/MultiSelect/
MultiSelect.tsx
MultiSelect.module.css
MultiSelect.test.tsx
src/design-system/primitives/InlineEdit/
InlineEdit.tsx
InlineEdit.module.css
InlineEdit.test.tsx
src/design-system/composites/ConfirmDialog/
ConfirmDialog.tsx
ConfirmDialog.module.css
ConfirmDialog.test.tsx
src/pages/Admin/
AuditLog/
AuditLog.tsx
AuditLog.module.css
auditMocks.ts
OidcConfig/
OidcConfig.tsx
OidcConfig.module.css
UserManagement/
UserManagement.tsx
UserManagement.module.css
UsersTab.tsx
GroupsTab.tsx
RolesTab.tsx
rbacMocks.ts
Out of Scope
- No backend API integration (static mock data with useState)
- No persistence across page refresh
- No access control / role gating in the example app
- Dashboard tab from RBAC is excluded per user request
- Split pane is page-local CSS, not a design system component