Files
design-system/docs/superpowers/specs/2026-03-24-mock-deviations-design.md
hsiegeln b168d7c867 docs: add EntityList, docs updates, and inventory to spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 11:54:52 +01:00

12 KiB

Mock UI Deviations — Design Spec

Overview

The mock pages in src/pages/ build several UI patterns using raw CSS and inline HTML that should either be promoted into the design system or refactored to use existing components. This spec captures each deviation and its resolution to minimize rework when transitioning to the real application.

Decision Framework

A pattern is promoted to the design system when it:

  • Appears on 2+ pages with the same structure
  • Is visually distinctive and would be inconsistent if reimplemented
  • Will be needed by the real application

A pattern stays in the pages when it is page-specific composition or a one-off layout.


1. KpiStrip — New Composite

Problem: Dashboard, Routes, and AgentHealth each build a custom KPI header strip (~320 lines of duplicated layout code). Same visual structure: horizontal row of cards with colored left border, uppercase label, large value, trend indicator, subtitle, and optional sparkline.

Solution: New composite KpiStrip.

interface KpiItem {
  label: string
  value: string | number
  trend?: { label: string; variant?: 'success' | 'warning' | 'error' | 'muted' }
  subtitle?: string
  sparkline?: number[]
  borderColor?: string           // CSS token, e.g. "var(--success)"
}

interface KpiStripProps {
  items: KpiItem[]
  className?: string
}

Layout:

  • Horizontal flex row with equal-width cards
  • Each card: 3px left border (colored via borderColor, default var(--amber)), padding 16px 20px
  • Card surface: var(--bg-surface), border: var(--border-subtle), radius: var(--radius-md)
  • Label: 11px uppercase, monospace weight 500, var(--text-muted)
  • Value: 28px, weight 700, var(--text-primary)
  • Trend: inline next to value, 11px. Color controlled by trend.variant (maps to semantic tokens). Default 'muted'. The caller decides what color a trend should be — "↑ +12%" on error count is 'error', on throughput is 'success'.
  • Subtitle: 11px, var(--text-secondary)
  • Sparkline: existing Sparkline primitive rendered top-right of card

Note: KpiStrip builds its own card-like containers internally. It does NOT reuse the Card primitive because Card uses a top accent border while KpiStrip needs a left border. The visual surface (bg, border, radius, shadow) uses the same tokens but the layout is distinct.

File location: src/design-system/composites/KpiStrip/

Pages to refactor: Dashboard.tsx, Routes.tsx, AgentHealth.tsx — replace inline KpiHeader functions with <KpiStrip items={[...]} />.


2. SplitPane — New Composite

Problem: Admin RBAC tabs (UsersTab, GroupsTab, RolesTab) each build a custom CSS grid split-pane layout with scrollable list, detail panel, and empty state placeholder.

Solution: New composite SplitPane.

interface SplitPaneProps {
  list: ReactNode
  detail: ReactNode | null       // null renders empty state
  emptyMessage?: string          // Default: "Select an item to view details"
  ratio?: '1:1' | '1:2' | '2:3' // Default: '1:2'
  className?: string
}

Layout:

  • CSS grid with two columns at the specified ratio
  • Left panel: scrollable, var(--bg-surface) background, right border var(--border-subtle)
  • Right panel: scrollable, var(--bg-raised) background
  • Empty state: centered text, var(--text-muted), italic
  • Both panels fill available height (the parent controls the overall height)

File location: src/design-system/composites/SplitPane/

Pages to refactor: UsersTab.tsx, GroupsTab.tsx, RolesTab.tsx — replace custom grid CSS with <SplitPane>.


2b. EntityList — New Composite

Problem: The left-side list panels in UsersTab, GroupsTab, and RolesTab all build the same frame: a search input + "Add" button header, a scrollable list of items (avatar + text + badges), and selection highlighting. Each tab re-implements this frame with ~50 lines of identical structure.

Solution: New composite EntityList.

interface EntityListProps<T> {
  items: T[]
  renderItem: (item: T, isSelected: boolean) => ReactNode
  getItemId: (item: T) => string
  selectedId?: string
  onSelect?: (id: string) => void
  searchPlaceholder?: string      // Default: "Search..."
  onSearch?: (query: string) => void
  addLabel?: string               // e.g. "+ Add user" — omit to hide button
  onAdd?: () => void
  emptyMessage?: string           // Default: "No items found"
  className?: string
}

Layout:

  • Header row: Input (search, with icon) on the left, Button variant="secondary" size="sm" (add) on the right. Header hidden when both onSearch and onAdd are omitted.
  • Scrollable list below header, var(--bg-surface) background
  • Each item: clickable row with var(--bg-hover) on hover, var(--amber-bg) + left amber border when selected
  • Items rendered via renderItem — the component provides the clickable row wrapper, the caller provides the content
  • role="listbox" on the list, role="option" on each item for accessibility
  • Empty state: centered emptyMessage text when items is empty

Typical item content (provided by caller via renderItem):

  • Avatar + name + subtitle + badge tags — but this is not prescribed by EntityList. The component is agnostic about item content.

Combined usage with SplitPane:

<SplitPane
  list={
    <EntityList
      items={filteredUsers}
      renderItem={(user, isSelected) => (
        <>
          <Avatar name={user.name} size="sm" />
          <div>
            <div>{user.name}</div>
            <div>{user.email}</div>
            <div>{user.roles.map(r => <Badge key={r} label={r} />)}</div>
          </div>
        </>
      )}
      getItemId={(u) => u.id}
      selectedId={selectedId}
      onSelect={setSelectedId}
      searchPlaceholder="Search users..."
      onSearch={setSearchQuery}
      addLabel="+ Add user"
      onAdd={() => setAddDialogOpen(true)}
    />
  }
  detail={selectedUser ? <UserDetail user={selectedUser} /> : null}
/>

File location: src/design-system/composites/EntityList/

Pages to refactor: UsersTab.tsx, GroupsTab.tsx, RolesTab.tsx — replace custom list rendering with <EntityList>. Combined with SplitPane, each tab reduces from ~200 lines to ~50 lines of domain-specific render logic.


3. Refactor AgentHealth Instance Table to DataTable

Problem: AgentHealth builds instance tables using raw HTML <table> elements instead of the existing DataTable composite.

Solution: Refactor to use DataTable with column definitions and custom cell renderers. No design system changes needed.

Refactor scope:

  • Replace <table> blocks in AgentHealth.tsx (~60 lines) with <DataTable> using flush prop
  • Define columns with render functions for State (Badge) and StatusDot columns
  • Remove associated table CSS from AgentHealth.module.css

4. LogViewer — New Composite

Problem: AgentInstance renders log entries as custom HTML with inline styling — timestamped lines with severity levels in monospace.

Solution: New composite LogViewer.

interface LogEntry {
  timestamp: string
  level: 'info' | 'warn' | 'error' | 'debug'
  message: string
}

interface LogViewerProps {
  entries: LogEntry[]
  maxHeight?: number | string    // Default: 400
  className?: string
}

Layout:

  • Scrollable container with max-height, var(--bg-inset) background, var(--radius-md) border-radius
  • Each line: flex row with timestamp (muted, monospace, 11px) + level badge + message (monospace, 12px)
  • Level badge colors: info=var(--running), warn=var(--warning), error=var(--error), debug=var(--text-muted)
  • Level badge: uppercase, 9px, var(--font-mono), pill-shaped with tinted background
  • Auto-scroll to bottom on new entries; pauses when user scrolls up; resumes on scroll-to-bottom

File location: src/design-system/composites/LogViewer/

Pages to refactor: AgentInstance.tsx — replace custom log rendering with <LogViewer entries={logs} />.


5. StatusText — New Primitive

Problem: Dashboard and Routes use inline style={{ color: 'var(--error)', fontWeight: 600 }} for status values like "BREACH", "OK", colored percentages.

Solution: New primitive StatusText.

interface StatusTextProps {
  variant: 'success' | 'warning' | 'error' | 'running' | 'muted'
  bold?: boolean                 // Default: false
  children: ReactNode
  className?: string
}

Styling:

  • Inline <span> element
  • Color mapped to semantic tokens: success=var(--success), warning=var(--warning), error=var(--error), running=var(--running), muted=var(--text-muted)
  • bold adds font-weight: 600
  • Inherits font-size from parent

File location: src/design-system/primitives/StatusText/

Pages to refactor: Dashboard.tsx, Routes.tsx — replace inline style attributes with <StatusText>.


6. Card Title Extension

Problem: Routes page wraps charts in custom divs with uppercase titles. The existing Card component has no title support.

Solution: Add optional title prop to existing Card primitive.

interface CardProps {
  children: ReactNode
  accent?: string                // Existing
  title?: string                 // NEW
  className?: string             // Existing
}

When title is provided:

  • Renders a header div inside the card, above children
  • Title: 11px uppercase, var(--font-mono), weight 600, var(--text-secondary), letter-spacing 0.5px
  • Separated from content by 1px var(--border-subtle) bottom border and 12px padding-bottom
  • Content area gets 16px padding-top

File location: Modify existing src/design-system/primitives/Card/Card.tsx

Pages to refactor: Routes.tsx — replace custom chart wrapper divs with <Card title="Throughput (msg/s)">.


Implementation Priority

  1. KpiStrip — highest impact, 3 pages, ~320 lines eliminated
  2. StatusText — smallest scope, quick win, unblocks cleaner page code
  3. Card title — small change to existing component, unblocks Routes cleanup
  4. SplitPane + EntityList — 3 admin tabs, clean pattern. Build together since EntityList is the natural content for SplitPane's list slot.
  5. LogViewer — 1 page but important for real app
  6. AgentHealth DataTable refactor — pure page cleanup, no DS changes

Testing

All new components tested with Vitest + React Testing Library, co-located test files. Page refactors verified by running existing tests + visual check that pages look identical before and after.

Barrel Exports

New components added to respective barrel exports:

  • src/design-system/primitives/index.ts — StatusText
  • src/design-system/composites/index.ts — KpiStrip, SplitPane, EntityList, LogViewer

Documentation Updates

COMPONENT_GUIDE.md

Add entries for each new component to the appropriate decision trees:

  • Data Display section: Add KpiStrip — "Use KpiStrip for a row of summary metrics at the top of a page (exchanges, error rate, latency, etc.)"
  • Data Display section: Add LogViewer — "Use LogViewer for scrollable log output with timestamped, severity-colored entries"
  • Layout section: Add SplitPane — "Use SplitPane for master/detail layouts: selectable list on the left, detail view on the right"
  • Data Display section: Add EntityList — "Use EntityList for searchable, selectable lists of entities (users, groups, roles, etc.). Combine with SplitPane for CRUD management screens."
  • Text & Labels section: Add StatusText — "Use StatusText for inline colored status values (success rates, SLA status, trend indicators). Use StatusDot for colored dot indicators."
  • Card section: Document new title prop — "Pass title to Card for a titled content container (e.g., chart cards). Title renders as an uppercase header with separator."

Inventory Page

Add demos for each new component to src/pages/Inventory/sections/:

  • CompositesSection.tsx: Add KpiStrip, SplitPane, EntityList, LogViewer demos with realistic sample data
  • PrimitivesSection.tsx: Add StatusText demo showing all variants
  • Card demo: Update existing Card demo to show the title prop variant

Each demo follows the existing DemoCard pattern with id anchors, and nav entries are added to Inventory.tsx.