Skip to content

feat(ui): add colorblind theme - #1192

Merged
backnotprop merged 2 commits into
mainfrom
feat/colorblind-themes
Aug 4, 2026
Merged

feat(ui): add colorblind theme#1192
backnotprop merged 2 commits into
mainfrom
feat/colorblind-themes

Conversation

@backnotprop

@backnotprop backnotprop commented Aug 4, 2026

Copy link
Copy Markdown
Owner

A new built-in app theme designed for red-green color vision deficiency, plus the findings from an attempted tritanopia companion theme that was investigated, measured, and deliberately dropped (details below). The Colorblind theme is a complete bundle in the existing theme system: UI chrome tokens, diff line backgrounds, gutter indicators, review-editor syntax colors, and plan-editor code-block colors, with full dark and light support.

Note: this revision corrects the baseline numbers from the original PR description. The original baseline was computed on Pierre's stock CSS constants; the app actually ships the pierre-dark/pierre-light gitDecoration pair (add light-dark(#18a46c, #07c480), del light-dark(#d52c36, #ff2e3f)) via the attached syntax theme. All baseline numbers below are computed on MEASURED rendered backgrounds of the shipped default theme, and every simulation is reported under two independent models: Machado et al. (2009) severity-1.0 and Vienot 1999.

The theme

Colorblind (id colorblind) targets red-green CVD (protanopia and deuteranopia, the common ~95% of cases). Diff additions are blue and deletions are orange: the pair differs along the blue-yellow axis that survives red-green dichromacy. Base pair: add #4f83d1 / del #c96e12 in dark, add #216cab / del #a55c1e in light.

Why the shipped default needed this

Measured rendered line backgrounds of the shipped default (plannotator theme with the pierre pair), CVD-simulated, CIEDE2000 between the simulated add/del pair:

Mode Intensity Prot (Machado / Vienot) Deut (Machado / Vienot)
dark subtle 3.5 / 3.8 6.4 / 7.0
dark normal 12.2 / 13.3 11.1 / 11.6
dark strong 19.7 / 21.4 12.8 / 13.4
light subtle 1.7 / 1.8 3.5 / 3.8
light normal 7.3 / 7.8 8.9 / 9.5
light strong 11.8 / 12.7 10.9 / 11.3

At the default subtle intensity the real separation for a protanope in light mode is 1.7 (deut 3.5), which is effectively indistinguishable. The deuteranopia range across configs is 3.5 to 12.8 (Machado). This is bad enough to justify the theme but not the near-zero numbers originally claimed; the correction does not change the motivation, only its magnitude.

Colorblind theme numbers (measured rendered backgrounds)

Same methodology, matched pair for pair against the table above. The Colorblind theme beats the shipped default in all 6 mode/intensity configurations under both models for both protanopia and deuteranopia:

Mode Intensity Rendered add bg Rendered del bg Prot (M / V) Deut (M / V) fg/add fg/del
dark subtle #1f2b3c #39281f 16.9 / 18.8 19.4 / 22.3 12.13 11.97
dark normal #2b5079 #77451f 37.1 / 39.9 40.3 / 45.0 7.05 6.71
dark strong #2f679f #9d571c 45.2 / 48.5 48.5 / 54.5 5.02 4.70
light subtle #dfe7f1 #f2e5df 9.0 / 9.8 10.4 / 11.9 12.18 12.31
light normal #9bb7d7 #dcb094 27.8 / 30.6 31.3 / 35.3 7.35 7.71
light strong #6f9bc7 #cc9169 36.0 / 39.0 40.0 / 44.6 5.21 5.62

The worst config (light subtle, prot) goes from 1.7 to 9.0. Normal-vision foreground contrast stays at or above 4.70:1 over the strongest tint (the shipped default drops to 2.96:1 there). The base colors also paint the +/- gutter signs, indicator bars, and per-file counts, and keep 4.2:1 to 5.3:1 contrast on the page background; measured via CDP, classic +/- signs render exactly the base pair (rgb(79,131,209) / rgb(201,110,18) in dark) and the bars indicator renders the addition bar solid and the deletion bar as Pierre's dashed gradient from the same deletion base, so change direction is also encoded by shape.

Measurement methodology: headless Chrome driven over CDP against a live review server on a real git diff; computed background-color read from rest-state addition/deletion lines inside Pierre's shadow DOM at each of the three diffLineBgIntensity levels in both modes, for both the shipped default theme and this theme; CVD simulation applied to the measured values.

The tritanopia theme was investigated and dropped

The original revision of this branch included a Colorblind Tritanopia theme with a teal/vermilion diff pair. Adversarial review found it backwards-effective, and re-measurement confirms: tritanopia destroys the blue-yellow axis and PRESERVES red-green, so the shipped default's green/red pair is already close to optimal for tritanopes. On measured rendered backgrounds the default scores tritan dE00 35.9 to 62.8 (Machado) and 40.1 to 70.0 (Vienot) across the 6 configs; the teal/vermilion pair loses to that in most configs because teal carries too much blue.

A matched-pair grid search (1,890 dark and 1,024 light candidates over green/yellow-green additions vs red/vermilion deletions, in oklch space) was run to find a replacement pair that beats the default in all 6 configs under both models while keeping 4.5:1 text contrast over the strongest tint. Result: zero feasible pairs. The best candidates converge on the default's own vivid green/red and still fall 2.8 to 6.1 dE00 short in the worst configuration, because beating the default requires brighter, more saturated colors than the text-contrast constraint allows (the default itself only reaches its numbers by dropping strong-intensity text contrast to 2.96:1).

Given that a theme named for tritanopia could at best tie the stock palette while implying it does better, it was removed rather than renamed: a renamed generic palette would add nothing over the Colorblind theme. Two useful findings stand: tritanopes are already well served by the default diff colors, and the Colorblind theme itself also holds up under tritanopia simulation (Vienot dE00 33.9 to 69.6 across all configs), so recommending it as the single CVD theme does not harm tritanope users.

How the diff colors flow

No app code was needed for the line backgrounds. @pierre/diffs derives everything (line bg mixes, emphasis chips, signs, bars, counts) from --diffs-addition-base / --diffs-deletion-base, whose fallback chains start at --diffs-addition-color-override / --diffs-deletion-color-override. Custom properties inherit into the diff shadow DOM from the document root, so the theme CSS file simply sets those override tokens per mode. Existing themes define neither token, so nothing changes for them, and buildLineBgOverrides's intensity machinery composes on top unchanged. Plan-diff views and the file tree's +N/-N follow --success / --destructive, which map to the same pair.

How syntax mapping works

  • Review editor (shiki): SHIKI_THEME_MAP in packages/review-editor/hooks/usePierreTheme.ts maps colorblind to the pierre-dark-protanopia-deuteranopia / pierre-light-protanopia-deuteranopia pair. These themes ship in @pierre/theme 2.0.0.
  • Plan editor (highlight.js): plan and annotate code blocks use the bundled github-dark stylesheet plus app-level light overrides. The theme file adds scoped .theme-colorblind .hljs-* token rules (dark and light) mirroring the corresponding Pierre shiki palettes, so plan code blocks avoid red-vs-green token pairs. Verified live: computed token colors in the annotate editor match the palette in both modes.

Bundle impact

The Pierre colorblind shiki themes were already in the bundle: @pierre/theming's collection statically registers dynamic imports for all ten pierre themes, and the single-file builds inline them (the baseline bundle already contains pierre-dark-soft and friends). Measured delta, built at origin/main vs this branch head:

  • apps/review dist/index.html: 18,132.08 kB to 18,136.79 kB (+4.71 kB raw, +0.94 kB gzip)
  • apps/hook dist/index.html: 23,026.88 kB to 23,031.48 kB (+4.60 kB raw, +0.85 kB gzip)

That delta is the theme CSS file and registry entry.

Verification

Check Result
bun run typecheck pass
bun test (full suite) 2794 pass, 0 fail, 214 skip
DOM_TESTS=1 file-browser isolated test (per test.yml) 6 pass
DOM_TESTS=1 UI seam-contract + DOM tests (per test.yml) 37 pass
ThemeProvider.test.tsx registry coverage for the new theme pass
Builds in order: apps/review, build:hook, build:opencode pass, no dist files tracked (gitignored)
CDP computed-color measurements, both themes (default baseline + colorblind), 3 intensities, both modes tables above
Tailwind @source no new tsx directories added, plain CSS theme file needs no entry
Marketing site docs mention only Dark/Light/System modes and never enumerate color themes, so no update needed

Remaining human QA

  • The honest gap: this palette is validated by simulation, not by eyes. A real-eyes check by users with protanopia and deuteranopia is the meaningful acceptance test, especially for the subtle intensity in light mode (the tightest margin, simulated dE00 9.0).
  • Aesthetic judgment on the UI chrome (primary/accent choices) in both modes.

Two new built-in themes tuned for color vision deficiency:

- Colorblind: for red-green CVD (protanopia and deuteranopia). Diff
  additions are blue (#4f83d1 dark / #216cab light) and deletions are
  orange (#c96e12 dark / #a55c1e light), a pair on the blue-yellow axis
  that survives red-green dichromacy.
- Colorblind Tritanopia: for blue-yellow CVD. Additions are teal
  (#2b939e dark / #1b7981 light) and deletions are vermilion (#db5f38
  dark / #c14f2b light), a pair on the surviving red-green axis.

Both pairs were verified computationally with Machado et al. (2009)
severity-1.0 CVD simulation matrices: the simulated add/del line
backgrounds keep CIEDE2000 >= 8.9 (red-green theme) and >= 18.6
(tritanopia theme) at every diffLineBgIntensity in both dark and light
modes, and normal-vision foreground contrast stays >= 4.7:1 over the
strongest tint. The +/- gutter signs and indicator bars reuse the same
base colors and keep >= 4.2:1 contrast on the page background.

The review editor's Pierre diff pipeline picks up the pair via the
library's own override seam: the theme files set
--diffs-addition-color-override / --diffs-deletion-color-override,
which inherit into the diff shadow DOM, so no app code changes were
needed for the line backgrounds. Syntax colors in diffs map to the
@pierre/theme colorblind shiki themes (already in the dependency tree),
and plan-editor highlight.js code blocks get theme-scoped token
overrides mirroring the same palettes.
@backnotprop

Copy link
Copy Markdown
Owner Author

Adversarial review (at 7d3d5615, measured on the tree merged with current main)

Verdict: needs changes. The engineering is clean and the red-green Colorblind theme convincingly delivers its claim. The Tritanopia theme does not, and the PR's motivating baseline number is computed on colors the app does not render. Both findings are in the accessibility claims, which are the point of the PR.

Verified clean first: the token-chain mechanism reads exactly as described against 1.3.2's stylesheet, with shadow-boundary inheritance empirically confirmed (declared overrides resolve byte-exact inside the shadow root in all four theme/mode combinations) and buildLineBgOverrides composing orthogonally, matching the base-color-independence proven in the stage-2 review. Default-theme regression check: 66 of 66 measured fields byte-identical before and after the branch. Token surface diffed token-for-token against plannotator.css: zero missing in both files. The hljs !important escalation is necessary (the app's own light-mode rules use !important, so the theme rules lose without theirs) and containment is airtight, all scoped under the theme classes. Contrast claims reproduce on measured colors (worst text-over-tint exactly 4.70:1, gutter signs 4.65 to 5.25). Direction is never color-dependent: the classic +/- glyphs and the solid-vs-striped bars are pre-existing library CSS. All twelve rendered hex values match the PR's measurement table exactly. Full suite 2794 pass / 0 fail, DOM sets green, builds green, bundle delta exact to the byte, merges cleanly onto current main, and the "four Pierre shiki themes were already inlined" claim is confirmed, which closes the stage-1 finding about bundled-but-unreachable themes.

Finding 1 (medium, blocks the tritanopia half only): the Tritanopia theme reduces tritan separation instead of improving it. Matched-pair comparison on measured rendered backgrounds, same mode and intensity: the default theme beats the tritanopia theme in 6 of 6 configurations under Machado (the PR's own model) and 6 of 6 under Vienot 1999 (substituted after the reviewer's Brettel implementation failed physics validation and was discarded; the two valid models agree). Physically explicable: tritanopia destroys the blue-yellow axis and preserves red-green, so the default red/green pair already sits on the surviving axis, while the theme's teal carries substantial blue content and gives separation away. The theme is still absolutely safe (worst case 18.5 dE00, far above the noticeable threshold) and shape encoding always recovers direction, but a theme named for tritanopia that measurably lowers tritan separation will mislead exactly the users who select it. Fix options: re-tune the addition color off the blue axis (a yellow-green against the vermilion would sit on the surviving axis and should win), or rename/reframe the theme as stylistic. The red-green Colorblind theme is independent and ships as-is.

Finding 2 (medium): the motivating baseline is computed on the wrong colors. The PR simulates Pierre's stock pair, but the app overrides it in the plannotator theme; on the colors actually shipped the default's deut separation is 3.5 to 12.8 across intensities, not 1.4 to 4.5, and "effectively indistinguishable across intensities" is wrong at strong intensity. The motivation survives where it matters: at the shipped subtle default the real rendered separation is 1.66, genuinely indistinguishable, lifted to 8.99 by the new theme. The numbers should be corrected to the shipped colors.

Low: the tritanopia ceiling differs slightly between implementations (53.1 claimed vs 56.6 measured from identical hex; a dE00 implementation nuance, immaterial). The screenshot artifacts referenced in the work summary are not present in the repo or PR; the CDP measurement tables, which were reproduced independently, carry the evidence instead.

Info for future theme work: these are the first themes in the repo carrying their own hljs token blocks; worth documenting as a pattern if more themes adopt it.

Independent re-simulation showed the shipped default diff pair (the
pierre-dark/pierre-light gitDecoration colors, green #07c480 / red
#ff2e3f in dark and #18a46c / #d52c36 in light) beats the teal/vermilion
pair under tritanopia simulation, and a matched-pair grid search over
green/yellow-green vs red/vermilion candidates confirms why: tritanopia
preserves the red-green axis, so the stock green/red palette is already
near-optimal for tritanopes. No candidate pair that also keeps 4.5:1
normal-vision text contrast over the strongest line tint can beat the
default in all six mode/intensity configurations under both Machado
severity-1.0 and Vienot 1999 (the best candidates converge on the
default's own vivid green/red and still fall 3 to 6 dE00 short in the
worst configuration).

Shipping a theme named for tritanopia that at best ties the default
would invite false confidence, so it is removed rather than renamed.
The Colorblind theme (red-green CVD, the common ~95% of cases) is
unaffected and also holds up under tritanopia simulation (Vienot
dE00 >= 32.8 in every configuration).
@backnotprop backnotprop changed the title feat(ui): add colorblind and tritanopia themes feat(ui): add colorblind theme Aug 4, 2026
@backnotprop

Copy link
Copy Markdown
Owner Author

Delta re-review (at 4fba8601)

Verdict: merge as-is. The tritanopia removal is surgically clean: the delta is exactly the theme file plus its three registration points and the test, git grep -in tritanopia returns zero hits repo-wide, and colorblind.css is byte-identical to the previously validated head, so the 6/6 red-green win and the 4.70:1 contrast floor carry over without re-measurement. Merges cleanly onto current main. Full suite 2794 pass / 0 fail, builds green, no em dashes, screenshot references removed as asked.

The grid-search infeasibility conclusion was verified independently rather than re-run: a separate search (65k dark and 434k light contrast-feasible candidates, after catching and fixing an out-of-gamut scoring bug in the first attempt) confirms zero dark-mode pairs beat the shipped default at every intensity under both models, and the binding constraint is exactly as claimed: the default's own dark/strong text contrast measures 2.96:1, reproducing the builder's figure to two decimals, so any candidate held to the 4.5:1 floor cannot match the default's vividness-bought separation. Light mode has a thin 0.3% sliver of technical winners topping out at +2.72 dE00 worst-case, essentially at the perception threshold; since a theme ships both modes and dark is genuinely infeasible, the drop is correct and robustly supported. The corrected baselines match the prior review's measurements exactly, and the root cause was verified directly in @pierre/theme@2.0.0's JSON (the gitDecoration colors are precisely the rendered base values).

Two low findings, neither blocking, both in supporting prose:

  1. The "Colorblind holds up under tritanopia (Vienot 33.9 to 69.6)" sentence does not reproduce: measured from rendered backgrounds the floor is 5.9 at light/subtle, mildly below the default's 8.8 at the same config. Practical impact is small (at subtle every pair is weak, and the +/- glyphs and solid/striped bars encode direction without color), but the sentence should be softened from a safety claim to a qualified one before it gets cited.
  2. The default's tritanopia floor is similarly over-reported (their 35.9 floor vs 22.1/8.8 measured), conservative in the safe direction here. Both discrepancies concentrate at subtle intensity, which is the same measurement trap flagged in the original review: the intensity cookie is silently overridden by server config, so a harness can believe it swept three intensities while measuring one. Worth a harness sanity check before these tables are cited again.

With those two sentences softened (or simply this comment standing as the correction of record), this is ready to merge.

@backnotprop
backnotprop merged commit c4acec9 into main Aug 4, 2026
14 checks passed
@backnotprop
backnotprop deleted the feat/colorblind-themes branch August 4, 2026 18:03
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.

1 participant