fix(theme): custom themes cover all twenty-one roles #210

Merged
nalum merged 1 commit from fix/custom-theme-roles into main 2026-08-26 18:33:51 +00:00
Owner

Closes #202.

#207 took the palette from nine roles to twenty-one. A household's custom
theme still overrode nine, so the other twelve fell back to the family
theme — and a dark custom theme on a light family kept that family's light
surface-raised and error-surface. A pale edit box on a dark card, and
an invalid-row ring on a ground nobody had checked.

The generator guarantees WCAG AA for everything it emits, but it runs at
build time over authored values. A custom theme escaped that guarantee for
twelve of twenty-one roles, silently, and would have started showing the
moment a component consumed them.

Eleven roles are now derived from the household's anchors and clamped at
runtime against the same thresholds cmd/tokengen enforces, including
border-strong at the 3:1 non-text floor — a checkbox outline is a mark,
not text. The clamp walks toward black and toward white a fiftieth at a
time and takes the first step that clears every ground: the smallest
nudge, not a replacement. Where no nudge reaches the floor it keeps the
closest and never blocks; a separate audit then names which of the
household's own colours to move, in plain words.

--warning is the twelfth, and it is asked rather than derived.
Deriving it fails exactly where it matters: a family whose celebrate is
pink or blue has nothing warm to turn toward, which is why two built-in
themes needed hand-tuning when the roles were authored. It joins
background, text and accent as a fourth anchor — the workshop asks for
four decisions, not twenty-one.

The border constants were fitted, not invented: 0.20 and 0.45 reproduce
hearth's authored values exactly in both modes, so a custom theme lands
where a hand-authored one would.

The rules exist twice, in TypeScript and Kotlin, because the derivation
runs at runtime on each client — the workshop re-derives on every
colour-well change, and the phone completes a stored map offline.
tokengen renders build-time constants and cannot produce a runtime
function, so the repo's existing answer for twice-implemented logic holds
them together: conformance/theme-derive.json.

A vocabulary drift gate now fails the suite if the client's token list and
the generated CSS disagree — the failure mode this PR fixes cannot recur
silently.

Existing custom themes keep every value they chose.

🤖 Generated with Claude Code

Closes #202. #207 took the palette from nine roles to twenty-one. A household's custom theme still overrode nine, so the other twelve fell back to the family theme — and a dark custom theme on a light family kept that family's light `surface-raised` and `error-surface`. A pale edit box on a dark card, and an invalid-row ring on a ground nobody had checked. The generator guarantees WCAG AA for everything it emits, but it runs at build time over authored values. A custom theme escaped that guarantee for twelve of twenty-one roles, silently, and would have started showing the moment a component consumed them. Eleven roles are now derived from the household's anchors and clamped at runtime against the same thresholds `cmd/tokengen` enforces, including `border-strong` at the 3:1 non-text floor — a checkbox outline is a mark, not text. The clamp walks toward black and toward white a fiftieth at a time and takes the first step that clears every ground: the smallest nudge, not a replacement. Where no nudge reaches the floor it keeps the closest and never blocks; a separate audit then names which of the household's own colours to move, in plain words. `--warning` is the twelfth, and it is **asked rather than derived**. Deriving it fails exactly where it matters: a family whose celebrate is pink or blue has nothing warm to turn toward, which is why two built-in themes needed hand-tuning when the roles were authored. It joins background, text and accent as a fourth anchor — the workshop asks for four decisions, not twenty-one. The border constants were fitted, not invented: 0.20 and 0.45 reproduce hearth's authored values exactly in both modes, so a custom theme lands where a hand-authored one would. The rules exist twice, in TypeScript and Kotlin, because the derivation runs at runtime on each client — the workshop re-derives on every colour-well change, and the phone completes a stored map offline. `tokengen` renders build-time constants and cannot produce a runtime function, so the repo's existing answer for twice-implemented logic holds them together: `conformance/theme-derive.json`. A vocabulary drift gate now fails the suite if the client's token list and the generated CSS disagree — the failure mode this PR fixes cannot recur silently. Existing custom themes keep every value they chose. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(theme): custom themes cover all twenty-one roles
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m59s
check / report (pull_request) Successful in 4s
android / build (pull_request) Successful in 6m16s
android / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m9s
2897911567
design/tokens.yaml gives every variant twenty-one token roles and
cmd/tokengen refuses to publish a theme whose text pairs fall below WCAG
AA. A household's custom theme escaped that gate for twelve of them: it
overrode nine roles plus --radius, so a dark custom theme built on a
light family kept that family's light --surface-raised and
--error-surface — a pale edit box on a dark card, an invalid-row ring on
a ground nobody checked. Nothing looks broken today only because no
component consumes the new roles yet; the next UI ticket does.

Every role a stored map lacks is now derived from the ones it carries,
on both surfaces, and each derived pair is clamped at runtime to the
same floors checkContrast enforces at build time: the value walks toward
black and toward white a fiftieth at a time and takes the smallest step
that clears the pair. Where no step reaches it — the household's own
anchors leave no room — the workshop says so in plain words, naming the
colour to move and which way, rather than shipping a pair nobody can
read. It warns; it still never blocks an edit.

--warning is asked for, not derived. A family whose --celebrate is pink
or blue has nothing warm to turn toward, and "amber, never red" cannot
be mixed out of a red and a pink; two built-in themes needed hand-tuning
for exactly this when the roles were authored. It joins Background, Text
and Accent as a fourth anchor, seeded from the base variant. The other
eleven stay invisible: the workshop asks for four decisions, not
twenty-one.

The rules are written twice — TypeScript for the workshop and the
browser, Kotlin because the phone completes a stored map offline with no
web in reach — and held to identical output by
conformance/theme-derive.json, the repo's existing answer to
twice-implemented logic. Stored maps need no proto change and no
migration (ADR-0035): an older record simply carries fewer keys, keeps
every value it chose, and the rules fill the rest.

Test report

Suite Tests Result Skipped
Unit 1434 ✅ pass 1
Integration 130 ✅ pass —

Coverage: 27.0%

Updated by the check workflow · commit 918ded3ca4

<!-- ci-test-report --> ## Test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit | 1434 | ✅ pass | 1 | | Integration | 130 | ✅ pass | — | **Coverage:** 27.0% <sub>Updated by the check workflow · commit 918ded3ca42ed2130fc75cbffe7947cdb31ae3c9</sub>

Android test report

Suite Tests Result Skipped
Unit (debug) 51 ✅ pass 0

Coverage: 10.3% of lines

Updated by the android workflow · commit 918ded3ca4

<!-- android-test-report --> ## Android test report | Suite | Tests | Result | Skipped | | --- | --: | --- | --: | | Unit (debug) | 51 | ✅ pass | 0 | **Coverage:** 10.3% of lines <sub>Updated by the android workflow · commit 918ded3ca42ed2130fc75cbffe7947cdb31ae3c9</sub>
nalum force-pushed fix/custom-theme-roles from 2897911567
All checks were successful
check / commits (pull_request) Successful in 7s
check / go (pull_request) Successful in 2m59s
check / report (pull_request) Successful in 4s
android / build (pull_request) Successful in 6m16s
android / report (pull_request) Successful in 3s
check / web (pull_request) Successful in 4m9s
to 918ded3ca4
Some checks failed
check / commits (pull_request) Successful in 19s
check / go (pull_request) Successful in 2m48s
android / build (pull_request) Successful in 7m2s
check / web (pull_request) Successful in 5m8s
check / report (pull_request) Successful in 5s
android / report (pull_request) Successful in 4s
android / report (push) Has been cancelled
check / report (push) Has been cancelled
check / web (push) Has been cancelled
tag / tag (push) Has been cancelled
android / build (push) Has been cancelled
check / go (push) Has been cancelled
check / commits (push) Has been cancelled
2026-08-26 07:03:58 +00:00
Compare
nalum changed target branch from feat/occurrence-attendance to main 2026-08-26 18:33:48 +00:00
nalum merged commit 918ded3ca4 into main 2026-08-26 18:33:51 +00:00
nalum deleted branch fix/custom-theme-roles 2026-08-26 18:33:54 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
eagraiclainne/app!210
No description provided.