Skip to content

ADR-0035: Design System and Component Library

Status: Proposed Date: 2026-05-26 Supersedes: ADR-0013 (zero-component-library UI) — for product surfaces Depends on: ADR-0033 (renders NormalizedState) Related: ADR-0031, ADR-0034, ADR-0039

Context

ADR-0013 chose zero external component dependencies: no Radix, no shadcn, no headless UI. The rationale was that Studio is a power-user tool with one primary user, and custom components keep bundle size minimal and control total.

That assumption no longer holds. The frontend now requires:

  • Dialogs for confirmation actions (ADR-0031 entity actions)
  • Dropdown menus for row actions in tables
  • Popovers for filter chips and column pickers
  • Comboboxes for project scope selection and search
  • Tooltips for status badges and graph nodes
  • Command palette for keyboard-first navigation
  • Focus traps for modals and drawers
  • Roving tabindex for table row navigation

Each of these requires correct ARIA roles, keyboard handling, focus management, and screen reader announcements. Building them from scratch is a multi-week effort that produces worse accessibility than existing headless libraries designed specifically for this purpose.

ADR-0013 itself noted: "This trade-off would not be acceptable for a public-facing product." The product direction has now changed — Studio is becoming the operational interface, designed for use by development teams of varying sizes.

Decision

Component architecture

components/ui/      — generic primitives (shadcn/ui source-owned)
components/lion/    — domain-aware design system
features/*/         — product surfaces (import from lion/, never from Radix directly)

This ADR is the canonical owner of components/lion/. Future feature ADRs that introduce new domain components MUST amend this ADR's component list (PR against §Domain components below). The catalog must remain discoverable in one place; scattering definitions across feature ADRs is the failure mode this rule prevents.

Primitive layer: shadcn/ui

Use shadcn/ui as source-owned primitives. shadcn is not a dependency — it generates component source files that we own and modify. The underlying accessibility primitives come from Radix.

Adopted primitives:

Primitive Source Use case
Button shadcn All buttons
Badge shadcn Metadata labels (model, source, version)
Dialog shadcn/Radix Confirmation actions, entity details
DropdownMenu shadcn/Radix Row actions, context menus
Popover shadcn/Radix Filter chips, column picker
Tooltip shadcn/Radix Status explanations, graph node details
Command shadcn/Radix Cmd/Ctrl-K command palette
Tabs shadcn/Radix Inspector panels, detail page sections
Sheet shadcn/Radix Mobile sidebar, inspector drawer

NOT adopted (use existing custom):

Component Reason
Table TanStack Table + custom markup for LionDataTable
Form inputs Existing custom inputs are sufficient for current scope
Toast Existing Toast provider works; migrate later if needed

Domain components: components/lion/

These encode Lion Studio's operational semantics:

lion/
  status-badge.tsx        — single status with tone + icon
  state-stack.tsx          — compound state: "Failed · Infra OK · Trace present"
  severity-indicator.tsx   — left-border severity accent
  data-table/
    lion-data-table.tsx    — TanStack Table wrapper with toolbar, filters, URL sync
    table-toolbar.tsx      — search, filter chips, column picker, density toggle
    table-pagination.tsx   — cursor pagination
  object-header.tsx        — universal detail page header (ADR-0031)
  object-lineage.tsx       — clickable breadcrumb chips
  attention-item.tsx       — attention queue row
  data-freshness-badge.tsx — "Live · verified 8s ago"
  section-boundary.tsx     — per-section error boundary
  connection-banner.tsx    — SSE disconnect indicator
  knowledge/
    knowledge-lens.tsx          — contextual claim listing for an entity scope
    claim-card.tsx              — single claim with status, confidence, evidence count
    claim-status-badge.tsx      — observed | inferred | hypothesis | verified | disputed | superseded
    evidence-trail.tsx          — expandable list of EvidenceRefs with source links
    confidence-meter.tsx        — visual confidence indicator (0.0–1.0)

The knowledge/* components implement the behavioral spec from ADR-0039 §"Product Implications". This ADR owns the catalog and rendering rules; ADR-0039 owns the data model and behavioral semantics.

Rule: No feature component imports Radix directly. If a shared primitive doesn't exist yet, add it to components/ui/ first.

Table engine: TanStack Table

TanStack Table provides headless table logic (sorting, filtering, pagination, column visibility, row selection). Lion Studio provides the markup and Tailwind styling via LionDataTable.

Relationship to shadcn's DataTable:

  • shadcn's Table component provides markup primitives (<Table>, <TableHeader>, <TableRow>, <TableCell>)
  • LionDataTable wraps TanStack Table logic + shadcn Table markup + Lion-specific toolbar, URL sync, density modes, and row actions

Status badge contract

Generic Badge (shadcn) is for metadata labels: codex/gpt-5.5, local, v1.0.0. Never for operational status.

Operational status uses domain components:

<StatusBadge tone="danger" icon={XCircle}>Failed</StatusBadge>

<StateStack
  outcome="failed"
  health="ok"
  delivery="present"
/>
// Renders: "Failed · Infra OK · Trace present"

Status rendering rules:

  • Never use color alone — always icon + text + color
  • Dark mode does not change status semantics
  • Critical/warning states always have icons
  • Reduced motion disables pulse/spin animations

Semantic color tokens

Extend existing CSS custom properties (which already exist in globals.css) to use the --ls- prefix for the severity system:

:root {
  --ls-critical-bg: ...;
  --ls-critical-border: ...;
  --ls-critical-text: ...;
  --ls-critical-accent: ...;
  /* warning, info, success, neutral variants */
  --ls-focus-ring: ...;
}
.dark {
  /* dark variants of all above */
}

Components reference semantic tokens, not raw Tailwind colors:

Good: bg-[var(--ls-critical-bg)]
Bad:  bg-red-50

Prefix decision: --ls- is the canonical design-token namespace. Lion Studio uses these directly. Custom themes may override values in theme-specific CSS files. Token NAMES stay stable across themes; only their VALUES vary.

The existing design token system in globals.css already uses CSS custom properties with dark mode via .dark class. This ADR extends that system with the severity-specific tokens, not replaces it.

Semantic palette: status colors

The severity → tone mapping in ADR-0033 determines color category. The concrete color values are defined here as the canonical palette:

Tone Light mode Dark mode Usage
danger red-700 / red-100 bg red-300 / red-950 bg failed, aborted, dead, missing
warning amber-700 / amber-100 bg amber-300 / amber-950 bg timed_out, partial, stalled, disputed
info blue-700 / blue-100 bg blue-300 / blue-950 bg running, due, observed
success green-700 / green-100 bg green-300 / green-950 bg succeeded, completed, merged, verified
neutral gray-700 / gray-100 bg gray-300 / gray-900 bg cancelled, skipped, unknown, hypothesis

These map directly to CSS custom properties: --ls-danger-text, --ls-danger-bg, --ls-warning-text, etc. Components NEVER reference raw Tailwind color classes for status; they reference these semantic tokens.

Knowledge-specific tones follow the same palette:

  • observed → info (assumes clean evidence)
  • inferred → info (with reduced opacity to signal lower certainty)
  • hypothesis → neutral (typed honestly, low confidence)
  • verified → success
  • disputed → warning
  • superseded → neutral (archived, not failed)

This palette is the source of truth. Issues #1178, #1179 (filter chip colors, graph node colors) MUST consume from this palette, not invent local colors.

Dark mode

Already supported via darkMode: "class" in tailwind.config.ts. Extend with:

  • Pre-hydration inline script to apply .dark before React renders (prevents flash of wrong theme)
  • Theme preference: light | dark | system, stored in localStorage
  • System preference via prefers-color-scheme media query
  • Theme toggle in nav showing resolved state: "System · Dark"

Accessibility target: WCAG 2.2 AA

Non-negotiable rules:

  • Never use color alone to communicate state (every status has icon + text + color)
  • Focus state visible on every interactive element
  • Keyboard can reach every action
  • Tables use semantic <table> markup with aria-sort
  • Live updates use aria-live (assertive for critical, polite for info)
  • Graph has keyboard navigation AND table fallback
  • Reduced motion: prefers-reduced-motion disables all animations

Verification strategy (addresses issue #1020 — 47 a11y findings):

Layer Tool Gate
Static (CI) eslint-plugin-jsx-a11y Block merge on error level
Component (CI) @testing-library/jest-dom + jest-axe Each components/lion/* has an a11y test
Page (manual + CI) Lighthouse / Axe DevTools Score ≥95 on Dashboard, Runs, Show detail
Live Manual screen reader sweep (VoiceOver, NVDA) Quarterly, owned by frontend lead

Component-level a11y test pattern:

import { axe } from "jest-axe";

it("StatusBadge has no axe violations", async () => {
  const { container } = render(<StatusBadge tone="danger" icon={XCircle}>Failed</StatusBadge>);
  expect(await axe(container)).toHaveNoViolations();
});

Every component in components/lion/ MUST ship with an a11y test. Components in components/ui/ (shadcn primitives) inherit Radix's verified a11y but get smoke tests for our customizations.

Keyboard-first interaction

Scoped shortcut system with priority ordering:

Dialog/modal > Editor/input > Graph > Table > Page > Global

Core shortcuts (ship in v1):

Shortcut Action
Cmd/Ctrl-K Command palette
? Keyboard help
/ Focus search
g d Go to Dashboard
g r Go to Runs
g s Go to Shows
j/k Table row navigation
Enter Open selected
Esc Clear selection / close overlay

Shortcut rules:

  • No printable shortcuts fire inside input/textarea/contenteditable
  • Cmd/Ctrl-K always opens command palette unless modal captures it
  • Escape closes innermost overlay

Error boundaries

Per-section on dashboard and detail pages. Page-level only when primary entity cannot load.

System health fails but runs load:
  → SystemHealthPanel shows inline error
  → AttentionQueue adds "System health unavailable" item
  → Rest of dashboard renders normally

All API calls fail:
  → Full-page "Backend unreachable" with last cached timestamp

Loading states

Section-level skeletons. Never blank an entire page during background refresh. Keep last known data with "verifying..." freshness indicator during revalidation.

Consequences

Positive

  • Correct accessibility from day one via Radix primitives
  • Keyboard-first interaction for operator efficiency
  • Consistent status rendering across all surfaces via domain components
  • Source-owned components (shadcn) — no locked dependencies
  • Dark mode with no flash

Negative

  • Radix primitives add ~15-25KB (gzipped). Combined with ADR-0034's TanStack Query (~40KB) + Zustand (~2KB), total new bundle weight is ~60KB gzipped.
  • Migration effort: existing custom components need gradual replacement
  • ADR-0013's "zero dependency" principle is explicitly abandoned for product surfaces. Preserved as a constraint for any future ultra-light embed mode.
  • Catalog ownership concentrated in this ADR — feature ADRs MUST amend rather than fork. This creates merge contention in component-heavy phases; mitigated by clear catalog entries and small per-component PRs.

Alternatives Considered

Alternative Why Rejected
Continue zero-dependency (ADR-0013) Accessibility debt grows with every new interactive component; ADR-0013 itself flagged this
MUI / Ant Design / Chakra Opinionated styling conflicts with existing Tailwind design system; large bundle
Headless UI (Tailwind Labs) Fewer primitives than Radix; no command palette; less active maintenance
Build everything custom with correct ARIA Multi-week effort per component; will produce worse a11y than battle-tested Radix

References

  • ADR-0013 — Zero Component-Library UI (superseded by this ADR for product surfaces)
  • ADR-0031 — Entity Header Pattern (consumes design system)
  • ADR-0033 — Unified Entity State Model (status/severity/tone semantics)
  • ADR-0034 — Frontend Data & State Architecture
  • ADR-0039 — Knowledge Substrate (claim/evidence components)
  • shadcn/ui documentation
  • Radix Primitives documentation
  • WCAG 2.2 — Contrast (Minimum), Focus Visible, Target Size
  • TanStack Table documentation
  • eslint-plugin-jsx-a11y, jest-axe — accessibility CI tooling
  • Issue #1020 — a11y baseline (driven by verification strategy above)
  • Issue #1168, #1178, #1179 — design system polish addressed via semantic palette