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
Tablecomponent provides markup primitives (<Table>,<TableHeader>,<TableRow>,<TableCell>) LionDataTablewraps 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→ successdisputed→ warningsuperseded→ 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
.darkbefore React renders (prevents flash of wrong theme) - Theme preference:
light | dark | system, stored in localStorage - System preference via
prefers-color-schememedia 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 witharia-sort - Live updates use
aria-live(assertive for critical, polite for info) - Graph has keyboard navigation AND table fallback
- Reduced motion:
prefers-reduced-motiondisables 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-Kalways opens command palette unless modal captures itEscapecloses 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