Skip to content

Commit fb6858f

Browse files
alicodingclaude
andauthored
feat: design wave 2 — identity tokens: accent scale, StatusStamp, mono utility (goal 0001) (#48)
A full-app design audit (screenshots, both themes) found pill/color/ typography drift beyond wave 1's convention fixes: 7 independent ad-hoc `<Label variant="...">` pill families with no shared vocabulary, node-kind canvas colors literally aliasing Primer's status-semantic tokens (a process-kind node and a success-status pill rendered the identical green -- "green triple duty"), and machine-readable facts (URLs, IDs, commands, timestamps) rendering in body text with nothing to mark them as such. 1. Accent scale: Mill's own desaturated verdigris/teal, layered over Primer's functional accent tokens (frontend/src/app/mill-tokens.css) so every Primer component reading them (links, focus rings, selected tabs, the sidebar active row) picks it up for free. Deliberately a separate file imported AFTER @primer/primitives' theme CSS in main.tsx -- Primer's own light.css sets these tokens on its ThemeProvider's internal wrapper <div> directly (not just :root), so a same-specificity override has to win the cascade by load order; caught by a computed-style e2e proof against the real compiled page after a specificity-only attempt looked correct on paper and still lost. 2. Node-Kind canvas colors (composition/nodeKind.ts) decoupled from status semantics: own fixed --mill-kind-* tokens, contrast-checked independently, never aliased to a Primer semantic token or the accent scale. 3. shared/StatusStamp.tsx: one stamp component replacing the 7 pill families across the app, five semantic variants only (success/ caution/danger/neutral/identity). Primer Label stays for genuine categorization tags (auth type, field In/Type) -- StatusStamp is for STATE, Label stays for CATEGORY. Also closed two stray un-migrated key-combo sites (NodeInspector, SettingsView) onto wave 1's KeyComboChip, a gap wave 1's own commit message had claimed closed. 4. shared/monoText.module.css: a mono utility class applied to MCP server commands, AI provider/Integration base URLs, run timestamps, ExecEnv directories, Decision output keys. Full local gate green (tsc/eslint/boundaries/vitest/check-loc/golangci- lint/go vet/go build desktop+server/go test/ls_lint) plus a full Playwright e2e run (196/196, 3 pre-existing timing-flake tests confirmed via isolated reruns, none touching a Label/StatusStamp testid or variant). docs/SPEC.md §3.8-area, goal 0001, and BACKLOG annotated; goal stays OPEN pending wave 3 (palette IA) and the owner's live sign-off. Claude-Session: https://claude.ai/code/session_018pkViCNAuZp2vBv2K9AbUh Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent e5e53d4 commit fb6858f

36 files changed

Lines changed: 579 additions & 158 deletions

docs/SPEC.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4503,6 +4503,57 @@ surface can already express.
45034503
authoring UI — Phase B's own bigger, genuinely separate design
45044504
surface, tracked in the ADR, not silently folded into what shipped.
45054505

4506+
- **Design system tokens, `LOCKED`, built (goal 0001, design wave 2,
4507+
2026-08-13) — a deliberate identity layer over Primer, not
4508+
per-surface ad hoc styling.** Prompted by a full-app design audit
4509+
(screenshots, both themes) finding pill/color/typography drift that
4510+
outlived wave 1's convention fixes above. Three pieces, all
4511+
app/frontend/src/app/mill-tokens.css + shared/:
4512+
- **Accent scale**: a desaturated verdigris/teal (`#1F6F6B` light /
4513+
`#3FA39E` dark text, `#2B7D77` dark fill — the two dark shades
4514+
split for contrast reasons Primer's own dark theme already
4515+
demonstrates: a fill needs contrast FOR white content painted on
4516+
it, text needs contrast AGAINST the page behind it) layered over
4517+
Primer's functional `--bgColor-accent-*`/`--borderColor-accent-*`/
4518+
`--fgColor-accent` tokens, so every Primer component reading them
4519+
(links, focus rings, selected tabs, the sidebar active row) picks
4520+
it up for free. Lives in its own `mill-tokens.css`, imported
4521+
*after* `@primer/primitives`' theme CSS in `main.tsx` — Primer's
4522+
own light.css sets these tokens on three selectors including one
4523+
matching its ThemeProvider's internal wrapper `<div>` directly
4524+
(not just `:root`), so a same-specificity override has to win by
4525+
load order, not by a selector-specificity trick alone (an earlier
4526+
attempt at boosted specificity looked correct on paper and still
4527+
lost the real cascade — caught by a computed-style e2e proof
4528+
against the compiled page, not assumed from reading the CSS).
4529+
- **Node-Kind canvas colors decoupled from status semantics**
4530+
(`composition/nodeKind.ts`'s `KIND_ICON_BG`, confined to
4531+
canvas/palette only): previously aliased Primer's own
4532+
`--bgColor-success-emphasis`/`--bgColor-attention-emphasis`/etc.
4533+
directly, so a `process`-kind node card and a `success`-status
4534+
pill rendered the literal same green ("green triple duty") — now
4535+
each kind has its own fixed `--mill-kind-*` custom property,
4536+
contrast-checked independently, never aliased to a Primer semantic
4537+
token or the accent scale above.
4538+
- **`shared/StatusStamp.tsx`**: one stamp component (mono uppercase
4539+
label, slightly-squared radius, subtle border) replacing 7 ad-hoc
4540+
`<Label variant="...">` pill families across the app (workflow
4541+
live/draft/disabled, run SUCCESS/ERROR, guardrail approved/denied,
4542+
built-in badges, trigger armed/not-live, run kind, dev-build
4543+
identity). Five semantic variants only — `success`/`caution`/
4544+
`danger`/`neutral`/`identity` — deliberately fewer than Primer's
4545+
own variant palette, so two different pill families can never
4546+
drift onto different colors for the same meaning again. Primer's
4547+
`Label` stays in use for genuine categorization tags (auth type,
4548+
field `In`/`Type`, a Decision's category column) — StatusStamp is
4549+
for STATE, Label stays for CATEGORY, not a wholesale replacement.
4550+
- **Mono utility** (`shared/monoText.module.css`'s single `.mono`
4551+
class, a `--mill-mono` ui-monospace stack): applied to
4552+
machine-readable facts — MCP server commands, AI provider/
4553+
Integration base URLs, run timestamps, ExecEnv directories,
4554+
Decision output keys — a class, not a component, since the only
4555+
thing every call site needs is one CSS property.
4556+
45064557
## 11. Enterprise / regulated deployment readiness
45074558

45084559
`OPEN` throughout — this section is Research only. Nothing here is

docs/goals/0001-authoring-surface-overhaul.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,60 @@ the real Settings dark-theme toggle and asserts computed
137137
than a manual-only note — Settings already ships a working dark-theme
138138
`SegmentedControl`, so this was e2e-feasible, not desktop-only.
139139

140+
## Design wave 2 — delivered (2026-08-13)
141+
142+
The taste-layer pass named as a separate later piece in wave 1's own
143+
note above: identity tokens across the whole app (not just the
144+
authoring surface), driven by the same full-app design audit's
145+
sections 1/2/6. Two sequential PRs; this is PR 1. Full detail in
146+
`docs/SPEC.md` §3.8-area's new "Design system tokens" bullet — summary
147+
here:
148+
149+
1. **Accent scale**: Mill's own desaturated verdigris/teal, layered
150+
over Primer's functional accent tokens (`app/mill-tokens.css`) so
151+
every Primer component reading them picks it up for free. Proven
152+
via a computed-style e2e test against the real compiled page after
153+
a same-specificity attempt first looked correct on paper and still
154+
lost the cascade.
155+
2. **Node-Kind canvas colors decoupled from status semantics**
156+
(`composition/nodeKind.ts`): no longer aliased to Primer's shared
157+
`--bgColor-success-emphasis`/etc. — own fixed `--mill-kind-*`
158+
tokens, closing the audit's "green triple duty" finding (a
159+
`process`-kind node and a `success`-status pill used to be the
160+
literal same green).
161+
3. **`shared/StatusStamp.tsx`**: one stamp component replacing 7 ad-hoc
162+
`<Label variant="...">` pill families (workflow live/draft/
163+
disabled, run SUCCESS/ERROR, guardrail approved/denied, built-in,
164+
trigger armed/not-live, run kind, dev-build identity) — five
165+
semantic variants only (`success`/`caution`/`danger`/`neutral`/
166+
`identity`). Primer `Label` stays for genuine categorization tags
167+
(auth type, field `In`/`Type`) — not a wholesale replacement.
168+
Two stray un-migrated key-combo sites found during the sweep
169+
(`NodeInspector.tsx`'s per-node hotkey editor, `SettingsView.tsx`'s
170+
global summon hotkey) now render through wave 1's own
171+
`KeyComboChip` too, closing a gap wave 1's commit message claimed
172+
was already closed everywhere.
173+
4. **Mono utility** (`shared/monoText.module.css`): applied to MCP
174+
server commands, AI provider/Integration base URLs, run timestamps,
175+
ExecEnv directories, Decision output keys.
176+
177+
Proofs: `e2e/dark-mode.spec.ts` gained two new tests — one asserting
178+
the real computed `--bgColor-accent-emphasis`/`--fgColor-accent`
179+
values in both themes (the actual cascade-order regression this
180+
caught), one asserting a rendered StatusStamp's computed color.
181+
Existing suites green (196/196 e2e, full Go suite, vitest, tsc,
182+
eslint, boundaries, check-loc) — 3 pre-existing timing-flake e2e tests
183+
unrelated to this change (confirmed via isolated reruns, none touching
184+
a Label/StatusStamp testid or variant). Screenshots: `scratchpad/audit/
185+
*-after.png` (workflows list, Configure MCP Servers, Settings, light +
186+
dark) for the owner's review against the `-light.png`/`-dark.png`
187+
before-references from the original audit.
188+
189+
Goal stays OPEN — wave 3 (palette IA regrouping, Configure Attributes
190+
tab conformance) is the second PR of this same design pass, and the
191+
owner's own live sign-off (this goal's stated Acceptance) hasn't
192+
happened yet.
193+
140194
## Live-review additions (2026-08-10)
141195
- Hotkey recorder vs native menu accelerators (owner hit ⌘⇧W while
142196
recording — the window closed, and since Mill exits on last-window-

docs/goals/BACKLOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ live-review material, interleaved during owner reviews, not a lane.**
208208
11. [ ] 0030 second-pass linters (gocritic/prealloc/contextcheck/sqlclosecheck — named future work in goal 0028) + `.ls-lint.yml` gains a root `node_modules` ignore (gap found 2026-08-12: a stray root node_modules broke root-file-naming; tiny, rides this or any PR).
209209

210210
**Owner-needed lane (parallel, never blocks the queue)**
211-
- [ ] [0001 — Authoring-surface overhaul](0001-authoring-surface-overhaul.md) (spacing audit + §3.8 prototype elements — live-review material, needs the owner driving; design wave 1 DELIVERED 2026-08-12 — 7 app-wide convention/bug fixes from a full-app audit screenshot pass, zero taste calls; the taste-layer wave 2/3 pending, goal stays open)
211+
- [ ] [0001 — Authoring-surface overhaul](0001-authoring-surface-overhaul.md) (spacing audit + §3.8 prototype elements — live-review material, needs the owner driving; design wave 1 DELIVERED 2026-08-12 — 7 app-wide convention/bug fixes from a full-app audit screenshot pass, zero taste calls; design wave 2 DELIVERED 2026-08-13 — identity tokens: Mill's own accent scale layered over Primer, node-kind canvas colors decoupled from status semantics, one StatusStamp component replacing 7 pill families, a shared mono-font utility; wave 3 (palette IA regroup, Configure Attributes conformance) next, goal stays open pending the owner's live sign-off)
212212
- [ ] Owner hands-on pass over the week's shipped features (Quick Panel focus fix, view-mode hardening, inline hotkey hints, seed reset/restore)
213213

214214
**Delivered**

frontend/e2e/dark-mode.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,54 @@ test('the canvas minimap is themed in dark mode, not React Flow\'s light default
5757
// dark in dark mode.
5858
expect(bg).not.toBe('rgb(255, 255, 255)')
5959
})
60+
61+
// Design wave 2: Mill's own teal accent (app/index.css) is layered
62+
// over Primer's shared --bgColor-accent-emphasis/-muted,
63+
// --borderColor-accent-emphasis/-muted, --fgColor-accent tokens via
64+
// `html:root`/`html[data-color-mode=...]` selectors carrying one extra
65+
// specificity segment over Primer's own bare `:root`/attribute
66+
// selectors -- this proves that actually wins the real cascade in a
67+
// live browser (not just "should win" from reading the CSS), in both
68+
// themes, rather than trusting the specificity math alone.
69+
test('Mill\'s teal accent tokens override Primer\'s default blue, light and dark', async ({ page }) => {
70+
await page.goto('/')
71+
72+
const readAccent = () => page.evaluate(() => {
73+
const style = getComputedStyle(document.documentElement)
74+
return {
75+
emphasis: style.getPropertyValue('--bgColor-accent-emphasis').trim(),
76+
fg: style.getPropertyValue('--fgColor-accent').trim(),
77+
}
78+
})
79+
80+
const light = await readAccent()
81+
expect(light.emphasis.toLowerCase()).toBe('#1f6f6b')
82+
expect(light.fg.toLowerCase()).toBe('#1f6f6b')
83+
// Never Primer's own default accent blue -- the actual regression
84+
// this guards (a same-specificity override losing to import order).
85+
expect(light.emphasis.toLowerCase()).not.toBe('#0969da')
86+
87+
await switchToDarkTheme(page)
88+
const dark = await readAccent()
89+
expect(dark.emphasis.toLowerCase()).toBe('#2b7d77')
90+
expect(dark.fg.toLowerCase()).toBe('#3fa39e')
91+
expect(dark.emphasis.toLowerCase()).not.toBe('#1f6feb')
92+
})
93+
94+
// StatusStamp (design wave 2, goal 0001 audit §1): a real rendered
95+
// stamp resolves to the semantic colors its variant promises, and the
96+
// `identity` variant specifically resolves to Mill's own teal accent
97+
// rather than Primer's default blue -- the concrete "green triple
98+
// duty"-style collision this component exists to prevent, proven for
99+
// the accent/identity pairing the same way the kind-color test proves
100+
// it for canvas nodes.
101+
test('StatusStamp variants resolve to distinct, correct colors', async ({ page }) => {
102+
await page.goto('/')
103+
await page.getByRole('link', { name: 'Workflows' }).click()
104+
105+
const builtInStamp = page.locator('[data-testid="inventory-row"][data-entity="workflow"] [data-variant="identity"]').first()
106+
await expect(builtInStamp).toBeVisible()
107+
const color = await builtInStamp.evaluate((el) => getComputedStyle(el).color)
108+
// rgb(31, 111, 107) == #1f6f6b, the light-theme accent fg.
109+
expect(color).toBe('rgb(31, 111, 107)')
110+
})

frontend/src/app/App.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { useState, useEffect, useRef } from 'react'
22
import { useTranslation } from 'react-i18next'
33
import {Events, WML} from "@wailsio/runtime";
4-
import {Label, PageLayout, useTheme} from "@primer/react";
4+
import {PageLayout, useTheme} from "@primer/react";
5+
import { StatusStamp } from '../shared/StatusStamp'
56
import HomeView from "../views/HomeView";
67
import ActivityView from "../views/ActivityView";
78
import ReviewView from "../views/ReviewView";
@@ -466,9 +467,9 @@ function App() {
466467
time by the band-tracking e2e's intercepted clicks. The
467468
footer never moves. */}
468469
{isIsolatedData && (
469-
<Label variant="accent" size="small" data-testid="isolated-data-badge">
470+
<StatusStamp variant="identity" data-testid="isolated-data-badge">
470471
{t('shell.testDataBadge')}
471-
</Label>
472+
</StatusStamp>
472473
)}
473474
</span>
474475
<span className={styles.time}>

frontend/src/app/BuildIdentityBadge.tsx

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useTranslation } from 'react-i18next'
2-
import { Label } from '@primer/react'
2+
import { StatusStamp } from '../shared/StatusStamp'
33
import { SettingsService } from '../shared/bindings'
44
import type { BuildInfo } from '../shared/bindings'
55
import { useGoSourceStale } from './goLiveness'
@@ -62,19 +62,19 @@ export function BuildIdentityBadge({ buildInfo }: { buildInfo: BuildInfo | null
6262
// amber (unlike green/red) isn't self-explanatory at a glance.
6363
if (goSourceStale) {
6464
return (
65-
<Label
66-
variant="attention" size="small" className={styles.devRibbon}
65+
<StatusStamp
66+
variant="caution" className={styles.devRibbon}
6767
data-testid="dev-go-stale-badge"
6868
title={t('buildIdentityBadge.goStaleTooltip')}
6969
>
7070
{t('buildIdentityBadge.devGoStale')}
71-
</Label>
71+
</StatusStamp>
7272
)
7373
}
7474
return (
75-
<Label variant="success" size="small" className={styles.devRibbon} data-testid="dev-build-badge">
75+
<StatusStamp variant="success" className={styles.devRibbon} data-testid="dev-build-badge">
7676
{t('buildIdentityBadge.devLive')}
77-
</Label>
77+
</StatusStamp>
7878
)
7979
}
8080

@@ -86,32 +86,32 @@ export function BuildIdentityBadge({ buildInfo }: { buildInfo: BuildInfo | null
8686
// the bundle got newer). A server-mode browser tab only informs: it
8787
// must never kill the shared server.
8888
return isNativeWebview ? (
89-
<Label
90-
variant="danger" size="small"
89+
<StatusStamp
90+
variant="danger"
9191
className={`${styles.devRibbon} ${styles.devRibbonAction}`}
9292
data-testid="stale-build-badge"
9393
onClick={() => { void SettingsService.QuitApp() }}
9494
>
9595
{t('buildIdentityBadge.staleBuildClickToClose', { binaryHead, repoHead: __MILL_REPO_HEAD__ })}
96-
</Label>
96+
</StatusStamp>
9797
) : (
98-
<Label variant="danger" size="small" className={styles.devRibbon} data-testid="stale-build-badge">
98+
<StatusStamp variant="danger" className={styles.devRibbon} data-testid="stale-build-badge">
9999
{t('buildIdentityBadge.staleBuildRestart', { binaryHead, repoHead: __MILL_REPO_HEAD__ })}
100-
</Label>
100+
</StatusStamp>
101101
)
102102
}
103103

104104
if (isNativeWebview) {
105105
return (
106-
<Label variant="secondary" size="small" className={styles.devRibbon} data-testid="installed-build-badge">
106+
<StatusStamp variant="neutral" className={styles.devRibbon} data-testid="installed-build-badge">
107107
{t('buildIdentityBadge.installed')}{binaryHead ? ` · ${binaryHead}` : ''}
108-
</Label>
108+
</StatusStamp>
109109
)
110110
}
111111

112112
return (
113-
<Label variant="secondary" size="small" className={styles.devRibbon} data-testid="server-build-badge">
113+
<StatusStamp variant="neutral" className={styles.devRibbon} data-testid="server-build-badge">
114114
{t('buildIdentityBadge.server')}{binaryHead ? ` · ${binaryHead}` : ''}
115-
</Label>
115+
</StatusStamp>
116116
)
117117
}

frontend/src/app/index.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@
3333
-webkit-text-size-adjust: 100%;
3434
}
3535

36+
/* Mill's own accent + node-kind + mono tokens live in mill-tokens.css,
37+
imported from main.tsx AFTER @primer/primitives' theme CSS
38+
(deliberately, not here) -- see that file's header comment for why
39+
this needed a real separate file rather than living in this one. */
40+
3641
* { box-sizing: border-box; }
3742

3843
html {

frontend/src/app/main.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import './index.css'
55
import '@primer/primitives/dist/css/primitives.css'
66
import '@primer/primitives/dist/css/functional/themes/light.css'
77
import '@primer/primitives/dist/css/functional/themes/dark.css'
8+
// AFTER Primer's own theme CSS, deliberately -- mill-tokens.css's own
9+
// header comment has the full reasoning (a same-specificity override
10+
// needs to win the cascade tie by load order here, not just win on
11+
// paper via a selector-specificity trick that turned out to target
12+
// the wrong element).
13+
import './mill-tokens.css'
814
import { ThemeProvider, BaseStyles } from '@primer/react'
915
import App from './App'
1016
import { QuickPanelApp } from './QuickPanelApp'

0 commit comments

Comments
 (0)