fix: restore contrast correction for themes and CLI output, add high-contrast mode#362
Conversation
…contrast mode Terminal text and app chrome have been hard to read since the theme system changed. Two independent causes: 1. xterm's minimumContrastRatio was 1 (correction fully disabled) in every dark theme, and only 4.5 in light. Removing the old `filter: invert(1) hue-rotate(180deg)` on `.light .xterm` took away the structural guarantee that Claude Code / Codex truecolor output stayed readable, and nothing replaced it for the 10 dark themes. 2. The muted text tokens fail WCAG AA against their own background in 7 of 12 themes (forge 2.90, night-owl 3.05, dusk 3.67, terracotta 3.95, ...). The theme-expansion PRs shipped without a contrast gate. Changes: - Raise --color-text-muted / -navigation-muted / -navigation-section / -interactive-muted to >=4.5:1 in dark, dusk, dusk-oled, forge, night-owl, night-owl-oled and terracotta. Hue preserved; lightness only. Applied at the semantic-token level, not the palette primitives, since --owl-400 and friends also feed --color-terminal-bright-black. - Add an opt-in "High contrast" toggle (Appearance settings) that pushes the same tokens to ~7:1 via a .high-contrast class. - Terminal minimumContrastRatio: 3 dark / 4.5 light by default, 9 when high contrast is on. 9 is deliberate — both xterm renderers halve the ratio for dim (SGR 2) cells, which is most of Claude Code's and Codex's secondary text, so 9 is what lands dim output at 4.5 AA. - Sync the pre-React bootstrap in index.html with THEME_CLASSES; it had drifted and was falling back to light-rounded for night-owl, night-owl-oled and terracotta, causing a light flash on every launch. - Give the remote PWA terminal a static minimumContrastRatio of 4.5. It has no AppConfig transport, so it is intentionally not wired to the setting. --color-text-disabled is deliberately left as-is; WCAG 1.4.3 exempts disabled controls and raising it destroys the disabled affordance.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f3a41f0ce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| scrollback: 10_000, | ||
| // Static AA floor: the remote PWA is browser-served with no AppConfig transport, | ||
| // so the desktop high-contrast setting cannot be plumbed here. | ||
| minimumContrastRatio: 4.5, |
There was a problem hiding this comment.
Raise the remote threshold for dim text
When Remote Pane renders SGR 2 (dim) output from Claude Code or Codex, xterm halves this configured contrast threshold—the same behavior documented in TerminalPanel.tsx when choosing a value of 9—so this value only enforces approximately 2.25:1 rather than the claimed AA floor. Since the remote terminal has no toggle and always uses this static value, set it to 9 as well if dim CLI output is meant to remain legible.
Useful? React with 👍 / 👎.
|
|
||
| /* Night Owl */ | ||
| :root.high-contrast.night-owl { | ||
| --color-text-muted: #b69496; /* 7.1:1 on --owl-900 */ |
There was a problem hiding this comment.
Calibrate high-contrast tokens against their actual surfaces
When Night Owl high-contrast mode is enabled, this color reaches 7:1 only against --owl-900, but --color-text-muted is also rendered on lighter surfaces—for example the command input placeholder in PanelTabBar.tsx uses it on --color-surface-secondary (#221618). Against that surface #b69496 is only about 6.4:1, so the newly advertised AAA mode does not provide AAA contrast in that user-visible context; the overrides need to account for the lightest backgrounds on which each semantic token is used.
Useful? React with 👍 / 👎.
Why
Text has been hard to read across Pane — most visibly in the Claude Code and Codex panels — since the theme system changed. Two independent root causes, both verified:
1. Terminal contrast correction was off.
0984a21replacedfilter: invert(1) hue-rotate(180deg)on.light .xtermwith a baked light palette plusminimumContrastRatio: 4.5. The old filter operated on rendered pixels, so it flipped everything the CLIs painted — including 24-bit truecolor, which bypasses the 16-slot ANSI palette entirely. The replacement only covers the 16 slots, and only in light mode: all 10 dark themes were left atminimumContrastRatio: 1, which disables xterm's correction completely.2. The muted text tokens never met AA. The theme-expansion PRs (
ed850b4dusk/forge →937209eember/aurora →0ff14c3night-owl/terracotta) each added backgrounds and palettes without a contrast gate.tests/axeTest.tscan't catch this — axe cannot see canvas-rendered terminal text, and these tokens were never audited.Measured before this PR,
--color-text-mutedfamily vs--color-bg-primary:What changed
Baseline (always on). The four muted tokens now clear 4.5:1 in all 12 themes — 4.70 to 5.57. Hue preserved, lightness only. Applied at the semantic-token level rather than the palette primitives, because
--owl-400/--forge-gray7/--dusk-500/--terra-400also feed--color-terminal-bright-black; changing them would have silently shifted ANSI output.High contrast toggle (Appearance settings, off by default). A
.high-contrastclass pushes the same tokens to ~7:1 (measured 7.01–7.08 across all 12 themes) and raises the terminal ratio.Terminal
minimumContrastRatio:9 is deliberate, not a typo. Both renderers halve the configured ratio for dim (SGR 2) cells —
TextureAtlas.ts:409andDomRendererRowFactory.ts:496. Claude Code and Codex use dim heavily for hints, spinners, and secondary text, which is the bulk of the unreadable output. 9 is what lands dim cells at 4.5 AA.The off-state floor is deliberately 3 and not 4.5: the muted grays in forge/ember/aurora/night-owl are doing design work, and forcing AA on every ANSI color flattens them. 3 only bites the pairs that were genuinely broken.
Also fixed:
frontend/index.html's pre-React bootstrap had drifted — it omittednight-owl,night-owl-oled, andterracotta, so those three fell back tolight-roundedand flashed light on every launch. Now synced withTHEME_CLASSES.Remote PWA terminal gets a static
minimumContrastRatio: 4.5. It's browser-served with noAppConfigtransport, so it's intentionally not wired to the setting.Deliberate non-goals
--color-text-disabledis untouched and still fails contrast in all 12 themes. WCAG 1.4.3 exempts disabled controls; raising it destroys the disabled affordance.--color-bg-primary. Muted text on elevated surfaces still lands below AA in places (dusk cards 3.90, forge tertiary 3.82, dark secondary 4.36). Real, but a separate scope call — noted below as follow-up.minimumContrastRationow covers the symptom; matching the themes properly is separate work.Review
Two independent review lanes (Claude + Codex), both of which re-derived all 24 contrast ratios from scratch rather than trusting the implementation. Both confirmed the math, the cascade specificity across all 12 themes including the OLED children and
light-rounded, and that no palette primitive was modified.Two Must-Fix findings, both resolved in this branch:
index.htmlbootstrap omitted three themes.getMinimumContrastRatio()readdocumentElement.classList, but that class is stamped byThemeProvider's effect — an ancestor. React flushes passive effects child-first, soTerminalPanelobserved the previous value and the terminal was one toggle behind. It now takeshighContrastas an argument.pnpm typecheckclean.pnpm lint0 errors (163 main / 165 frontend warnings, all pre-existing).Manual testing
Follow-up
ThemeContext), which is why it drifted — worth collapsing