Brand Identity & UX Design System¶
This page carries two things: the words SynthOrg uses about itself, which every other page inherits, and the visual system the dashboard is built from. The voice comes first, because a page can be accurate about the code and still be wrong about the product.
What SynthOrg Is¶
Where one agent stops, an organisation starts. SynthOrg is a synthetic one: it staffs the roles, splits the work into jobs that stand alone, builds them in parallel, each in its own workspace, and sends each one to a reviewer who had no hand in it. On your hardware, against models you choose.
The order is load-bearing: problem, then mechanism, then where it runs. Never lead with the mechanism. The product name carries the answer rather than the problem, so the opening line has to supply what that answer is an answer to, and the line after it says what a synthetic organisation does rather than leaving a reader to infer it from a name they have just met.
The binding constraint is decomposition quality, not agent supply. The parallel tree is not a speed feature and must never be sold as one. A single agent does one thing at a time, so the twentieth damages the first; a tree does not have that failure mode, provided the merges hold. Making the parts genuinely independent is the hard problem.
The Three Claims, and How Far Each Goes¶
It fans out. Work is decomposed recursively into units that can be built
independently, built concurrently, each in its own git worktree, and assembled
bottom-up. Never state a size the system handles: the decomposition ceiling is
being measured and has no answer yet. The isolation to name is the workspace,
never a container per part: the shipped sandbox default is subprocess
(tools/sandbox/sandboxing_config.py), and where an operator selects Docker a
container belongs to an agent and is reused across every command and task that
agent runs (tools/sandbox/lifecycle/config.py).
Each part is checked by something that did not write it. The reviewer is structurally prevented from being the author, in the service layer, the model binding, and a database constraint. The claim stops there. It does not mean the work is correct, and nothing binds the two to different model families, so a blind spot they share is one the pair cannot see. The narrow version is the true one and it is worth more than the broad one.
It runs on your hardware. Self-hosted and provider-agnostic, local models included, with no SynthOrg service in the path. State it as a property, never as a boast.
What Is Not Sold¶
Organisational realism as the reason the output is good. That position is closed (see Research and Prior Art). Roles, departments, staffing and the approval gate are shipped, working machinery, pages documenting them must stay accurate, and the vocabulary is the product's own, so a headline may use it. What no page may do is argue that simulating an organisation is why the work comes out well, or describe the org as something to watch rather than something that decides who builds and who checks.
Voice Rules¶
A page that breaks one of these is wrong, not merely off-tone.
| Rule | What it means |
|---|---|
| Never promise an outcome | No page says the reader will get working software. Every claim is about how the system is built and behaves. The loop has been driven live twelve times and has never reached the assembly stage |
| Pre-alpha, stated plainly | Not softened, not implied, not left below the fold |
| No autonomy claims | Supervision is a property of the design, not a stage the product has graduated from |
| Bound the checking claim | The thing that checks the work is not the thing that produced it. Anything broader says more than the code supports; model-family independence in particular is a recommendation the code does not implement |
| Quote no build size | The decomposition ceiling has no measured answer, so a figure invented for it is a claim about an open question |
| Mark intent as intent | Never write a thing that is not built in the present tense, and never hedge one that ships |
House rules on every word: British English; no em-dashes; current state only; no
unverified number; and no LLM vendor privileged where a provider or model is
configured, dispatched to or illustrated (example-provider,
example-{basic,capable,expert}-001).
Design Direction¶
Chosen direction: Warm Ops, a warm, approachable aesthetic with balanced density and spring-physics interactions, combined with semantic state-driven colour encoding where every colour communicates system status rather than serving as decoration.
Why this direction: warmth makes an operations dashboard approachable without losing professionalism. Tying colour exclusively to state (green = rising, amber = attention, red = critical) gives operators instant comprehension of system health. The brand accent is a warm soft blue, deliberately neutral so that semantic state colours dominate the visual hierarchy. Orange and amber mean "attention needed", not "this is SynthOrg".
What was rejected and why: a cool blue-cyan palette (data centre aesthetic) was too generic to tell apart from any other monitoring dashboard. A neutral grey palette (no hue) lacked enough identity to be recognisable. A high-energy violet/purple palette was visually fatiguing over a long working session. Each scored well on individual criteria and failed the combination of distinct identity plus sustained usability.
Design influences: Linear (clean layout, balanced density), Vercel (status-first design), Dust.tt (warm approachability), Grafana (data density as a user preference).
Colour System¶
Semantic Colour Tokens¶
Colours are state-driven, not decorative. Every coloured element answers: "what is the system telling me?"
| Token | Purpose | Example hex | When to use |
|---|---|---|---|
accent |
Brand, interactive elements, links, focus rings, active nav | #38bdf8 (warm soft blue, tunable) |
Default state, clickable things, brand identity |
accent-dim |
Muted brand, secondary interactive, onboarding | #0ea5e9 (fixed per-theme --so-accent-dim) |
Hover states, secondary info, less prominent interactive |
success |
Rising, improving, healthy, completed | #10b981 (emerald) |
Metrics trending up, tasks completed, agents active |
warning |
Declining, degrading, attention needed | #f59e0b (amber) |
Metrics trending down, budget nearing limit, stale tasks |
danger |
Critical, error, immediate action | #ef4444 (red) |
Agent errors, budget exceeded, failed tasks |
text-primary |
Main content text | #e2e8f0 |
Headings, values, primary content |
text-secondary |
Supporting text | #94a3b8 |
Labels, descriptions, secondary info |
text-muted |
Least prominent text | #8b95a5 |
Timestamps, metadata, disabled items |
bg-base |
Page background | #0a0a12 |
Deepest background layer |
bg-surface |
Sidebar, elevated surfaces | #0f0f1a |
Sidebar, panels, raised areas |
bg-card |
Card backgrounds | #13131f |
All card containers |
bg-card-hover |
Card hover state | #181828 |
Card background on mouse-over |
border |
Default borders | #1e1e2e |
Card borders, dividers |
border-bright |
Interactive/hover borders | #2a2a3e |
Focus rings, hover states |
Dynamic Colour Assignment¶
Metric cards, sparklines, and trend indicators use colours dynamically based on direction of change:
| Data state | Colour token | Rationale |
|---|---|---|
| Improving / rising | success |
Green = things getting better |
| Stable / normal | accent or text-muted |
Neutral; no action needed |
| Declining / degrading | warning |
Amber = attention warranted |
| Critical / threshold | danger |
Red = act now |
This ensures operators instantly understand system state from colours alone, without reading values. The same metric card shows green when tasks are completing faster and amber when they are slowing down.
How to Add a New Colour Palette¶
Every colour in the dashboard resolves through a --so-* custom property declared in web/src/styles/design-tokens.css. Warm Ops is :root; each other palette is a class on <html> that overrides only the properties it changes. To add one:
- Add a
.theme-<name>block todesign-tokens.cssoverriding the--so-*properties that differ from:root - Add
<name>to theColorPaletteunion and theCOLOR_PALETTESarray inweb/src/stores/theme.ts, which derives the class list from it - Give it a label in the
THEME_META.PALETTEmap inweb/src/components/layout/AppLayout.tsx, which is what the command palette and theme popover show
No component code changes. The five palettes (Warm Ops, Ice Station, Stealth, Signal, Neon) each override the accent pair and a handful of surfaces; everything else inherits from :root.
Typography¶
Chosen Pairing¶
| Role | Font | Usage |
|---|---|---|
| Monospace | Geist Mono (via @fontsource, self-hosted) | Data values, code, metrics, timestamps, agent names |
| Sans-serif | Geist Sans (via @fontsource, self-hosted) | Labels, descriptions, UI text, headings |
Rationale: Geist was designed by Vercel specifically for developer dashboards. Excellent readability at small sizes, clean number rendering in mono, professional but not clinical in sans.
Typography is a theme axis: other pairings (JetBrains Mono + Inter, IBM Plex Mono + IBM Plex Sans) are available and can be selected independently of colours.
Self-Hosted Fonts¶
All fonts are bundled via @fontsource packages. No external CDN (Google Fonts) dependencies. This ensures:
- No privacy concerns from third-party font loading
- Consistent rendering regardless of network
- Faster first contentful paint (no font fetch waterfall)
Density¶
Density is an independent user preference, not tied to theme colours.
| Level | Padding | Section gap | Grid gap | Metric size | Body size | Use case |
|---|---|---|---|---|---|---|
| Dense | p-3 (12px) |
gap-3 (12px) |
gap-3 (12px) |
text-2xl |
text-xs |
Power users, large monitors, data-heavy workflows |
| Balanced (default) | p-4 (16px) |
gap-4 (16px) |
gap-4 (16px) |
text-3xl |
text-sm |
General use, comfortable reading distance |
| Medium | p-[14px] |
gap-4 (16px) |
gap-4 (16px) |
text-2xl |
text-xs |
Slightly tighter than balanced |
| Sparse | p-5 (20px) |
gap-6 (24px) |
gap-6 (24px) |
text-3xl |
text-sm |
Presentation mode, low information density tasks |
Token usage: Components must use density-aware token classes (
p-card,space-y-section-gap,gap-section-gap,gap-grid-gap) instead of the raw Tailwind utilities shown above. The raw values are what each token resolves to at each density level.
How to Add a New Density Level¶
Same shape as a palette. Balanced is :root; each other level is a .density-<name> class in design-tokens.css overriding the --so-density-* properties, registered in the Density union and DENSITIES array in web/src/stores/theme.ts and labelled in THEME_META.DENSITY. Components consume the density tokens (p-card, gap-section-gap, gap-grid-gap), so no component changes.
Animation¶
Animation is an independent user preference, controlling motion intensity.
Each profile resolves to one config in useAnimationPreset(): a primary transition (spring, used for modals, panels and card interactions, which need not be a spring), a tween for hover and colour changes, a stagger delay, and whether layout animations run at all. Components read the hook rather than reaching into lib/motion.ts directly.
| Profile | Primary transition | Tween | Stagger | Layout animations | Use case |
|---|---|---|---|---|---|
| Minimal | tweenFast (150ms) |
150ms ease-out | None | Off | Distraction-free, close to reduced motion |
| Spring | springDefault |
tweenDefault (200ms) |
30ms | On | Playful, responsive feel |
| Instant | Instant (zero duration) | Instant | None | Off | Maximum performance, zero-latency feel |
| Status-driven | tweenDefault (200ms) |
tweenDefault |
20ms | On | Animation earns attention; only what changed moves |
| Aggressive | springBouncy |
tweenDefault |
50ms | On | High-energy demo and presentation mode |
Recommended default: status-driven. Animation should communicate a state change, not decorate. Static elements stay still; only things that changed move.
Page transitions are deliberately outside this axis. Every profile gets the same short opacity cross-fade, because on a dense dashboard any horizontal slide reads as a layout shift rather than a transition.
Sidebar¶
Sidebar mode is an independent user preference.
| Mode | Behaviour | Width | Suits |
|---|---|---|---|
| Rail | Icons only, pinned collapsed; labels surface on hover | 56px | Maximum content area while keeping nav reachable |
| Collapsible (default) | Expanded by default, can collapse to an icon rail. Remembers user preference. | 220px / 56px | Most users (full nav when needed, compact when focused) |
| Hidden | Hamburger toggle, content gets full width | 240px (overlay) | Maximum content area, presentation |
| Persistent | Expanded with labels, no collapse toggle; desktop-sm collapses it |
220px / 56px | High-interactivity workflows, many nav items |
| Compact | Expanded with labels in a narrower column; desktop-sm collapses it |
180px / 56px | Small screens, secondary monitors |
Rail is the only mode that is always icon-only; collapsible also hides labels at
full desktop width whenever the user collapses it. The desktop-sm
breakpoint pins every mode collapsed regardless of preference, so compact and
persistent render the icon rail there too, at the collapsed width rather than
their own. Above that breakpoint the two keep labels and differ by column width
alone; neither renders the collapse toggle, which belongs to collapsible.
Notification badges render in every mode that shows labels, not only persistent.
Persistence¶
Every appearance preference is backend-owned. The five theme axes live in the appearance settings namespace and the sidebar collapse state in dashboard.sidebar_collapsed; the stores hydrate from the API on mount and write each change straight back. The stores hold the hydrated values in memory for the session and nothing else keeps a copy, because the dashboard is a pure API consumer and a preference persisted only in the browser is a preference no other client can see.
Theme Architecture¶
Independent Axes¶
The theme system has 5 orthogonal axes that users can configure independently:
This gives users full control without combinatorial explosion in theme definitions. A user can run "warm blue colours + dense layout + IBM Plex fonts + minimal animation + compact sidebar" without any custom theme code.
Implementation Pattern¶
flowchart TD
TS["Theme store (Zustand, hydrated from the appearance settings namespace)"]
TS --> P1["Applies one class per axis to the html element (theme-*, density-*, typography-*, animation-*, sidebar-*)"]
P1 --> P2["Each class overrides the --so-* custom properties it changes; the defaults live on :root"]
P2 --> P3["global.css maps --so-* onto Tailwind theme variables with @theme inline"]
P3 --> P4["Components use the resulting utilities (text-accent, bg-card, p-card, gap-section-gap)"]
TS --> P5["Animation preference is read through useAnimationPreset; sidebar mode selects the sidebar rendering"]
Because the mapping is @theme inline, the compiled utilities resolve as var(--so-*) references rather than baked values, which is what lets a class swap on <html> restyle the whole dashboard at runtime.
Critical Implementation Note: Tailwind v4 CSS Layers¶
When using Tailwind v4 with @import "tailwindcss", all custom CSS resets MUST be inside @layer base. Tailwind v4 uses CSS cascade layers, and unlayered styles (like * { margin: 0; padding: 0; }) have higher priority than layered utilities, silently overriding all spacing, padding, margin, and gap utilities.
/* WRONG -- breaks all Tailwind spacing utilities */
* { margin: 0; padding: 0; }
/* CORRECT -- respects Tailwind cascade layers */
@layer base {
* { margin: 0; padding: 0; }
}
An unlayered reset is hard to diagnose because the utilities still appear in the generated CSS and simply have no visual effect.
Dark Mode¶
Dark mode only. There is no light mode. All colour tokens assume dark backgrounds, and WCAG AA contrast ratios are validated against the dark card and surface backgrounds.
Accessibility¶
- WCAG AA contrast minimum on all text (4.5:1 on backgrounds, 3:1 for large text)
prefers-reduced-motionsupported:AnimatedPresenceusesreducedPageVariants(opacity-only fade), skeleton shimmer disabled via CSS media query, Motion'suseReducedMotion()hook used for runtime detection- Keyboard navigation for all interactive elements
aria-hidden="true"on decorative icons- Escape key closes overlays/drawers
- Storybook a11y enforcement:
parameters.a11y.test: 'error'set globally in.storybook/preview.tsx; all stories fail on WCAG violations, catching regressions at component development time
Storybook Tooling (v10)¶
The component development environment uses Storybook 10 with native type-safe configuration:
- Config:
defineMain(from@storybook/react-vite/node) anddefinePreview(from@storybook/react-vite) for full TypeScript inference - Addons:
@storybook/addon-docs(autodocs),@storybook/addon-a11y(WCAG testing) andmsw-storybook-addon. Essentials (backgrounds, controls, viewport, actions) and interactions are built into core - Backgrounds:
initialGlobals.backgrounds.value = 'dark'selects the singlebackgrounds.options.darkentry, whose value is the same#0a0a12the--so-bg-basetoken carries, so stories render against the real brand background - Decorator: Global dark-mode wrapper (
div.dark.bg-background.p-4.text-foreground) applies our design tokens to all stories - API mocking: MSW (Mock Service Worker) via
msw-storybook-addon, registered inpreview.tsxwith an explicit worker whoseonUnhandledRequestisbypass, so a story that mocks nothing stays silent instead of warning on every request. Stories declare handlers viaparameters.msw.handlersusing pre-built handler arrays fromweb/src/mocks/handlers/. All responses use theApiResponse<T>envelope via theapiSuccess()helper
Component Inventory¶
The following shared components live in web/src/components/ui/ and form the building blocks for all dashboard pages. Always compose pages from these; never recreate equivalent functionality inline.
Core Components¶
| Component | File | Props | Purpose |
|---|---|---|---|
StatusBadge |
status-badge.tsx |
status, label?, pulse? |
Status indicator dot. label is a boolean that toggles display of the built-in status text (not a custom string). Maps AgentRuntimeStatus to semantic colours via getStatusColor(). |
MetricCard |
metric-card.tsx |
label, value, change?, sparklineData?, progress?, subText? |
Numeric KPI display with optional sparkline, change badge (+/-%), and progress bar. |
Sparkline |
sparkline.tsx |
data, color?, width?, height?, animated? |
Pure SVG sparkline with gradient fill and animated draw. color defaults to var(--so-accent). Standalone or inside MetricCard. |
SectionCard |
section-card.tsx |
title, icon?, action?, children |
Titled card wrapper with Lucide icon, action slot, and content area. Use for every content section. |
AgentCard |
agent-card.tsx |
name, role, department, status, currentTask?, timestamp? |
Consistent agent display. Composes Avatar + StatusBadge internally. Must look identical everywhere. |
DeptHealthBar |
dept-health-bar.tsx |
name, health?, agentCount |
Animated horizontal fill bar with utilisation percentage (null-safe; shows N/A when utilisation unavailable). Colour auto-mapped via getHealthColor(). |
ProgressGauge |
progress-gauge.tsx |
value, max?, label?, variant?, size? |
Circular or linear gauge for budget/utilisation. variant defaults to 'circular', max defaults to 100. |
StatPill |
stat-pill.tsx |
label, value |
Compact inline label + value pair for metadata rows. |
Avatar |
avatar.tsx |
name, size?, borderColor? |
Circular initials avatar with optional coloured border. Sizes: sm (24px), md (32px), lg (40px). |
Button |
button.tsx |
shadcn standard | Standard button component (shadcn/ui). |
TaskStatusIndicator |
task-status-indicator.tsx |
status: TaskStatus, label?: boolean, pulse?: boolean, className?: string |
Task status dot with optional label and pulse animation. |
PriorityBadge |
task-status-indicator.tsx |
priority: Priority, className?: string |
Task priority coloured pill badge. |
ProviderHealthBadge |
provider-health-badge.tsx |
status: ProviderHealthStatus, label?: boolean, pulse?: boolean, className?: string |
Provider health status dot (up/degraded/down/unknown) with optional label. |
RunOutcomeBadge |
run-outcome-badge.tsx |
outcome: RunOutcome, className? |
Failure-aware badge for a task run's outcome (succeeded / empty / failed): colour + icon + label so the signal is never colour alone. Shared across the approvals queue, review drawer, and chat prompts. |
StatusPill |
status-pill.tsx |
tone?, toneClassName?, icon?, ariaLabel?, children, className? |
The single inline status-pill primitive (one corner radius, one padding scale). tone maps to the shared palette; toneClassName carries a feature-specific one. Every status pill composes this rather than re-implementing the span. |
PlanStatusBadge |
plan-status-badge.tsx |
status: PlanStatus, className? |
Plan lifecycle pill across the tail: executing, integrating, evaluating, completed. Built on StatusPill. |
CompletionOracleVerdictBadge |
completion-oracle-verdict-badge.tsx |
verdict: CompletionOracleVerdict, className? |
Semantic badge for a completion-review verdict. |
RedTeamVerdictBadge |
red-team-verdict-badge.tsx |
verdict: RedTeamVerdict, className? |
Semantic badge for an adversarial red-team gate verdict. |
ConnectionHealthBadge |
connection-health-badge.tsx |
status: ConnectionHealthStatus, label?, pulse?, className? |
Connection health dot. The single owner of the mapping from the connection enum (healthy/degraded/unhealthy/unknown) onto ProviderHealthBadge's (up/degraded/down/unknown). Reports the outbound probe only; inbound readiness is surfaced separately, where it is actionable. |
LocalityBadge |
locality-badge.tsx |
isLocal |
Flags an agent whose model runs on a local, free-to-run provider. Renders nothing when false. |
ModelStalenessBadge |
model-staleness-badge.tsx |
stale: ModelStaleness \| null, className? |
Warning badge for a model the refresh service flagged as removed or deprecated, with the successor in a tooltip. Renders nothing for a current model. |
ToolCallingUnavailableBadge |
tool-calling-unavailable-badge.tsx |
toolCallsVerified: boolean \| null, className? |
Warning badge for a model the runtime downgraded after repeated tool-call failures. Renders only on an explicit false, never on an unobserved verdict. |
ProvenanceBadge |
provenance-badge.tsx |
className?, title?, children |
Presentational skeleton for data-provenance labels (measured vs absent). The caller owns the kind-to-tone mapping and passes the tone in. |
Timeline |
timeline.tsx |
frames, currentIndex, onSeek, label?, className? |
Horizontal scrubber of recorded turns, colour-coded by status. Click a dot to seek; arrow keys step, Home/End jump to the ends. |
ProgressIndicator |
progress-indicator.tsx |
variant: 'determinate' \| 'indeterminate' \| 'stages', value?, label?, description?, stages? |
Long-running-operation progress. The indeterminate variant can carry a live elapsed-time chip. |
Breadcrumbs |
breadcrumbs.tsx |
items, maxItems?, className? |
Trail for detail routes; collapses the middle to an ellipsis past maxItems (default 4). |
ListHeader |
list-header.tsx |
title, count?, countLabel?, description?, primaryAction?, secondaryActions? |
Standard list-page header: title with count, primary action top-right, and a slot for search/filter/sort that sits inline on wide viewports. |
InfoTooltip |
info-tooltip.tsx |
content, children, className? |
Hover/focus explanation beside an icon or compact control. Renders its trigger as a <span> so it can nest inside an existing interactive element; the popup is non-interactive. |
KeyboardShortcutHint |
keyboard-shortcut-hint.tsx |
keys, label?, size?, className? |
Renders a key sequence as <kbd> pills with an optional trailing label. |
Interaction Components¶
| Component | File | Props | Purpose |
|---|---|---|---|
Toast / ToastContainer |
toast.tsx |
toast (ToastItem), onDismiss, maxVisible? |
Notification toasts (success/error/warning/info) with auto-dismiss queue, Motion animations. Mount ToastContainer once in AppLayout. |
Skeleton variants |
skeleton.tsx |
shimmer?, lines?, rows?, columns? |
Loading placeholders: Skeleton (base), SkeletonText, SkeletonCard, SkeletonMetric, SkeletonTable. Shimmer respects prefers-reduced-motion. |
EmptyState |
empty-state.tsx |
icon?, title, description?, action? |
No-data / no-results placeholder with optional action button. |
ErrorBoundary |
error-boundary.tsx |
fallback?, onReset?, level? |
React error boundary with retry. Levels: page (full-height), section (card), component (inline). |
ConfirmDialog |
confirm-dialog.tsx |
open, onOpenChange, title, onConfirm, variant?, loading? |
Confirmation modal built on Base UI AlertDialog. Variants: default, destructive. |
CommandPalette |
command-palette.tsx |
className? |
Global Cmd+K search built with cmdk-base. Hosted inside a Base UI Dialog for focus trapping, fuzzy search, scope toggle, recent items. |
InlineEdit |
inline-edit.tsx |
value, onSave, validate?, type?, disabled? |
Click-to-edit with Enter/Escape, inline validation, optimistic save via useFlash. |
AnimatedPresence |
animated-presence.tsx |
routeKey, className? |
Page transition wrapper. Uses Motion AnimatePresence with reduced-motion fallback. |
StaggerGroup / StaggerItem |
stagger-group.tsx |
staggerDelay?, animate?, layoutId?, layout? |
Card entrance stagger container with configurable delay and layout animation support. |
Drawer |
drawer.tsx |
open, onClose, title?, ariaLabel?, side?, header?, footer?, contentClassName?, children, className? |
Slide-in panel (left or right via side, default right) with overlay, spring animation, focus trap, and Escape-to-close. At least one of title or ariaLabel must be provided for accessible naming. A custom header slot (via DrawerCustomHeader + DrawerCloseButton) replaces the default title bar, and a footer slot renders as a bordered sibling below the scroll area (used for the sticky approval decision buttons). Header omitted when neither title nor header is present. |
InputField |
input-field.tsx |
label, error?, hint?, multiline?, rows?, placeholder?, required?, disabled?, type?, value, onChange |
Labelled text input with inline error/hint display and optional textarea mode. Extends native input/textarea props. |
SelectField |
select-field.tsx |
label, options, value, onChange, error?, hint?, placeholder?, required?, disabled?, className? |
Labelled select dropdown with error/hint display and placeholder support. |
SliderField |
slider-field.tsx |
label, value, onChange, min, max, step?, formatValue?, disabled?, className? |
Labelled range slider with custom value formatter and aria-live value display. |
ToggleField |
toggle-field.tsx |
label, checked, onChange, description?, disabled? |
Labelled toggle switch (role="switch") with optional description text. |
TagInput |
tag-input.tsx |
label, value, onChange, placeholder?, disabled?, className? |
Chip-style multi-value input with add/remove, keyboard support (Enter to add, Backspace to remove), paste splitting. |
TokenUsageBar |
token-usage-bar.tsx |
data, segments?, max?, animated?, className? |
Segmented horizontal meter bar for token usage (multi-segment with auto-colours, role="meter", animated transitions). |
CodeMirrorEditor |
code-mirror-editor.tsx |
value, onChange, language, readOnly?, aria-label?, className? |
CodeMirror 6 editor with JSON/YAML modes, design-token dark theme, line numbers, bracket matching, and readOnly support. |
SegmentedControl |
segmented-control.tsx |
label, options, value, onChange, disabled?, size?, className? |
Accessible radiogroup with keyboard navigation (arrow keys + wrapping), size variants (sm/md), generic <T extends string> typing. |
ThemeToggle |
theme-toggle.tsx |
className? |
Base UI Popover with 5-axis theme controls (colour, density, typography, animation, sidebar). Rendered in StatusBar for global access. |
LiveRegion |
live-region.tsx |
children, politeness?, debounceMs?, className? |
Debounced ARIA live region wrapper for real-time WS updates without overwhelming screen readers. Default: 500ms polite, 0ms assertive. |
MobileUnsupportedOverlay |
mobile-unsupported.tsx |
(none, self-managing) | Full-screen overlay at <768px viewports directing users to desktop or CLI. Self-manages visibility via useBreakpoint. |
LazyCodeMirrorEditor |
lazy-code-mirror-editor.tsx |
Same as CodeMirrorEditor |
Suspense-wrapped lazy-loaded CodeMirrorEditor. Drop-in replacement that defers ~200KB+ CodeMirror bundle. |
MetadataGrid |
metadata-grid.tsx |
items, columns?, className? |
Key-value metadata grid for detail pages with configurable 2/3/4 columns and density-aware spacing. |
ProjectStatusBadge |
project-status-badge.tsx |
status, showLabel?, className? |
Project status dot with optional label and semantic colours (planning/active/integrating/evaluating/on_hold/completed/failed/cancelled). |
ContentTypeBadge |
content-type-badge.tsx |
contentType, className? |
MIME content type pill badge with semantic colours (JSON, PDF, Image, Text, Markdown, CSV, Binary). |
TaskProgress |
task-progress.tsx |
status, stages, className? |
Live task-execution progress panel (running / finished / error header + accumulated ProgressStages). Presentational leaf fed by the useTaskProgress hook; shown inline in the chat flows so an operator watches approved work execute instead of a silent gap. |
Dialog |
dialog.tsx |
open, onOpenChange, children; composed with DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogCloseButton |
Themed modal built on the Base UI Dialog primitive, for anything that is not a confirmation. ConfirmDialog is the pre-composed confirmation. |
Checkbox |
checkbox.tsx |
checked?, defaultChecked?, onCheckedChange?, disabled?, id?, name?, value?, aria-label? |
Themed checkbox on the Base UI primitive, so selection controls match the design system instead of the browser-native box. |
Collapsible |
collapsible.tsx |
title, summary?, open? / defaultOpen?, onOpenChange?, disabled?, children, contentClassName? |
Disclosure section with a trigger row and an optional right-aligned summary (count, status, badge). Controlled or uncontrolled. |
Pagination |
pagination.tsx |
page, pageSize, total, onPageChange, onPageSizeChange?, pageSizeOptions?, hidePageSize?, ariaLabel? |
1-indexed pager with page-size selector. An undefined total signals an unknown count. |
SearchInput |
search-input.tsx |
value, onChange, placeholder?, ariaLabel?, focusShortcut?, disabled? |
List-page search field. focusShortcut binds the global / key on the page's primary search only, and is ignored while focus is already in a text field. |
SearchFilterSort |
search-filter-sort.tsx |
search?, filters?, sort?, trailing?, className? |
Layout wrapper for list-page controls, so search, filters and sort line up identically on every list. |
BulkActionBar |
bulk-action-bar.tsx |
selectedCount, onClear, children, loading?, ariaLabel? |
Slide-up bar for a multi-select list. The caller owns the action buttons; the bar owns the count, the Clear control, and the disabled state. |
BulkDeleteControls |
bulk-delete-controls.tsx |
selection, noun, description, ariaLabel |
The bar plus the confirmation behind "delete the rows I picked". One component rather than a copy per list, so no list can skip the confirmation or word the count differently. |
DetailNavBar |
detail-nav-bar.tsx |
label?, canPrev, canNext, onPrev, onNext, position, bindShortcuts? |
Previous/next navigation between detail rows with a position counter, binding J / ArrowLeft and K / ArrowRight by default. A null position hides the counter (deep link or refresh). |
CommandCheatsheet |
command-cheatsheet.tsx |
open?, onOpenChange?, disableShortcut? |
The ? keyboard-shortcut overlay, rendered from the live shortcut registry rather than a hand-written list. Self-manages when uncontrolled. |
ErrorBanner |
error-banner.tsx |
variant?, severity?, title, description?, onRetry?, retryAfterSeconds? |
Page, inline, and offline error banners. error takes role="alert", while warning and info take role="status". A retryAfterSeconds value renders a cosmetic countdown on the Retry button; the caller still owns the retry. |
ErrorTechnicalDetails |
error-technical-details.tsx |
technical, className? |
Collapsed-by-default technical panel with copy-to-clipboard, shared by the router error page and the page-level error boundary. |
WsConnectionBanner |
ws-connection-banner.tsx |
title?, description? |
The standard "real-time updates disconnected" notice, so every live page words a dropped channel the same way. |
InheritToggle |
inherit-toggle.tsx |
inherit, onChange, inheritFrom?, disabled? |
Inherit-or-override switch for a setting that has a parent scope. |
AgentModelPicker |
agent-model-picker/ |
currentProvider, currentModelId, providers, onChange, label?, hideLabel?, disabled? |
The (provider, model) picker. Both halves are chosen together, because a model id means nothing without the connection it is reached through. |
HealthPopover |
health-popover/ |
children (the trigger) |
Shared system-health dialog behind both the StatusBar pill and the sidebar connection indicator. Reads the shared health snapshot and the live WebSocket state rather than fetching its own. |
Slot |
slot.tsx |
children, standard HTML attributes |
Merges its props and ref onto its single child, for components that need to render as whatever element the caller passes. |
Conversational Components¶
Used by the Chat surface and the other places the organisation speaks.
| Component | File | Props | Purpose |
|---|---|---|---|
ChatBubble |
chat-bubble.tsx |
variant, content?, children?, timestamp?, roleLabel?, agentName?, agentRole?, agentTopic?, isError? |
One turn in a transcript. Markdown body for an assistant or agent turn, plain text for a human one, and a custom body for the event and notice variants. A streaming bubble is hidden from assistive technology until it settles. |
ChatMarkdown |
chat-markdown.tsx |
content, className? |
Renders assistant markdown through descendant selectors only, so the tree carries design tokens and no raw HTML reaches the DOM. Tables and code blocks scroll inside themselves. |
ChatInputArea |
chat-input-area.tsx |
value, onChange, onSend, disabled, inputDisabled?, label, hideLabel?, placeholder |
Composer. disabled blocks sending while leaving the field editable, so composed text is never discarded; inputDisabled freezes the field itself for terminal states. |
ExamplePrompts |
example-prompts.tsx |
prompts, onSelect, disabled? |
Clickable starting points on an empty conversational surface. |
ResponderAttribution |
responder-attribution.tsx |
name, role?, topic?, loading? |
Names the agent that answered, with its role and the concern topic that routed to it. A responder carrying no role renders name-only. |
RequestCard |
request-card.tsx |
request, pending, onScope, onApprove, onReject |
A client request in the intake queue, with per-request in-flight flags so a double submission is impossible. |
Utility Functions¶
| Function | File | Purpose |
|---|---|---|
cn() |
lib/utils.ts |
Tailwind class merging (clsx + twMerge). Use in every component. |
getStatusColor() |
utils/agent-status.ts |
Maps AgentRuntimeStatus to SemanticColor \| "text-secondary" token name (offline maps to "text-secondary"). |
getHealthColor() |
utils/agent-status.ts |
Maps 0-100 percentage to SemanticColor (>=75 success, >=50 accent, >=25 warning, <25 danger). |
getTaskStatusColor() |
utils/tasks.ts |
Maps TaskStatus to SemanticColor. |
getTaskStatusLabel() |
utils/tasks.ts |
Maps TaskStatus to display label. |
getPriorityColor() |
utils/tasks.ts |
Maps Priority to SemanticColor. |
getPriorityLabel() |
utils/tasks.ts |
Maps Priority to display label. |
getTaskTypeLabel() |
utils/tasks.ts |
Maps TaskType to display label. |
getProviderHealthColor() |
utils/providers.ts |
Maps ProviderHealthStatus to SemanticColor \| "muted". |
formatLatency() |
utils/providers.ts |
Formats milliseconds to human-readable string (e.g. "123ms", "1.5s"). |
formatErrorRate() |
utils/providers.ts |
Formats error rate percentage with <0.1% handling. |
formatTokenCount() |
utils/providers.ts |
Formats token count with K/M suffixes. |
formatCost() |
utils/providers.ts |
Formats cost value using the runtime-configured currency (falls back to DEFAULT_CURRENCY); display-only, no FX conversion. |
toRuntimeStatus() |
utils/agents.ts |
Maps API-layer AgentStatus (HR lifecycle) to AgentRuntimeStatus for UI components. |
getRiskLevelColor() |
utils/approvals.ts |
Maps ApprovalRiskLevel to SemanticColor \| "accent-dim". |
getRiskLevelLabel() |
utils/approvals.ts |
Maps ApprovalRiskLevel to display label. |
getRiskLevelIcon() |
utils/approvals.ts |
Maps ApprovalRiskLevel to LucideIcon. |
getRunOutcomeColor() |
utils/approvals.ts |
Maps RunOutcome to SemanticColor (succeeded=success, empty=warning, failed=danger). |
getRunOutcomeLabel() |
utils/approvals.ts |
Maps RunOutcome to display label. |
getRunOutcomeIcon() |
utils/approvals.ts |
Maps RunOutcome to LucideIcon. |
isFailedApproval() |
utils/approvals.ts |
Whether an approval represents a failed run (drives the danger styling + Acknowledge/Retry relabel). |
getApprovalStepLabel() |
utils/approvals.ts |
The source-aware step label ("Approve to start" vs "Review completed work"). |
approvalDetailPath() |
utils/approvals.ts |
Builds the /approvals?selected=<id> deep-link path. |
getApprovalStatusColor() |
utils/approvals.ts |
Maps ApprovalStatus to SemanticColor \| "text-secondary". |
getApprovalStatusLabel() |
utils/approvals.ts |
Maps ApprovalStatus to display label. |
getUrgencyColor() |
utils/approvals.ts |
Maps UrgencyLevel to SemanticColor \| "text-secondary". |
formatUrgency() |
utils/approvals.ts |
Formats seconds_remaining into human-readable countdown string. |
groupByRiskLevel() |
utils/approvals.ts |
Groups approvals into Map<ApprovalRiskLevel, ApprovalResponse[]> sorted critical-to-low. |
filterApprovals() |
utils/approvals.ts |
Client-side filtering by status, risk level, action type, and search text. |
RISK_LEVEL_ORDER |
utils/approvals.ts |
Numeric ordering map for risk levels (critical=0 through low=3). |
DOT_COLOR_CLASSES |
utils/approvals.ts |
Maps SemanticColor \| "accent-dim" to Tailwind background classes. |
URGENCY_BADGE_CLASSES |
utils/approvals.ts |
Maps SemanticColor \| "text-secondary" to Tailwind badge classes. |
RISK_BADGE_CLASSES |
utils/approvals.ts |
Maps SemanticColor \| "accent-dim" to Tailwind badge (bg + text + border) classes for the risk pill. |
formatFileSize() |
utils/format.ts |
Formats byte count to human-readable size string (e.g. "1.2 MB", "340 KB"). |
Design System Hooks¶
| Hook | File | Purpose |
|---|---|---|
useFlash() |
hooks/useFlash.ts |
Real-time update flash effect. Returns { flashing, flashClassName, triggerFlash, flashStyle }. Uses STATUS_FLASH timing constants. |
useStatusTransition() |
hooks/useStatusTransition.ts |
Animate between agent status colours. Returns { displayColor, motionProps } for spreading on motion.div. |
useCommandPalette() |
hooks/useCommandPalette.ts |
Global command palette state. registerCommands() adds page-local commands (cleanup on unmount). open() / close() / toggle(). |
useAnimationPreset() |
hooks/useAnimationPreset.ts |
Returns animation config (spring, tween, staggerDelay, enableLayout) based on the user's theme animation preference. Components use this instead of directly referencing lib/motion.ts constants. |
useCountAnimation() |
hooks/useCountAnimation.ts |
Animated numeric value transitions for metric displays. Uses rAF with ease-out cubic and prefers-reduced-motion support. |
useAutoScroll() |
hooks/useAutoScroll.ts |
Auto-scroll container to bottom on new content, pausing when user scrolls away. Returns { isAutoScrolling, scrollToBottom }. |
useBreakpoint() |
hooks/useBreakpoint.ts |
Reactive viewport breakpoint detection via matchMedia. Returns { breakpoint, isDesktop, isTablet, isMobile }. |
Types¶
| Type | File | Values |
|---|---|---|
AgentRuntimeStatus |
utils/agent-status.ts |
"active", "idle", "error", "offline" |
SemanticColor |
utils/agent-status.ts |
"success", "accent", "warning", "danger" |
TaskStatus |
api/types/enums |
"created", "assigned", "in_progress", "in_review", "completed", "blocked", "failed", "interrupted", "suspended", "cancelled", "rejected", "auth_required", "awaiting_input" |
Priority |
api/types/enums |
"critical", "high", "medium", "low" |
ProviderHealthStatus |
api/types/providers |
"up", "degraded", "down", "unknown" |
ApprovalStatus |
api/types/enums |
"pending", "approved", "rejected", "expired" |
ApprovalRiskLevel |
api/types/enums |
"low", "medium", "high", "critical" |
UrgencyLevel |
api/types/enums |
"critical", "high", "normal", "no_expiry" |
ApprovalPageFilters |
utils/approvals |
Filter shape: status?, riskLevel?, actionType?, search? |
ProjectStatus |
api/types/enums |
"planning", "active", "integrating", "evaluating", "on_hold", "completed", "cancelled", "failed" |
ArtifactType |
api/types/enums |
"code", "tests", "documentation" |
Every enum above sourced from api/types/enums is re-exported from web/src/api/types/enum-values.gen.ts, generated from the backend's own API surface by scripts/generate_dto_types_ts.py and held in step by a pre-push drift check. A value is added by changing the backend and regenerating, never by editing the generated file. The types sourced from utils/ are UI-local and have no backend counterpart, so they are hand-written and generated from nothing.
When to Create a New Shared Component¶
Create a new component in web/src/components/ui/ when:
- The same UI pattern appears (or will appear) on 2+ pages
- It represents a semantic concept (not just a styled div)
- It has configurable behaviour via props (variants, states, sizes)
Every new shared component must have:
- A
.stories.tsxfile with all states (default, hover, loading, error, empty) - A TypeScript props interface
- Design token usage exclusively (no hardcoded colours/fonts/spacing)
cn()for conditional class merging
Enforcement¶
A PostToolUse hook (scripts/check_web_design_system.py) runs automatically on every Edit/Write to web/src/ files. See CLAUDE.md "Web Dashboard Design System" section for the full rule set.
Reference Materials¶
| Resource | Location |
|---|---|
| Page structure and information architecture | Page Structure & IA |
| UX design guidelines (implementation specs) | UX Guidelines |
| Framework evaluation behind the dashboard stack | Dashboard Framework Research |
| Design tokens (the source of every colour, spacing and density value) | web/src/styles/design-tokens.css |
| Theme axes and their persisted keys | web/src/stores/theme.ts |
| Motion presets | web/src/lib/motion.ts |
| WCAG verification script | scripts/wcag_check.py |
| Design-system enforcement hook | scripts/check_web_design_system.py |