Integrity Console Design System

The support dashboard rebuilt as a component library: 40 components, 31 stories, and a token layer the clickthrough and the React build both speak.
- 40
- Components in the library
- 31
- Stories browsable in Storybook
- 12
- Full page compositions
- Role
- Staff Designer
- Type
- Design System
- React
- Storybook
- Tailwind
- Base UI
- Design tokens
Problem
A clickthrough prototype proves an interaction and then stops being useful. The Integrity Console existed as 25 hand-built screens of vanilla HTML: convincing to click through, impossible to hand to an engineer as anything but a picture.
Task
Turn the prototype's decisions into a component library an engineer could build from: every surface, state and colour named once, and every component demonstrable in isolation.
Two artefacts, one product
The Support Agent Dashboard shipped as a 25-screen clickthrough: vanilla HTML, CSS and JavaScript, no framework, no build step. Right shape for proving an interaction, wrong shape for handing to engineering, since a prototype has no components in it. It has markup that happens to look like components.
This is the other half: the same product expressed as a library.
The system
Base UI primitives, Tailwind v4, shadcn conventions, typeset in Geist. 40 components across four groups, plus 12 page compositions that assemble them at real scale.
| Group | Count | What's in it |
|---|---|---|
ui/ | 21 | Accordion, avatar, badge, button, card, chart, command, dialog, dropdown, input, input group, popover, segmented control, skeleton, sonner, switch, table, tabs, textarea, toggle, toggle group |
inbox/ | 10 | Conversation list and rows, message thread, composer, chat header, side panel, role avatar and badge, command palette, close-conversation dialogs |
dashboard/ | 5 | Stat tile, trend chart, source card, compare toggle, report metric layout |
chrome/ | 4 | App shell, icon rail, secondary nav, topbar |
Primitives, at the token level
UI / Button: all variants
Full screenUI / Badge: all variants
Full screenTwo reds, one meaning each. --primary (#c70036) carries every primary
action. --red-deep (#b20030) is reserved for the inbox Close button and the
live-attempt badge, and nothing else. A single --red would have blurred two
meanings into one swatch, and they'd have drifted apart in code anyway.
Inbox
Pages / Inbox
Full screenOne elevation, everywhere. The content card is the only shadowed surface on a
page; nested blocks get --surface-nested and a hairline border, never a
second shadow.
Dashboard and reports
Dashboard / StatTile: grid
Full screenPages / Reports Overview
Full screenRole colour is semantic, not decorative. Student, instructor and admin each own a background/foreground pair, and the same three hues carry the chart series: an orange line on the Overview chart means the same thing an orange badge means in the Inbox.
The token layer is the actual deliverable
The prototype already had a disciplined tokens.css: surfaces, chat row
states, a series palette for charts. Rather than redraw those decisions in
Tailwind's vocabulary, the rebuild promotes them into a @theme layer under the
same names, so a value never means one thing in the clickthrough and another in
the React build.
:root {
--primary: #c70036; /* the one red that means "primary action" */
--background: #efeceb; /* app ground: cards float on it, no dividers */
--surface-nested: #f8f7f6; /* tiles and chart wells inside a white card */
--sel: #dbd3d1; /* selected chip, filter pill, received bubble */
/* Inbox rows carry their own scale, because "unread" has to read as
lighter than the ground while "read" reads as a step darker. */
--unread: #fdfcfc;
--chat-read: #e6e1e0;
}Role pairs
Student, instructor and admin each own a background/foreground pair. The chart series reuse the same hues, so a colour carries one meaning across the whole product rather than one meaning per view.
| Role | Background | Foreground |
|---|---|---|
| Student | #fae0d1 | #b94513 |
| Instructor | #dbeafe | #1d4ed8 |
| Admin | #feeeaf | #d97706 |
Status
| Token | Value | Use |
|---|---|---|
--success | #047857 | Resolved, healthy, within threshold |
--warning | #b45309 | Approaching a threshold |
--destructive | #e7000b | Failed check, above threshold |
--readonly | #778ba6 | Fields the agent can see but not change |
Inbox row states
The one place the palette gets fussy, because three states have to be
distinguishable at a glance down a long list. Unread reads lighter than the
ground; read reads a step darker; selected gets its own warm grey rather than
borrowing --sel.
| State | Rest | Hover |
|---|---|---|
| Unread | #fdfcfc | #f4f0ef |
| Read | #e6e1e0 | #dcd6d4 |
| Selected | #d2c8c6 | #c7bab7 |
Type
Geist Variable throughout: --font-heading is an alias of --font-sans, not a
second family. Headings differentiate by weight and size against Tailwind's
default scale; there is no bespoke scale to memorise.
Radius
One base, everything else derived. Changing --radius reshapes the product.
--radius: 0.75rem; /* lg */
--radius-sm: calc(var(--radius) * 0.6);
--radius-md: calc(var(--radius) * 0.8);
--radius-xl: calc(var(--radius) * 1.4);
--radius-2xl: calc(var(--radius) * 1.8);Elevation
--shadow-card: 0 0 4px rgba(0, 0, 0, 0.15);That is the entire elevation system. The content card is the only shadowed
surface on a page; anything nested inside it uses --surface-nested with a
hairline border. A second shadow is a bug.
What I'd do next
- Wire Code Connect so the Figma components point at these implementations rather than at a description of them.
- Publish the report views as a template, since seven of the eight are the same layout with a different metric.
