exact versions are immutable; binary and bundle artifacts are digest-pinned.
skill doc
skills/design-system.md
iii Schematic — design system
The console reads as an engineering document, not a SaaS dashboard: hierarchy comes from
layered surfaces (one background step per region), the chrome draws no lines, every corner is
the same 6px, and a single rationed accent marks live state and primary actions. Sans for
humans, mono for machines. The code is the spec: tokens in ade/web/src/index.css, public CSS
recipes in ade/web/src/styles/ui-recipes.css, components in ade/web/src/components/ui
exported through packages/console-ui (index.d.ts, component-names.mjs, ui-classes.mjs,
token-names.mjs, hooks.d.mts, format.d.mts). This file is the only canonical text;
ade/web/DESIGN.md is a pointer to it. Workspace behaviour (panes, tabs, chords, palette,
annotations) is documented in ade/web/README.md, not here.
Principles
Surfaces, not borders. A new region is a one-step background change, never an outline or
divider. Three strokes exist, each with one job: rule-focus (focus), edge (the hairline
frame of floating workspace panels and the PageBody gap), and an optional neutral edge
rail on a selected row or card. rule, rule-2, rule-strong are transparent legacy names.
One radius. Every Tailwind radius step resolves to 6px; only none and full differ.
Accent rationing.accent (burnt orange on cream, electric blue on dark) is for focused
controls, live/running state, primary actions and semantic data. Never for selection, body
text, large fills or decoration.
Neutral selection. Selected = surface-selected fill + ink text (+ optional edge
rail), identical in both themes. Status arrives through ok/warn/alert on a dot, a mark
or a -muted tint, never a solid background or a stripe.
Sans for humans, mono for machines. Inter for copy and controls; Geist Mono for IDs,
timestamps, metrics, function names; the code font only on editor, terminal, pre/code.
Motion communicates, never decorates. Shared duration/ease tokens only; streaming, logs,
drag and resize update instantly; prefers-reduced-motion zeroes every token.
Density is deliberate. Panels, traces, code and forms share a surface without a
hierarchy contest; the trace timeline is the most colourful area and the only one that draws
data lines (alpha-ink fills such as bg-ink/15).
Tokens
Use the Tailwind utility (bg-surface, text-ink-faint) inside the console; injected worker
CSS uses var(--color-*). Public names are the ones in packages/console-ui/token-names.mjs;
everything else below is console-only and may change.
Surface ramp (light → dark)
Token
Light
Dark
Use
bg
#f2f0ed
#0a0a0a
Application canvas, the deepest layer
sidebar
#f2f2f2
#0e0e0e
Navigation columns (PageSidebar)
panel
lab(98.26% 0 0)
#111111
Main columns (PageShell, PageMain)
panel-raised
#f7f5f2
#171717
Headers, cards, popovers, composer, tool cards
paper-2
#ebe8e3
#171717
Legacy alias of panel-raised; do not add uses
surface
rgba(20,16,8,.055)
rgba(255,255,255,.055)
Inputs, controls, chips, secondary cards
surface-hover
rgba(20,16,8,.085)
rgba(255,255,255,.085)
Hover on rows, items, ghost controls
surface-selected
rgba(20,16,8,.12)
rgba(255,255,255,.12)
Selected row, card, tab, chip, segment
surface-active
rgba(20,16,8,.12)
rgba(255,255,255,.12)
Pressed / strong active
card-highlight
#dbdbdb63
#0d0d0e63
Borderless inset inside a card; never a state
The surface* fills are alpha, so one step reads the same over any base layer.
Ink, strokes, accent, status
Token
Light
Dark
Use
ink
#0a0a0a
#ededed
Primary text, primary buttons, wordmark
ink-faint
#6b6865
#a6a6a6
Secondary copy, captions, inactive navigation
ink-ghost
#a3a09c
#6f6f6f
Placeholders, timestamps, line numbers
ink-disabled
#b8b4ae
#4d4d4d
Disabled labels (paired with opacity-40)
muted-foreground
#6b6865
#b8b8b8
Legacy alias of ink-faint (dark one step brighter)
trigger-running
#57534f
#a6a6a6
In-flight function description shimmer base
rule, rule-2, rule-strong
transparent
transparent
Legacy; border-rule* is an inert 1px
rule-focus
rgba(184,66,15,.6)
rgba(40,168,247,.7)
The focus stroke on inputs and controls
edge
#14100829
rgba(255,255,255,.07)
Panel frame, PageBody gap, selected rail
accent / accent-fg
#b8420f / #f2f0ed
#28a8f7 / #070909
Live state, focus ring, primary action
accent-hover
#a53a0c
#46b6fa
Hover on accent fills
accent-muted / accent-border
10% / 35% alpha
12% / 35% alpha
Accent tint / legacy accent stroke
alert / alert-muted
#ff0026 / 8%
#f05d68 / 12%
Errors, failed calls
warn / warn-muted
#a87a00 / 12%
#f5a524 / 12%
Warnings, pending approval
alert-strong / warn-strong
#c60021 / #7a5700
#ff4d63 / #d49b00
The same status as 11–12px text (AA); dots and badges keep the base tone
ok / ok-muted
#356f3d / 12%
#36c98f / 12%
Success, completed calls, diff additions
workdir / workdir-muted
#0ea5e9 / 12%
same / 14%
Console-only: the chat activity folder mark
ring
5% ink mix
same
Tailwind's default ring colour
Glyph tones
glyph-blue #2563eb, glyph-purple #7c3aed, glyph-teal #0d9488, glyph-green #15803d,
glyph-amber #b7791f, glyph-rose #e11d48 (dark: #3b82f6 #8b5cf6 #14b8a6 #22c55e #eab308 #f43f5e). Identity tints for one 16px glyph beside a label — an agent profile in the
conversation tree, a folder or scope mark in a worker's tree — applied through data-color on
uiClasses.treeItemIcon. Each holds at least 3:1 against sidebar. A tone is never a fill, a
border, text or a selection/status state; neutral means the glyph keeps ink-ghost.
Fonts — three roles
Token
Stack
Role
--font-sans
Inter, ui-sans-serif, system-ui
Human copy and controls (400/500/600 loaded)
--font-mono
Geist Mono, Chivo Mono, ui-monospace
Machine values in chrome: IDs, times, eyebrows, keys
--font-code
Monaco, SF Mono, Menlo, Consolas
Editor, terminal, pre, code — not the chrome mono
--font-geist-mono
Geist Mono, ui-monospace
Alias used by function-trigger chrome
Ligatures are off on every mono/code surface (liga clig calt dlig 0). Do not add a family.
Radii, shadows, motion, spacing
Token
Value
radius-xs … radius-xl
6px, all of them; radius-none 0; radius-full 9999px
inset 1px highlight + inset 1px ring + 1px outer edge + two tight drops (composer)
shadow-keycap
lift upside down — lit edge at the bottom, drops upward (Kbd only)
motion-duration-instant/fast/control/panel
0 / 120 / 160 / 220 ms
motion-ease-standard
cubic-bezier(0.2, 0, 0, 1) — state changes
motion-ease-enter / ease-glide
cubic-bezier(0.16, 1, 0.3, 1) — mounting
motion-ease-exit
cubic-bezier(0.4, 0, 1, 1) — dismissal
spacing-gutter / section-x / section-y
24 / 36 / 80 px
spacing-sheet-max / content-max
1200 / 1216 px
Four shadows, each a complete theme-aware value; the --iii-ui-lift-* ingredients are
console-only. Under prefers-reduced-motion the three non-zero durations become 0ms and every
animation runs once at 0.01ms. Console-only transitions.dev scale: --duration-stagger 40,
micro 80, quick 150, fast 250, medium 350, slow 400, very-slow 500 ms,
--ease-smooth-out, --distance-*, --scale-*, --blur-* — used by dropdown, sheet and
picker-page motion. Note --duration-fast (250ms) is not --motion-duration-fast (120ms).
Dark theme is the same token set flipped under html[data-theme="dark"]: a neutral gray ramp
from black, white-alpha component fills, blue accent, and heavier shadows because there the
shadow is the only depth cue. index.html picks the theme before paint from localStorageiii-theme, else the OS prefers-color-scheme; useTheme (use-theme.ts) then owns
html[data-theme], color-scheme, the theme-color meta and the stored value. Once a user
has chosen, the console does not follow later OS changes.
Typography
Two families plus the code font (above). The scale is whatever the shared components render:
StatusBar, tree meta, field description, Kbd (mono)
Floor
11px
lint-worker-ui warns below it
Micro
10px mono
Trace timeline labels only — data visualisation, never chrome
Every number, timestamp and KPI sets tabular-nums. Case: copy keeps its authored case. The
only sanctioned CSS case transform is the eyebrow (Eyebrow, eyebrowClassName,
.iii-ui-eyebrow); no uppercase/lowercase/capitalize anywhere else, on any element.
Numbers
The single source of truth — other skills link here and never restate these.
Number
Value
Source
Body text
13px desktop; inputs 16px on phones
Input.tsx, ui-recipes.css
Page title / description
14px / 12px
PageChrome.tsx
Card header
13px / 600
.iii-ui-card__header
Metadata / eyebrow / floor
11px
Toolbar.tsx, Eyebrow.tsx, lint-worker-ui.mjs
Icon
16px (size-4, .iii-ui-icon)
ui-recipes.css, lint rule icon-size
Radius
6px
--radius-*
Desktop rows
tree row 28, status bar 28, toolbar 36, page header 44
14px, 13px at ≥ 40rem container; cells 12px, compact 8px
.iii-ui-table
Panel gutter
6px between floating workspace panels
workspace-panel-divider-*
Card padding
body 12px; EmptyState cell 20px; header 10px/12px
.iii-ui-card__body, Cell.tsx
Tooltip
bottom by default, offset 6px, 12px text
Tooltip.tsx
Shared components
Everything below is exported by @iii-dev/console-ui (component-names.mjs); props are the
ones in index.d.ts. State is expressed with data-* attributes (data-selected,
data-tone, data-narrow, data-density), never with extra classes.
label shorthand, or TooltipTrigger + TooltipContent (side, default bottom)
Don't tooltip a bare ✕
DropdownMenu + Trigger/Content/Group/Item/Label/Separator, CheckboxItem (toggle, onSelect + preventDefault keeps it open), RadioGroup/RadioItem (one choice), Sub/SubTrigger/SubContent (a setting row that pushes into a submenu)
Radix menu on panel-raised, shadow-floating; check in the left gutter, tree caret on a sub-trigger
Destructive last; no Menubar — one trigger per menu
Feedback
Component
Anatomy / props
Do / Don't
EmptyState
icon, title, description, one action (ghost sm); compact inside a card, actions[] for more
Write a sentence, not a shrug
Skeleton
surface block with skeleton-pulse
Match the final layout's shape
StatusPanel
variant info/success/warn/alert; icon, headline, detail on the tint; action slot, role passes through
glyph-* on one 16px glyph via treeItemIcon[data-color]; status stays on StatusDot/Chip
Mono vs sans
Values in font-mono, labels in sans; never a whole panel in mono
Function-call cards
MetaRow first, ActionLine per reported action, body in fcall-chrome (console)
Theme
Read useTheme(); never branch on prefers-color-scheme yourself
Do / Don't
Do
Build every page from PageShell → PageHeader → PageBody → (PageSidebar) + PageMain.
Step the background one level to make a region; reach for edge only on the panel frame, the
PageBody gap and a selected rail.
Use Inter for copy and controls, Geist Mono for machine values, the code font for code.
Keep every number tabular-nums; keep authored case; Eyebrow is the one uppercase.
Ration the accent to focus, live/running state, primary actions and semantic data.
Lay panes out with @container and @2xl: / @3xl: / @5xl:; reserve the 640px viewport
breakpoint for console chrome (phone sheet vs popover, 16px inputs, 48px touch targets).
Use the four shadows (raised, floating, lift, keycap) and the motion tokens; honour
reduced motion by default (the tokens already do).
Use Lucide icons at 16px and give every icon-only control a label.
Don't invent radii, shadows, durations, font sizes below 11px, or a fourth font.
Don't put a solid status colour behind text; tint with -muted and colour the text/dot.
Don't uppercase, lowercase or capitalise copy outside Eyebrow.
Don't use viewport media queries for pane layout, window.confirm, inline SVG, document key
listeners or bare-key shortcuts.
Don't use gradients, except fcall-chrome (function-call cards) and thinking-shimmer
(streaming text), and no glow except pulse-dot.
Don't set a whole panel in mono, or technical values in sans.
Don't reach for a Tailwind utility to override a property a recipe already sets:
ui-recipes.css is imported unlayered, so a normal utility in @layer utilities loses to it
at any specificity and silently does nothing. Only an !important utility would win, and that
is not the answer — put the rule in the recipe, keyed by a data attribute.