Temps design system
The design system is a sandbox app plus a component package. Everything a UI
task needs is in the repo; do not invent tokens, colours or page shapes.
| What |
Where |
| Rules digest for agents (read first, imperative, short) |
design-system/docs/RULES.md |
| Brand guidelines (why the rules exist) |
design-system/docs/brand-guidelines.md |
| Handoff: tokens, primitive catalogue, page templates, responsive, keyboard |
design-system/docs/design-system-handoff.md |
| Component package consumed by screens |
web/packages/ds (@temps-sdk/ds) |
Consumer setup (a plugin, an outside app): install, op.css, @source, the skin class, fonts |
web/packages/ds/README.md |
| Reference implementation of every screen |
design-system/src/sections/ConsoleV1*.tsx |
| Browsable guide, component gallery, console mockups |
cd design-system && bun install && bun run dev → /guide, /op-components, /v1 |
Scope boundary
What exists today, stated plainly so nobody assumes more: the production
console (web/src, rsbuild) does not import @temps-sdk/ds yet. What is
built is the system (the docs), the package, and the sandbox that renders every
primitive and every screen shape against it. Console migration happens screen by
screen, on a schedule, not as a side effect of another task.
- Redesign work (new screens on
@temps-sdk/ds, the sandbox, the landing
and status page mockups): this skill applies in full.
- Legacy console (
web/src/** on shadcn/ui): follow the frontend rules in
CLAUDE.md. Do not restyle legacy screens piecemeal to the new system; a
screen moves to the new system whole, when its migration is scheduled.
- Plugin UI (a separate document in an iframe): see "Plugin UI" below. It
gets the system by bundling the package, not by inheriting anything.
- The package (
web/packages/ds): change a primitive only together with
its entry in the handoff doc §6, the gallery on /op-components and the
CHANGELOG.md of the package.
Procedure for a UI task
- Read
design-system/docs/RULES.md end to end. It is 120 lines. When it
disagrees with the two long docs, the long docs win; fix the digest.
- Classify the screen from its data, not from habit (RULES.md "Page
structure"): many records of one kind →
Ledger; one record read top to
bottom → Detail + Columns; a configuration → Settings; nothing yet,
not set up or failed → PageState.
- Find the closest reference screen in
design-system/src/sections/ and
start from its shape. Deployment (ConsoleV1Deploy.tsx), Nodes
(ConsoleV1Nodes.tsx), Database (ConsoleV1Database.tsx) and Settings
(ConsoleV1Settings.tsx) cover the record, list, tool and configuration
cases.
- Build with primitives from
@temps-sdk/ds only. Import the skin once
(@import '@temps-sdk/ds/op.css') and put operator ink v1 on the
root you want skinned, including portalled content.
- Apply the record recipe: title + meta → status verdict →
Lede with four
to six facts → Columns (main: the thing and its timeline; aside: what is
left) → sections. A fact appears once. Colour only through Status, as
glyph + word + tone. Icons say what kind, glyphs say what state.
- Wire every drawn control. A
Kbd badge needs a handler, a filter must
filter, a destination is a typed /${string} path, never #. The ledger
cursor moves DOM focus.
- Check both widths, both modes: 1440 and 390, light and dark. Below md,
ledger rows render
mobile and it carries the row's primary action.
Adding a primitive
- Build it in
web/packages/ds/src/*.tsx and export it from src/index.ts.
- Give it a gallery block: a
<section id="…"> in
design-system/src/sections/blocks/*.tsx, or in OpComponents.tsx for a
primitive that belongs to no rule document. Show every state, not a happy
path. Add its id and label to the page's TOC (OpComponents.tsx, or the
*_TOC export the page spreads).
- Add the id to
BLOCKS in design-system/e2e/visual.spec.ts, in page order.
A toEqual([...BLOCKS]) assertion compares that list against the sections
the page renders, so the run stays red until both agree.
- Shoot the baseline: run the gallery tests, then adopt only the new
block's actuals — copy each
-actual.png Playwright wrote under
test-results/ over
design-system/e2e/__screenshots__/visual.spec.ts/<name>-<project>.png.
Never a blanket --update-snapshots: it rewrites ~85 blocks for sub-pixel
noise and the diff you were meant to read drowns.
- Write the handoff §6 entry: what it is for, what it refuses to do, its
states.
- Add the
CHANGELOG.md line in web/packages/ds/.
Before you ship
Run from design-system/:
bun run lint # tsc --noEmit + scripts/audit-records.mjs + tokens.mjs check
bun run e2e # overflow at 390/1440, keyboard, drop focus, reload signatures, axe, visual
Both must be clean. Fix dev-console warnings from Lede and Detail. When a
visual baseline changes on purpose, adopt the actuals as in step 4 above and
say which blocks moved, and why, in the PR.
audit-records.mjs audits src/sections by default and takes --dir <path>
(repeatable) for any other folder of screens:
node scripts/audit-records.mjs --dir ../examples/example-plugin/web/src
Know what green proves. Lint-enforced: types (tsc), the record recipe
(audit-records.mjs, literal-only and heuristic on two of its rules), and
tokens.json against op.css (tokens.mjs check). E2E-enforced: no
horizontal scroll at 390/1440 with a clean console, no new serious/critical axe
violation in light and dark, the keyboard contract, reload signatures, and the
visual baselines.
Honour system — nothing fails if you break these: paper and ink only, no second
hue, colour only through Status, no cards and one .op-raise per screen, no
hex / oklch() / palette literal / ms literal in a .tsx, the closed
spacing scale, every drawn control wired, view state in the URL beyond what
state.spec.ts samples, and the words (content.md, localisation.md,
icons.md). A green lint means the types, the recipe and the tokens hold; it
does not mean the screen follows the design system. Read it yourself.
Plugin UI
A plugin's UI is a separate document: web/src/pages/plugins/PluginPage.tsx
mounts it in a same-origin iframe at /api/x/{plugin}/ui/, and the plugin
serves its own HTML, JS and CSS. Nothing crosses that boundary — not the
console's stylesheet, not the operator ink v1 root, not the fonts, not the
Tailwind build that generated the utilities the primitives use. A plugin that
assumes it inherits the skin renders unstyled.
So a plugin sets the system up for itself, like any outside app. It is a plain
Vite + React app (examples/example-plugin/web/ is the shape), so
web/packages/ds/README.md is the setup, verbatim:
bun add @temps-sdk/ds plus its peer dependencies, and pin the version
the console ships — two versions of the skin side by side drift in a way
that reads as "this page looks slightly wrong", not as a bug.
@import '@temps-sdk/ds/op.css' at the top of the entry stylesheet, and
@source "../node_modules/@temps-sdk/ds/dist" so the consumer's Tailwind
scans the package and generates the utilities it renders.
operator ink v1 on the plugin's own root, and on any portalled content.
- Theme: there is no theme channel today.
PluginPage.tsx syncs the route
(hash or postMessage) and nothing else, so read prefers-color-scheme for
now and toggle .dark from it; theme sync from the parent is a follow-up
(handoff §15).
- Same conventions as a console screen:
CopyAction for a copy, Button busy
for an action in flight, useUrlState for the view. A plugin's route is
mirrored into the console's address bar, so a plugin that keeps its facet in
React state produces a link that does not reopen what the reader was looking
at.
Handoff §3b says the same thing at length.
Changing a rule
A rule changes in one commit that edits brand-guidelines.md,
design-system-handoff.md, RULES.md and the reference page together. A
rule stated in only one place is not a rule.
1---2name: temps-design-system3description: Build or review console UI so it reads as Temps: the paper-and-ink operator design system (`@temps-sdk/ds` primitives, the `operator ink v1` skin, the Ledger / Detail / Settings page templates, the status vocabulary and the record recipe). Invoke when a task adds or redesigns a console screen, a landing section or a status page on the new design system, when the user says "follow the design system", "make it look like temps", "brand guidelines", "taste", "op components", or when reviewing a UI PR against the guidelines. Not for the legacy `web/src` console: that stays on its current shadcn look until it is migrated screen by screen.4---56# Temps design system78The design system is a sandbox app plus a component package. Everything a UI9task needs is in the repo; do not invent tokens, colours or page shapes.1011| What | Where |12|---|---|13| Rules digest for agents (read first, imperative, short) | `design-system/docs/RULES.md` |14| Brand guidelines (why the rules exist) | `design-system/docs/brand-guidelines.md` |15| Handoff: tokens, primitive catalogue, page templates, responsive, keyboard | `design-system/docs/design-system-handoff.md` |16| Component package consumed by screens | `web/packages/ds` (`@temps-sdk/ds`) |17| Consumer setup (a plugin, an outside app): install, `op.css`, `@source`, the skin class, fonts | `web/packages/ds/README.md` |18| Reference implementation of every screen | `design-system/src/sections/ConsoleV1*.tsx` |19| Browsable guide, component gallery, console mockups | `cd design-system && bun install && bun run dev` → `/guide`, `/op-components`, `/v1` |2021## Scope boundary2223What exists today, stated plainly so nobody assumes more: **the production24console (`web/src`, rsbuild) does not import `@temps-sdk/ds` yet.** What is25built is the system (the docs), the package, and the sandbox that renders every26primitive and every screen shape against it. Console migration happens screen by27screen, on a schedule, not as a side effect of another task.2829- **Redesign work** (new screens on `@temps-sdk/ds`, the sandbox, the landing30 and status page mockups): this skill applies in full.31- **Legacy console** (`web/src/**` on shadcn/ui): follow the frontend rules in32 `CLAUDE.md`. Do not restyle legacy screens piecemeal to the new system; a33 screen moves to the new system whole, when its migration is scheduled.34- **Plugin UI** (a separate document in an iframe): see "Plugin UI" below. It35 gets the system by bundling the package, not by inheriting anything.36- **The package** (`web/packages/ds`): change a primitive only together with37 its entry in the handoff doc §6, the gallery on `/op-components` and the38 `CHANGELOG.md` of the package.3940## Procedure for a UI task41421. Read `design-system/docs/RULES.md` end to end. It is 120 lines. When it43 disagrees with the two long docs, the long docs win; fix the digest.442. Classify the screen from its data, not from habit (RULES.md "Page45 structure"): many records of one kind → `Ledger`; one record read top to46 bottom → `Detail` + `Columns`; a configuration → `Settings`; nothing yet,47 not set up or failed → `PageState`.483. Find the closest reference screen in `design-system/src/sections/` and49 start from its shape. Deployment (`ConsoleV1Deploy.tsx`), Nodes50 (`ConsoleV1Nodes.tsx`), Database (`ConsoleV1Database.tsx`) and Settings51 (`ConsoleV1Settings.tsx`) cover the record, list, tool and configuration52 cases.534. Build with primitives from `@temps-sdk/ds` only. Import the skin once54 (`@import '@temps-sdk/ds/op.css'`) and put `operator ink v1` on the55 root you want skinned, including portalled content.565. Apply the record recipe: title + meta → status verdict → `Lede` with four57 to six facts → `Columns` (main: the thing and its timeline; aside: what is58 left) → sections. A fact appears once. Colour only through `Status`, as59 glyph + word + tone. Icons say what kind, glyphs say what state.606. Wire every drawn control. A `Kbd` badge needs a handler, a filter must61 filter, a destination is a typed `/${string}` path, never `#`. The ledger62 cursor moves DOM focus.637. Check both widths, both modes: 1440 and 390, light and dark. Below md,64 ledger rows render `mobile` and it carries the row's primary action.6566## Adding a primitive67681. Build it in `web/packages/ds/src/*.tsx` and export it from `src/index.ts`.692. Give it a gallery block: a `<section id="…">` in70 `design-system/src/sections/blocks/*.tsx`, or in `OpComponents.tsx` for a71 primitive that belongs to no rule document. Show every state, not a happy72 path. Add its id and label to the page's TOC (`OpComponents.tsx`, or the73 `*_TOC` export the page spreads).743. Add the id to `BLOCKS` in `design-system/e2e/visual.spec.ts`, in page order.75 A `toEqual([...BLOCKS])` assertion compares that list against the sections76 the page renders, so the run stays red until both agree.774. Shoot the baseline: run the gallery tests, then adopt **only** the new78 block's actuals — copy each `-actual.png` Playwright wrote under79 `test-results/` over80 `design-system/e2e/__screenshots__/visual.spec.ts/<name>-<project>.png`.81 Never a blanket `--update-snapshots`: it rewrites ~85 blocks for sub-pixel82 noise and the diff you were meant to read drowns.835. Write the handoff §6 entry: what it is for, what it refuses to do, its84 states.856. Add the `CHANGELOG.md` line in `web/packages/ds/`.8687## Before you ship8889Run from `design-system/`:9091```bash92bun run lint # tsc --noEmit + scripts/audit-records.mjs + tokens.mjs check93bun run e2e # overflow at 390/1440, keyboard, drop focus, reload signatures, axe, visual94```9596Both must be clean. Fix dev-console warnings from `Lede` and `Detail`. When a97visual baseline changes on purpose, adopt the actuals as in step 4 above and98say which blocks moved, and why, in the PR.99100`audit-records.mjs` audits `src/sections` by default and takes `--dir <path>`101(repeatable) for any other folder of screens:102103```bash104node scripts/audit-records.mjs --dir ../examples/example-plugin/web/src105```106107**Know what green proves.** Lint-enforced: types (`tsc`), the record recipe108(`audit-records.mjs`, literal-only and heuristic on two of its rules), and109`tokens.json` against `op.css` (`tokens.mjs check`). E2E-enforced: no110horizontal scroll at 390/1440 with a clean console, no new serious/critical axe111violation in light and dark, the keyboard contract, reload signatures, and the112visual baselines.113114Honour system — nothing fails if you break these: paper and ink only, no second115hue, colour only through `Status`, no cards and one `.op-raise` per screen, no116hex / `oklch()` / palette literal / `ms` literal in a `.tsx`, the closed117spacing scale, every drawn control wired, view state in the URL beyond what118`state.spec.ts` samples, and the words (`content.md`, `localisation.md`,119`icons.md`). A green lint means the types, the recipe and the tokens hold; it120does not mean the screen follows the design system. Read it yourself.121122## Plugin UI123124A plugin's UI is a **separate document**: `web/src/pages/plugins/PluginPage.tsx`125mounts it in a same-origin iframe at `/api/x/{plugin}/ui/`, and the plugin126serves its own HTML, JS and CSS. Nothing crosses that boundary — not the127console's stylesheet, not the `operator ink v1` root, not the fonts, not the128Tailwind build that generated the utilities the primitives use. A plugin that129assumes it inherits the skin renders unstyled.130131So a plugin sets the system up for itself, like any outside app. It is a plain132Vite + React app (`examples/example-plugin/web/` is the shape), so133`web/packages/ds/README.md` is the setup, verbatim:1341351. `bun add @temps-sdk/ds` plus its peer dependencies, and **pin the version136 the console ships** — two versions of the skin side by side drift in a way137 that reads as "this page looks slightly wrong", not as a bug.1382. `@import '@temps-sdk/ds/op.css'` at the top of the entry stylesheet, and139 `@source "../node_modules/@temps-sdk/ds/dist"` so the consumer's Tailwind140 scans the package and generates the utilities it renders.1413. `operator ink v1` on the plugin's own root, and on any portalled content.1424. **Theme:** there is no theme channel today. `PluginPage.tsx` syncs the route143 (hash or `postMessage`) and nothing else, so read `prefers-color-scheme` for144 now and toggle `.dark` from it; theme sync from the parent is a follow-up145 (handoff §15).1465. Same conventions as a console screen: `CopyAction` for a copy, `Button busy`147 for an action in flight, `useUrlState` for the view. A plugin's route is148 mirrored into the console's address bar, so a plugin that keeps its facet in149 React state produces a link that does not reopen what the reader was looking150 at.151152Handoff §3b says the same thing at length.153154## Changing a rule155156A rule changes in one commit that edits `brand-guidelines.md`,157`design-system-handoff.md`, `RULES.md` and the reference page together. A158rule stated in only one place is not a rule.