Skip to content

fix(cli): warn when a theme overrides one side of a colour pair - #5281

Closed
cixzhang wants to merge 1 commit into
mainfrom
fix/theme-contrast-pair-warning
Closed

fix(cli): warn when a theme overrides one side of a colour pair#5281
cixzhang wants to merge 1 commit into
mainfrom
fix/theme-contrast-pair-warning

Conversation

@cixzhang

@cixzhang cixzhang commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Closes #5014, the last unclaimed finding from the theme-authoring study (#5047).

expandColorScale holds generated text at 4.5:1 and control boundaries at 3:1 for any accent — but only for what it generated. Replace --color-accent in tokens and the --color-on-accent beside it still holds the value derived for the colour that is no longer there. The theme ships below-AA text without anyone having written a bad colour, and nothing says so until someone measures rendered pixels.

That is not hypothetical. In the study, a bedside medication-administration theme hand-tuned its dark accent, kept the generated on-accent, and rendered its primary button at 2.61:1 — measured off the DOM. Its author had run contrast math on the accent, against white, which is not what the theme renders. Every theme in that study that hand-wrote --color-border-emphasized also fell under 3:1; the ones that left neutrals to color: {} did not.

astryx theme build now resolves ten pairs — fill/label for accent and each status, text on body/surface/card, control boundary and focus ring on surface — in both colour modes, and warns with the token you forgot:

⚠ label on the accent fill is 2.61:1 in dark mode, below 4.5:1 —
  you set --color-accent but left --color-on-accent, which still holds the
  value generated for the colour you replaced

A warning, not a notice (where #5045 put the font advisory): a font a theme cannot load is the app's job by design, but this is the theme's own defect and fixable in the theme file.

The comparison keeps its full precision until after the threshold decision and uses core's shared color parser. A valid value the parser cannot measure now warns explicitly instead of reading as clean. theme build --check returns the same validation warnings as a normal build.

The scope decision, which is the whole design

Only pairs with exactly one side hand-written. Two neighbours are deliberately silent:

I built the wider version first and ran it over the bundled themes: all seven warn, 2–9 findings each. Some are real (chocolate's white-on-#FD0000 error label is 4.06:1). But stone's is the one that settled it — its --color-on-error is #58413e and its --color-error is also #58413e in light mode, a literal 1:1. Not a bug: stone tunes --color-on-error for --color-error-muted (the badge fill) while --color-error carries its icon/border tone. A pair the defaults treat as fill+label, stone treats as two separate roles. The token contract itself is not settled, so a check that calls stone broken is asserting a semantics nobody agreed to. Narrowed to the one-sided case, all seven go silent and the real bug still fires.

Worth a separate decision if you want it: whether --color-on-* is defined against --color-* or --color-*-muted. Settle that and the both-sides case becomes checkable.

It caught one of mine

The shipped theme.template.ts overrides --color-background-surface to #1F1F22 and inherited a --color-border-emphasized toned for the surface it replaced — 2.94:1, just under the floor, in the file that teaches people. Fixed by authoring the pair, with the reason in a comment, so the template now demonstrates the discipline twice (it already did for on-accent).

Visual evidence

Light mode is pixel-identical. In dark mode the boundary token moves from rgb(102, 103, 116) to rgb(111, 116, 124), raising contrast against the surface from 2.94:1 to 3.50:1. The enlarged swatch makes the color change judgeable; the real 2px control boundary is shown beneath it.

Before After
Light Light mode before Light mode after
Dark Dark mode before Dark mode after

Test plan

  • 17 unit tests on the helpers. The silence cases are pinned as hard as the catch — a check that fires on themes doing nothing wrong gets muted.
  • 7 API-contract tests through a real themeBuild(), including named colors and --check parity.
  • One bug only an end-to-end test could find: the check reads __inputTokens to know which side was hand-written, and the build has two loaders. jiti returns the DefinedTheme (has them); the legacy eval fallback, used whenever the theme file cannot be imported — the common case outside this repo, where @astryxdesign/core/theme does not resolve — returns the raw input, which does not. On that path the check was silently dead. build.mjs now re-mints from the input (whose tokens are exactly the hand-written set), and a test builds a plain-object theme to hold that path open.
  • Ran over all seven bundled themes: 0 warnings each, before and after.
  • Full serialized CLI suite: 2868/2868 pass. All four CLI TypeScript projects, check:repo, eslint, and formatting pass.

@vercel

vercel Bot commented Aug 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 26, 2026 10:18pm

Request Review

@github-actions

github-actions Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

github-actions Bot added a commit that referenced this pull request Aug 21, 2026
@cixzhang
cixzhang force-pushed the fix/theme-contrast-pair-warning branch from 8be0c03 to 897328a Compare August 25, 2026 17:12
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@cixzhang
cixzhang force-pushed the fix/theme-contrast-pair-warning branch from 897328a to 2689110 Compare August 26, 2026 17:06
@cixzhang
cixzhang force-pushed the fix/theme-contrast-pair-warning branch from 2689110 to bf1cef3 Compare August 26, 2026 21:29
github-actions Bot added a commit that referenced this pull request Aug 26, 2026
expandColorScale holds generated text at 4.5:1 and control boundaries at 3:1
for any accent — but only for what it generated. Replace --color-accent in
`tokens` and the --color-on-accent beside it still holds the value derived for
the colour that is gone, so a theme ships below-AA text without anyone writing
a bad colour, and nothing says so until someone measures pixels.

theme build now measures ten pairs in both modes and names the token you
forgot. Scoped to pairs with exactly one side hand-written: both sides set was
a choice, not a silent void, and neither side set belongs to the defaults
(#5019). Silent on all seven bundled themes; it caught a real 2.94:1 boundary
in the shipped theme.template.ts, fixed here.

Fixes #5014

@cixzhang cixzhang left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve (GitHub won't take a self-approval, so this is a comment).

The false-clean paths are fixed: raw thresholding, shared parsing, unmeasurable colours, and normal/check parity. The light/dark evidence is clean.

[Reviewed by Robohands]

github-actions Bot added a commit that referenced this pull request Aug 26, 2026
@cixzhang

Copy link
Copy Markdown
Contributor Author

Closing by product-direction decision. The checker is internally consistent and reviewed, but we do not want to add this new warning contract without a concrete gap asking for it.

@cixzhang cixzhang closed this Aug 27, 2026
@github-actions
github-actions Bot deleted the fix/theme-contrast-pair-warning branch August 27, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Overriding one side of a colour pair silently voids the contrast guarantee

1 participant