fix(a11y): dark theme meets AA on brand-primary surfaces without changing the brand - #55
Merged
Merged
Conversation
…ging the brand The dark theme painted white text on the brand coral (#ffffff on #e54d5e = 3.78:1, under WCAG AA 4.5:1) — the last violation in the accessibility matrix (one node on /dashboard/providers, one on /dashboard/logs, one on the onboarding wizard, at every audited width). The brand colour is unchanged: `--color-primary` is still #e54d5e in the dark theme and #b83242 in the light one. What changes is the FOREGROUND on solid primary surfaces, via a new theme-aware `--color-on-primary` token (white in the light theme, black in the dark one — 5.6:1 on the coral fill and 4.7:1 on the bg-primary/90 hover fills). The dark `--color-primary-hover` now lightens from `--color-primary` with color-mix instead of hard-coding a darker coral, because black text on the old #c93d4e was 4.3:1. Call sites use `text-on-primary` instead of `text-white` (113 lines), so no hex and no literal foreground at call sites, and `text-primary-foreground` is now a real alias of the same token instead of silently inheriting the page text colour. themeStore derives the same token for a colour preset or custom colour written inline on <html>, so a user-chosen colour also lands on an AA foreground (custom #22c55e went from 2.27:1 to 9.2:1) and its hover shade moves away from the text. The axe e2e gate now audits every page in BOTH themes; until now a fresh Playwright context always measured the light theme, which is why this shipped. Nightly job budget 30 → 45 min for the doubled axe passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Testing a custom preset (#22c55e) surfaced a second, pre-existing gap with the same root cause: `--color-primary-on-tint` mixes the brand with 80% black (light) or white (dark), a ratio tuned for the coral brand. Another hue lands under AA on the very tints Phase 9 introduced the token for — axe measured 2.95:1 on the light sidebar nav item and 2.92:1 on the /dashboard/logs column pills. themeStore now derives the tint text for a preset colour per theme, shading it away from the tint until it clears 4.5:1 on every surface/alpha combination the app paints (sidebar, page, card, subtle × /10, /15, /22) and keeping as much of the hue as the contrast allows. globals.css reads those two overrides through a var() fallback, so the DEFAULT preset keeps the exact coral-tuned mixes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LMPrado-DZ23
pushed a commit
that referenced
this pull request
Sep 20, 2026
…ragments check:changelog-integrity caught the CHANGELOG-eat pattern on this branch: the merge of release/v3.8.55 recorded this branch's older CHANGELOG.md, dropping the New Features header and 8 bullets that landed in #48, #50, #51, #52, #55, #57, #58 and #59. No commit here ever edits that file, so the fix is to take the base version wholesale. Verified identical to origin/release/v3.8.55 afterwards, and the gate now reports "no base bullets lost". Adds this PR's own entry as fragments instead, which is the convention that exists precisely to stop this: one feature fragment for the Gemini CLI entry and the new placeholder, one fix fragment for the two corrected cards. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 20, 2026
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.
The residual
The dark theme painted white text on the brand coral —
#ffffffon#e54d5e= 3.78:1, under WCAG AA (4.5:1). It was the only violation left in the accessibility matrix, and the gate never saw it: the axe e2e suite only ever measured the light theme (a fresh Playwright context has no persisted theme preference).Approach — the brand colour is unchanged
--color-primaryis still#e54d5ein the dark theme and#b83242in the light one. Nothing repaints the brand.What changed is the foreground on brand surfaces, through theme-aware tokens (approach 1 of the brief):
--color-primary(brand)#b83242— unchanged#e54d5e— unchanged--color-on-primary(new)#ffffff(5.9:1)#000000(5.6:1 on the fill, 4.7:1 on thebg-primary/90hover fills)--color-primary-hover#9f2a38— unchangedcolor-mix(in srgb, var(--color-primary) 85%, #ffffff)— derived, lightens#c93d4eis only 4.3:1. It is now derived from--color-primarywithcolor-mix, so it follows the brand instead of drifting from it — the same pattern Phase 9 used for--color-primary-on-tint.text-whitetotext-on-primary.text-primary-foreground(used at a handful of call sites with no token behind it, so it silently inherited the page text colour) is now a real alias of the same token.<html>bythemeStoreand overrides both themes, so the store derives the matching--color-on-primaryfor that exact colour (white while it meets AA, otherwise black, which then always does) and shades hover away from that text colour.--color-primary-on-tintmixes the brand with 80% black/white, a ratio tuned for coral. On another hue it lands under AA on the tints it exists for (green measured 2.95:1 on the sidebar nav item, 2.92:1 on the/dashboard/logscolumn pills, in the LIGHT theme).themeStorenow derives that tint text per theme as well, shading away from the tint until it clears 4.5:1 on every surface × alpha the app paints (sidebar, page, card, subtle × /10, /15, /22) and keeping as much hue as contrast allows;globals.cssreads it through avar()fallback so the default preset keeps the exact coral-tuned mixes.Before / after — axe with the repo's own tag set (wcag2a, wcag2aa, wcag21a, wcag21aa)
Measured by running the product on an isolated dev server +
DATA_DIR(OMNIROUTE_BOOTSTRAPPEDpinned), theme pinned on both sides (persisted store +prefers-color-scheme,html.darkasserted per page), server warmed twice before each sweep. Totals are violations of any impact; critical/serious in brackets.Dark theme — BEFORE
Every one of them
color-contrast,fg=#ffffff bg=#e54d5e ratio=3.78— e.g.<button class="... bg-primary text-white border-primary">All</button>on/dashboard/logs, the "Get started" button on the wizard, and the count spans inside the active category pill on/dashboard/providers.Dark theme — AFTER
Light theme — BEFORE
Light theme — AFTER
Custom colour preset —
#22c55e, not the default, @1440¹ measured with the first commit applied, i.e. with
--color-primary-on-tintstill untouched — these are the pre-existing tint failures (fg=#1b9e4b, 2.92–2.95:1) that the second commit fixes, not a regression from this PR.Closing the gate that missed it
tests/e2e/a11y.spec.tsnow audits both themes for every page (THEMES = ["light", "dark"]): it pins the theme per document (persisted theme store +prefers-color-scheme) and waits for the store to applyhtml.darkbefore running axe. The frozen baselines are untouched and still 0 — they now hold for both themes. The nightly job budget goes 30 → 45 min for the doubled axe passes (the suite is nightly-only, so per-PR e2e shard timings are unchanged).tests/unit/ui/on-primary-contrast.test.ts(new) holds the line statically: the dark--color-primaryis still#e54d5e, on-primary meets AA at rest / on hover / overbg-primary/90on both dark surfaces, each theme's tint text falls back to its own preset override, no call site paintstext-whiteon a brand surface, and the e2e gate keeps auditing dark.tests/unit/ui/theme-store-default-primary.test.tsxcovers the preset derivation, including a sweep over the sRGB cube (every colour gets an AA foreground) and AA tint text for every built-in preset in both themes.Verification
tsc --noEmiton core / api / dashboard / noimplicit-core): clean.--max-warnings=0, suppressions file) on all changed files: clean. Prettier: clean.tests/unit/ui(node:test, 189 tests) and the theme-store/providers/webhooks vitest files: pass.check-file-size: only the pre-existingsrc/lib/db/core.ts(1770 > frozen 1745), untouched here. Complexity ratchets: 2747/3218 and 1235/1437, both under baseline.🤖 Generated with Claude Code