Skip to content

Phase 0: contrast helper + report-only theme baseline (no visual change) - #2464

Open
mpaiva wants to merge 3 commits into
bastani-inc:mainfrom
mpaiva:feat/phase0-contrast-baseline
Open

Phase 0: contrast helper + report-only theme baseline (no visual change)#2464
mpaiva wants to merge 3 commits into
bastani-inc:mainfrom
mpaiva:feat/phase0-contrast-baseline

Conversation

@mpaiva

@mpaiva mpaiva commented Aug 17, 2026

Copy link
Copy Markdown

Phase 0 of #2314, scoped per @flora131's request in #2463: establish a contrast baseline and tooling with zero change to Atomic's appearance or UX.

What this adds

  • Contrast helper in theme/color-utils.tsrelativeLuminance, contrastRatio, rateContrast, and colorValueToHex (reusing the WCAG math already in impeccable's screenshot-contrast.mjs). Pure functions; no rendering path touched.
  • Unit tests for the helper (black/white extremes, order-independence, a known reference pair, AA thresholds, ColorValue resolution).
  • Report-only fixture over the six built-in themes that computes contrast and prints a table. It asserts the math is well-formed, not that themes pass — no build gate.

What this deliberately does NOT do

Baseline (assumed backgrounds)

Foregrounds measured against an assumed canvas (#1e1e1e dark / #ffffff light); *Bg tokens against assumed text. "" (terminal default) tokens are omitted as unknowable — making that gap visible is part of the point. Full table posted in #2463.

Verification

  • npm run typecheck — clean
  • npm run test --workspace=@bastani/atomic -- theme-contrast-helpers theme-contrast-baseline theme-builtins — 13 passing
  • biome check — clean

Refs #2314

Figma reference

The Phase 0 documentation page (Atomic - Design System → Phase 0) — what shipped, the before/after (capability, not pixels), the baseline, and a clearly-labeled NOT shipped Phase 1 preview. Link is a Figma export (valid ~30 days):

Phase 0 documentation page

Closes #2463

Follow-up finding: 256-color background collapse (tracked in #2550)

The 256-color measurement surfaces a genuine rendering defect (not a measurement error), which is exactly the point of reporting both modes. In 256-color terminals every near-neutral dark background collapses to #5f5f5f, and the tool backgrounds snap to saturated cube corners (toolPendingBg → #00005f, toolSuccessBg → #005f00, toolErrorBg → #5f0000). That is why most 256-color rows read FAIL — e.g. mdCode on userMessageBg 5.85 (AA) → 2.67 (FAIL).

Root cause is the spread < 10 grayscale gate in rgbTo256() (color-utils.ts): slightly-tinted dark colors (spread 13–25) are forced onto the 6×6×6 cube even though the grayscale ramp is 30–60× closer (#343541 → #3a3a3a recovers that pair to 4.76).

Out of scope for this measurement-only PR (no rendering change here). Tracked for a follow-up fix + baseline regeneration in #2550.

Contrast baseline mockup — comfortably-passing pairs that FAIL after 256-color quantization

Mockup (Figma frame) — each row is the same rendered foreground/background pair in truecolor vs 256-color, showing where the WCAG rating degrades. Measurement only; the rgbTo256() fix is intentionally not applied here and is tracked in #2550.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Greptile Summary

This change adds WCAG contrast utilities and a checked-in baseline for bundled terminal themes. The baseline currently reports ordinary text with contrast below the 4.5:1 requirement as AA-large, which can make failing normal-text combinations appear acceptable.

Confidence Score: 4/5

The contrast baseline can misstate accessibility compliance for normal rendered text, so it should be corrected before relying on the report for theme decisions.

One reproduced non-security defect remains in the report’s rating logic: normal text is evaluated using the large-text threshold.

Files Needing Attention: packages/coding-agent/test/theme-contrast-baseline.helper.ts; packages/coding-agent/src/modes/interactive/theme/color-utils.ts

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced proof for a posted P1 finding and included the narrow script that invokes the contrast baseline generator.
  • The observed generator output showed ordinary text rated AA-large.
  • The focused contrast test run passed.
  • T-Rex produced proof for another posted P1 finding.
  • The general-contract-validation-proof confirmed that rateContrast(3.00) and rateContrast(4.49) both return AA-large, with concrete color examples for dark truecolor toolOutput on toolSuccessBg at 3.37 and light truecolor mdLink on userMessageBg at 3.52, and it noted that no repository source files were changed and that only the requested proof artifacts were added.

View all artifacts

T-Rex Ran code and verified through T-Rex

Comments Outside Diff (1)

  1. General comment

    P1 Baseline report marks failing ordinary text as AA-large

    • Bug
      • The report contains rows explicitly classified as text whose ratios are below the 4.5 normal-text threshold but whose visible rating is AA-large. The executed generator found 300 such rows across its six themes and two color modes, including toolOutput on toolSuccessBg at 3.37 and mdLink on userMessageBg at 3.52.
    • Cause
      • rateContrast() has only a ratio argument and unconditionally maps every ratio >=3.0 and <4.5 to AA-large (packages/coding-agent/src/modes/interactive/theme/color-utils.ts:288-291). measureTheme() calculates kind independently but does not use it to determine the rating (packages/coding-agent/test/theme-contrast-baseline.helper.ts:211-218).
    • Fix
      • Make the report verdict kind-aware: rate kind: "text" rows below 4.5 as FAIL; reserve the 3.0 threshold/rating for rows genuinely classified as non-text (or clearly report the 3.0 result as a conditional large-text result rather than a pass). Add focused assertions that normal-text mdLink and toolOutput rows in [3.0, 4.5) are not reported as passing AA.

    T-Rex Ran code and verified through T-Rex

Prompt To Fix All With AI
### Issue 1
packages/coding-agent/test/theme-contrast-baseline.helper.ts:218
**Normal text is reported as AA-large**

`measureTheme()` records whether a row is ordinary text, but rates every 3.0–4.49 ratio as `AA-large` without considering that kind. The generated report therefore presents normal-size rendered text—including `toolOutput on toolSuccessBg` (3.37) and `mdLink on userMessageBg` (3.52)—as an AA-large result even though those text surfaces require 4.5:1. Make the verdict kind-aware: ordinary text below 4.5 should be `FAIL`, while the 3.0 threshold remains applicable to non-text rows, or clearly label it as conditional on large text.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (3): Last reviewed commit: "test(theme): render-accurate contrast ba..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Adds relativeLuminance/contrastRatio/rateContrast and colorValueToHex to
theme/color-utils.ts (measurement only, no rendering change), plus unit
tests and a report-only fixture that publishes a contrast baseline across
the six built-in themes. No theme values change and no build gate is added.

Refs bastani-inc#2314, bastani-inc#2463
Comment thread packages/coding-agent/test/theme-contrast-baseline.test.ts Outdated
Comment thread packages/coding-agent/test/theme-contrast-baseline.test.ts Outdated
Review fix (PR bastani-inc#2464): the Bg-suffix heuristic scored context-specific text
tokens against the assumed canvas, reporting ratios users never see (e.g.
Mocha userMessageText 11.53 on canvas vs 8.69 on userMessageBg) and omitting
fallback-backed pairs like searchMatchText/searchMatchBg. The fixture now
measures explicit rendered pairs (user/custom messages, tool boxes, search
matches, selection) using the same optional-token fallbacks as theme-class.ts,
and only sweeps true canvas foregrounds against the assumed background.
greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 17, 2026
@mpaiva

mpaiva commented Aug 17, 2026

Copy link
Copy Markdown
Author

Scope note / next steps. This PR is deliberately Phase 0 only — the contrast helper, a report-only fixture, and published baselines. No theme values, components, or layouts change, and there's no build gate.

Once this lands, Phase 1 follows separately, per the plan in #2314: before/after mockups against the new fullscreen main (dark + light) with exact current→proposed token values, for review before any implementation ticket. Reduced motion, shared token architecture, density, and border styles remain deferred to later phases.

@flora131

Copy link
Copy Markdown
Collaborator

Thanks, Marcelo. We’re starting a formal merge-readiness code review now, including current-main compatibility and the relevant checks. We’ll follow up here shortly with the review result and any required changes.

@flora131 flora131 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, Marcelo — I ran a formal review of the current head against its base and current main. The WCAG helper math checks out, all 14 focused tests pass, the output is deterministic, and this PR does not change Atomic’s appearance or runtime rendering.

I’m requesting four changes before Phase 0 lands:

  1. Test each color against the background where users actually see it. The current report treats several tokens as canvas text, but Markdown and syntax colors render inside userMessageBg and customMessageBg; tool renderers place more colors inside pending, success, and error backgrounds; selected rows use additional foregrounds; and working-indicator colors are omitted. This matters in practice: some Catppuccin Latte combinations reported as passing against the canvas fail against their rendered message background. Please derive and assert the complete rendered surface-to-foreground mapping rather than falling back to the canvas for these tokens.

  2. Account for terminals that reduce colors to a 256-color palette. The report measures source hex values, while Atomic also supports production rendering through fgAnsi() and bgAnsi() in 256-color terminals. That quantization changes some ratings from passing to failing. Please emit separately labelled truecolor and 256-color results, or clearly narrow the report’s scope. Any terminal foreground/background assumptions should be labelled with the exact assumed values.

  3. Remove UI pairs that no longer render on current main. Transcript search was removed, so searchMatchText on searchMatchBg is now an unused compatibility pair rather than a visible Atomic surface. Please refresh the rendered-pair list against current main; the tokens can remain loadable for compatibility without being counted in the visible baseline.

  4. Commit the baseline report as a Markdown artifact. The issue scope calls for the baseline numbers to be committed and posted. The PR currently prints the table only when the test runs. Please commit the generated table with its assumptions, color mode, generation command, and source revision, and add a synchronization assertion so the checked-in report cannot drift from the generator.

The package-test location and direct node:fs use are correct. Once these baseline issues are addressed, we can rerun the package checks and required CI for final approval. Thanks again for taking on this foundation work and for correcting the original foreground/background pairing.

Address the Phase 0 review (PR bastani-inc#2464): measure each color on the surface
where it actually renders, cover 256-color quantization, drop the removed
search pair, and commit the baseline so it cannot drift.

- Derive the full rendered surface -> foreground mapping: assistant prose and
  chrome on the terminal canvas; Markdown/syntax inside userMessageBg and
  customMessageBg; toolTitle/toolOutput/diffs/muted inside tool
  pending/success/error backgrounds; text/muted/accent on selectedBg; and the
  working-indicator tones on the canvas. Context-specific tokens are never
  scored against the canvas.
- Add color-utils.quantizeColorValueToHex(value, mode) and emit separately
  labelled truecolor and 256-color results, so fgAnsi()/bgAnsi() quantization
  that flips ratings is visible. Assumed terminal fg/bg are labelled with exact
  values and left unquantized (the terminal paints its own defaults).
- Remove searchMatchText/searchMatchBg from the visible baseline; transcript
  search was removed from main and the tokens remain loadable only as a
  compatibility fallback.
- Commit test/theme-contrast-baseline.md (assumptions, color mode, generation
  command, stable theme-source fingerprint) and assert it stays in sync with
  the generator (regenerate via UPDATE_CONTRAST_BASELINE=1).

Refs bastani-inc#2314, bastani-inc#2463
@greptile-apps
greptile-apps Bot dismissed their stale review August 20, 2026 15:17

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

fgHex,
bgHex,
ratio,
rating: rateContrast(ratio),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Normal text is reported as AA-large

measureTheme() records whether a row is ordinary text, but rates every 3.0–4.49 ratio as AA-large without considering that kind. The generated report therefore presents normal-size rendered text—including toolOutput on toolSuccessBg (3.37) and mdLink on userMessageBg (3.52)—as an AA-large result even though those text surfaces require 4.5:1. Make the verdict kind-aware: ordinary text below 4.5 should be FAIL, while the 3.0 threshold remains applicable to non-text rows, or clearly label it as conditional on large text.

Artifacts

Narrow script that invokes the actual contrast baseline generator

  • Authored TypeScript script imports the PR's `measureTheme()` and `rateContrast()` implementations, enumerates built-in themes and modes, and asserts/report prints ordinary-text AA-large rows. Takeaway: the proof exercises the exact report generator rather than a reimplementation.

Observed generator output showing ordinary text rated AA-large

  • Captured successful command output for the narrow generator script, including all-row count and mdLink/toolOutput examples with expected normal-text failures. Takeaway: 300 ordinary-text rows below 4.5 are labeled AA-large by the running implementation.

Focused contrast test run passing

  • Captured successful focused Vitest run for the contrast helpers and baseline generator tests: 2 files and 11 tests passed. Takeaway: the misleading report classification is accepted by the current focused test suite.

View artifacts

T-Rex Ran code and verified through T-Rex

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/coding-agent/test/theme-contrast-baseline.helper.ts
Line: 218

Comment:
**Normal text is reported as AA-large**

`measureTheme()` records whether a row is ordinary text, but rates every 3.0–4.49 ratio as `AA-large` without considering that kind. The generated report therefore presents normal-size rendered text—including `toolOutput on toolSuccessBg` (3.37) and `mdLink on userMessageBg` (3.52)—as an AA-large result even though those text surfaces require 4.5:1. Make the verdict kind-aware: ordinary text below 4.5 should be `FAIL`, while the 3.0 threshold remains applicable to non-text rows, or clearly label it as conditional on large text.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@mpaiva

mpaiva commented Aug 20, 2026

Copy link
Copy Markdown
Author

@flora131 Re: change #2 (256-color quantization). The baseline now emits separately-labelled truecolor and 256-color results, with the assumed terminal fg/bg stated exactly. Visual summary of the impact — the same rendered pair in both modes, showing where ratings degrade in a 256-color terminal:

Contrast baseline mockup — comfortably-passing pairs that FAIL after 256-color quantization

606 rendered pairs/mode, 189 WCAG rating flips (172 degrading). The 256-color failures trace to a real rgbTo256() defect — near-neutral dark backgrounds collapse to #5f5f5f, and tool backgrounds to saturated cube corners — which is out of scope for this measurement-only PR and tracked separately in #2550. Live Figma frame: node 78:2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 0: contrast baseline + tooling (no visual change)

3 participants