Define colours in terms of role-based tokens - #99
Open
microbit-matt-hillsdon wants to merge 8 commits into
Open
Conversation
Recipes and components consume roles, not ramp stops. Phase 3 of the focus-and-colour plan; the design note is ui-private's docs/role-tokens.md. Roles are grouped by the property they belong to — fg, surface, fill, border — so a foreground role cannot be used as a background. The surface/fill split (Polaris's, and Carbon and Primer draw the same line in different words) is what makes the two grey state ladders coherent: rows on a raised surface hover to gray.50, a grey button fill hovers to gray.300. They were never inconsistent, just on different axes. button.* survives as a component group rather than dissolving into the roles, restructured to button.primary.bg / button.secondary.border and so on. Primer and Carbon both keep button tokens out of their functional layer, and the family needs it: half the apps are brand-coloured and half black-on-white, and the two idioms differ in structure, not just hue — one darkens the border and keeps the background clear, the other holds the border and washes the background. No numeric ramp carries both. languageDialog.* and closeButton.* join it for the same reason. The ramps stay public and unrenamed, so raw var(--colors-gray-*) in consumers' plain CSS (python-editor's CodeMirror themes) is unaffected. Value-preserving, measured: Panda's generated CSS resolved through every var() chain shows 0 changed declarations across 916 selectors, and every renamed atomic class resolves to a byte-identical value. Two deliberate exceptions in LanguageDialog, made rather than adding two roles for one call site each — the "help translate" link moves brand.500 to fg.link (brand.600, the link colour everywhere else) and the English-name label moves gray.700 to fg.muted. The two tests updated asserted the old token names, not behaviour.
Two live defects, both because a role can carry an _onDark condition and a raw ramp stop cannot. Phase 1 built the condition for focus; this is the rest of it. - `ghost` was black text with a black wash: invisible on dark chrome. That is why python-editor carries its own `sidebar` variant. It now renders white with a white wash inside a tagged surface. This does not retire `sidebar` — its hover is a solid white pill and this is a 0.08 wash, which is a design change rather than a substitution — but the variant is now optional rather than necessary. - The Toast close button's blackAlpha.100 hover darkened a dark teal toast, reading as a hole. The card was already tagged, so the flip fixes it with no call-site change. Alphas are not mirrored. A white wash reads weaker than a black one at equal alpha, so the on-dark values are two ramp steps up: 0.08/0.16 against 0.04/0.06 for `ghost`, 0.16/0.24 for the close buttons. 0.08 is Material's hover figure. These want an eyeball in Storybook. `toolbar` had to be pulled out of the roles to get here. It is a white pill that lives ON dark chrome, so it sits inside the tag while being light itself, and a flipping `fg` role rendered its label white on white. It now reads a static buttonToolbar.* component group, and the rule generalises: anything painting its own contrasting fill must opt out of the flips. The whole mechanism is seven custom-property redefinitions in one [data-surface="dark"] block. Resolved-CSS diff against main: still 0 changed declarations in the light context; the dark context is Phase 1's focus flips plus exactly five new ones.
The old comment described how each consumer resolves the token, which was accurate but left the impression the value was arbitrary. It descends from a Chakra-era `language` button variant that @microbit/ui-patterns' shared LanguageDialog replaced, and both overriding presets carried its wording forward with it (corrected separately in data-microbit-org and ml-trainer-microbit). The divergence is real regardless of where it came from, and worth stating in its own terms: data-microbit-org uses black because black is its emphasis colour throughout, while its links stay blue — which is exactly why this must not fold into fg.link. The exception is fgHover, which has no live justification: it dates from when this was a text link with no background, and the card now hovers its own background. Both overriding presets set it equal to fg. Kept because dropping it is a visible change in the three apps still on the default, and the decision is to preserve the current look per app.
The contract still described the pre-role-token world. It named
`languageText`, `statusBarBg`, `toast*Bg` and `controlCheckedBg` as the
extension points a brand preset overrides — all four deleted in the role
token work — and gave `focusBorder` a base of brand.700, which is 600.
That is not just stale. A brand preset author following it writes a key
Panda accepts and silently ignores, which is how python-editor-v3-microbit
ended up with four dead toast overrides and its brand build falling back
to the library's teal.
So the section now says what the layers are rather than listing leaves:
ramps versus roles, the four property-namespaced role groups and what the
surface/fill split buys, the four component groups and why an idiom gets
one, and a pointer to base-preset.ts as authoritative for the inventory.
A list here goes stale; that is the failure being fixed, so it should not
be replaced with another list.
Adds the two silent failures explicitly — an unknown `semanticTokens` key
and a missed `colorPalette` stop — with the instruction that a green
typecheck does not verify a preset change, and the cssgen command that
does.
Lists all seven `{ base, _onDark }` tokens rather than the two focus ones,
and records the corollary: a component painting its own contrasting fill
must not consume them, which is what `buttonToolbar.*` is for.
Completes the last Phase 3 exit criterion.
Panda accepts an unknown `semanticTokens` key without complaint: it
typechecks, it generates, and the override never applies — the token keeps
this preset's value with no error at build or runtime. So a rename here
silently un-does every downstream override of the old name.
That has now happened twice. data-microbit-org's `button.*` block was
nearly missed when the flat keys became nested, which would have reverted
its black buttons to brand blue. python-editor-v3-microbit's four
`toast*Bg` overrides actually were missed, and its brand build's toasts
fell back to the library's teal until this week. Neither was visible to a
typecheck; only a resolved-CSS comparison found them.
`unknownSemanticTokens(preset, { introduces })` returns the leaf paths a
preset sets that this library does not define and that the preset has not
declared as its own. It reads `theme.semanticTokens` and
`theme.extend.semanticTokens`, and stops at Panda's `value` key so a
`{ base, _onDark }` condition object counts as one leaf rather than two.
It cannot be a Panda plugin: the check has to run per consumer, because
the library cannot see the presets merged after it. So it ships as an
export and each consumer asserts in a test.
Verified against the real defect — checked out at the commit before the
fix, it names all four dead keys.
# Conflicts: # packages/ui/src/Avatar.recipe.ts
The role-token sweep converted every recipe but left globalCss painting the body with gray.800/white and *::placeholder with gray.500 — the canonical fg.default, surface.canvas and fg.placeholder call sites, and the ones a dark mode most needs on roles. The full-size Modal's safe-area gradient similarly hardcoded white next to token(colors.surface.statusBar); it now uses surface.raised, the dialog's own surface, so the gradient can't diverge from the panel it caps. Value-identical throughout: every role referenced resolves to the ramp stop it replaces.
unknownSemanticTokens covers unknown names, but the other silent
preset failure is one door over: overriding a { base, _onDark } token
with a flat value (or a condition object missing a key) merges
wholesale and silently loses the flip. droppedConditionTokens flags
any override of a base-preset condition token that does not keep all
of its condition keys; a deliberate no-flip is stated with both keys
equal. Documented beside the seven-token list in the README, which
this check also keeps from going stale.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I'm sure we'll end up revisiting this but it's a start. There are draft app PRs that can be linked with it to explore it in that context.