Files
design-system/docs/superpowers/specs/2026-03-18-admin-pages-design.md
hsiegeln df5450925e docs: address spec review feedback for admin pages design
- 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>
2026-03-18 22:46:45 +01:00

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 onChange with 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 have role="option" with aria-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 confirmText exactly
  • Input clears on open
  • Enter submits when enabled; Escape closes
  • Confirm button uses danger/warning/info variant styling (matches AlertDialog pattern)
  • When loading is 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 MultiSelect and MultiSelectOption type to src/design-system/composites/index.ts
  • Add ConfirmDialog and ConfirmDialogProps type to src/design-system/composites/index.ts
  • Add InlineEdit and InlineEditProps type to src/design-system/primitives/index.ts

Page: Audit Log (src/pages/Admin/AuditLog/)

Layout: Full-width content area (no split pane).

Sections:

  1. Header — "Audit Log" title + event count badge
  2. Filter bar — DateRangePicker (from/to), Input (user), Select (category: INFRA/AUTH/USER_MGMT/CONFIG), Input (search)
  3. Data table — DataTable with columns: Timestamp (monospace), User, Category (Badge), Action, Target, Result (Badge with success/error color)
  4. Expandable rows — Clicking a row reveals detail section with IP address, user agent, and JSON detail (CodeBlock)
  5. 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:

  1. Header — "OIDC Configuration" + Save/Test/Delete buttons
  2. Behavior — Two Toggle fields (Enabled, Auto Sign-Up) wrapped in FormField
  3. Provider Settings — FormField-wrapped Inputs for Issuer URI, Client ID, Client Secret (type=password)
  4. Claim Mapping — FormField-wrapped Inputs for Roles Claim, Display Name Claim with hint text
  5. Default Roles — Tag list (removable) + Input + Button to add new roles
  6. 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