fix(gui): typography contrast — WCAG AA pass for text-dim consumers + mention chips - #235
Conversation
… mention chips Closes jcast90#159. Follow-up to PR jcast90#158, which fixed hover-state contrast (1.4.11). This pass walks every --color-text-dim text consumer and the mention-chip palette per the issue's acceptance criteria. What changed - Per-call-site audit: 30 CSS selectors + 12 inline style={} blocks using --color-text-dim for *text* migrated to --color-text-muted (≥6:1 on paper-base). 4 non-text uses retained (status dots ×3, hover border). - Mention-chip fg tokens darkened so chip text clears AA (≥4.5:1) on its paired soft-pastel bg: - --color-mention-primary-fg: #e65a4f → #a43c32 (2.83:1 → 5.17:1) - --color-mention-human-fg: #2a7a5a → #1f6e54 (4.39:1 → 5.22:1) - --color-mention-attached-fg: kept (already 4.94:1) The accent tokens (--color-accent-coral / -mint) stay at their resting hue for buttons, focus rings, and links where contrast was already fine. - New @media (prefers-contrast: more) block pushes the muted-text family toward primary for users opting into higher OS contrast. - agent_docs/tidewater_handoff/tokens.json synced with the override values and a _meta.overrides note documenting that CSS supersedes JSON for any WCAG-darkened token, so re-derivation doesn't silently revert the a11y fix. Found during audit, not in this PR - .channel-header-tier raw-hex pills (tier-bugfix 3.54:1, tier-question 3.21:1, tier-feature 4.39:1) also fail AA. Out of scope here because they're tier-specific palette, not text-dim drift — opening a follow-up. - --color-text-dim now only applies to non-text decorative uses. Renaming to --color-decorative-dim would clarify intent; deferred to a follow-up to keep this PR focused on contrast. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
jcast90
left a comment
There was a problem hiding this comment.
Solid audit — the by-call-site walk + retained decorative uses + chip-fg darkening hit all three classes of contrast failure cleanly, and the WCAG ratios in the inline comments make it auditable later. The _meta.overrides note in tokens.json is the right shape for the CSS-vs-handoff drift problem; just a couple of inline notes on how to make that contract stick. The prefers-contrast: more block is a nice opt-in.
The deferred follow-ups in the PR description (channel-header tier pills, --color-text-dim rename to reflect non-text-only semantics) are both worth their own issues — happy to triage those once #159 closes.
| "source": "design/direction-a-base.jsx — window.RELAY_A.A", | ||
| "notes": "Two-surface system: deep 'ink' rail (sidebar/workspace rail) + warm 'paper' content. Coral is the only strong accent; reserve it for primary CTAs, active tabs, the primary repo, and selection." | ||
| "notes": "Two-surface system: deep 'ink' rail (sidebar/workspace rail) + warm 'paper' content. Coral is the only strong accent; reserve it for primary CTAs, active tabs, the primary repo, and selection.", | ||
| "overrides": "Some values below are accessibility-darkened from the original Tidewater palette to clear WCAG 2.1 AA (≥4.5:1 for normal text). When the live CSS in gui/src/styles/tokens.css differs from this file, the CSS value supersedes — re-deriving from this file without re-checking ratios will revert the a11y fix. Current overrides: color.mention.repoPrimaryFg #a43c32 (was #e65a4f, 5.17:1 on -primaryBg); color.mention.humanFg #1f6e54 (was #2a7a5a, 5.22:1 on -humanBg)." |
There was a problem hiding this comment.
The CSS-supersedes-JSON contract is the right call given the handoff doc's purpose, but it's fragile — encoded entirely in prose, no enforcement. Two ways to harden it without taking on this PR's scope:
- Quick win: a tiny script (
scripts/check-token-overrides.mjs) that diff-checks the override-listed tokens betweentokens.jsonandgui/src/styles/tokens.cssand fails CI if they re-converge. Wire it into theformat-checkjob. Catches the "someone re-derived from JSON" regression deterministically. - Heavier: generate
tokens.cssfromtokens.json+ an explicittokens.overrides.json, removing the divergence by construction.
Not blocking for this PR — the prose is fine for now — but worth a follow-up issue, since the only thing standing between the next refactor and a silent a11y regression is that someone reads this overrides field before editing.
| --color-mention-primary-bg: #fbe1dd; | ||
| --color-mention-primary-fg: #e65a4f; | ||
| --color-mention-primary-fg: #a43c32; /* 5.17:1 on -primary-bg */ | ||
| --color-mention-primary-line: rgba(230, 90, 79, 0.33); |
There was a problem hiding this comment.
You called this out in the PR description's "Notes for review" — flagging here so the line itself carries the marker. With --color-mention-primary-fg darkened to #a43c32, the border rgba(230, 90, 79, 0.33) is still keyed to the old coral fg. On the soft-pink bg the resting coral-tinted border now sits next to a much darker brick-red fg, which can read as a two-tone outline (the rgba and the text don't share a hue family any more).
Speculation, no design eyes from me — but two reasonable directions:
- Re-tint the line to
rgba(164, 60, 50, 0.33)(the darkened fg at the same alpha) so the chip reads as one hue family again. - Or drop the alpha further (
0.18-0.22) so the line becomes barely-there structure rather than an accent in its own right.
Whichever — would be good to do it in this PR rather than as a follow-up, because the visual is currently worse-looking (even if more accessible) than before the patch.
| opt in. */ | ||
| @media (prefers-contrast: more) { | ||
| :root { | ||
| --color-text-muted: #3a4254; /* ~9.4:1 on paper-base */ |
There was a problem hiding this comment.
Good addition. Two notes for the future:
- Dark-mode interaction with feat(gui): dark/light mode toggle #162. If the dark-mode toggle PR lands, the
[data-theme="dark"]block will need its own parallel@media (prefers-contrast: more)overrides — otherwise dark-mode + high-contrast users get the light-mode-tuned values applied to a dark surface, inverting the intent. Worth dropping a comment here noting that and/or pinging feat(gui): dark/light mode toggle #162 to track. Not blocking — feat(gui): dark/light mode toggle #162 isn't merged yet, just don't want this falling through the cracks if dark lands first. text-on-dark-muted: #c5cbd9(~10:1). That'stext-primary-bright on dark; arguably too bright if it's used in a glance-only context (timestamps, line metadata) — the user opted into contrast, not strain. If you have a sample dark-rail surface lying around, eyeballing this inprefers-contrast: morebefore merging would catch the case where 10:1 starts to read as a second primary text tier.
…) (#244) #235 fixed contrast on resting surfaces and is a real improvement, but its audit only walked resting backgrounds. `text-muted` (#5b6579) clears AA on paper-base at 5.57:1 — and then `.popover-item:hover` swaps the ground to paper-hover (#d8cfb0), where it drops to 3.76:1. paper-pressed (#bdb088) takes it to 2.72:1. So `.item-sub` fails AA precisely while the user is hovering to read it, which is the one moment it has to be legible. (#235 still improved that spot a lot — it was 1.98:1 before.) Adds --color-text-muted-strong (#3a4254) and applies it to `.item-sub` on hover and active: 6.45:1 and 4.66:1, both clear AA. It's the same hex the prefers-contrast tier already uses for muted, so no new color enters the palette. Also corrects three annotations so the next audit can trust them: - tokens.css: attached-fg is 5.02:1, not 4.94:1; human-fg is 5.17:1, not 5.22:1. - tokens.json `_meta.overrides` read "repoPrimaryFg #a43c32 (was #e65a4f, 5.17:1 on -primaryBg)", which parses as though the OLD value scored 5.17:1. It scored 2.85:1; 5.17:1 is the new one. That note exists specifically to stop someone re-deriving from the handoff and reverting the a11y fix — as written, it invited exactly that. Now spelled `NEW (ratio) replaces OLD (ratio)`, with both ratios and a PASS/FAIL on each. Every ratio recomputed with the WCAG 2.1 relative-luminance formula. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Merged — thanks, this is a solid audit. I recomputed every pair you changed with the WCAG 2.1 relative-luminance formula and your numbers hold up; in two cases you actually understated the improvement ( One gap it missed, fixed in follow-up #244: the audit walked resting backgrounds only. Also corrected the Worth an issue separately: pulling |
Closes #159. Follow-up to #158 (hover-state contrast — SC 1.4.11). This pass walks every
--color-text-dimtext consumer and the mention-chip palette per the issue's acceptance criteria.Summary
Per-call-site
text-dimaudit. 30 CSS selectors + 12 inlinestyle={}blocks using--color-text-dimfor text migrated to--color-text-muted(≥6:1 on paper-base). 4 non-text uses retained:pr-state-closedchip dots ×2,.stream-card.closeddot bg, and the.rail-session-card:hoverborder affordance.Mention-chip fg darkened to clear AA on its soft-pastel bg:
--color-mention-primary-fg:#e65a4f→#a43c32(2.83:1 → 5.17:1)--color-mention-human-fg:#2a7a5a→#1f6e54(4.39:1 → 5.22:1)--color-mention-attached-fg: kept, already 4.94:1The accent tokens (
--color-accent-coral,--color-accent-mint) keep their resting hue for buttons / focus rings / links, where contrast was already fine. Only the chip-specific fg tokens diverged.@media (prefers-contrast: more)block pushes the muted-text family toward primary for users opting into higher OS contrast (text-muted≈ 9.4:1,text-dim≈ 6.6:1,text-on-dark-muted≈ 10:1).agent_docs/tidewater_handoff/tokens.jsonsynced with the new accessible mention-chip values and a_meta.overridesnote documenting that CSS supersedes JSON for any WCAG-darkened token, so re-derivation from the handoff doc doesn't silently revert the a11y fix.How I chose what to bump vs. keep
Per WCAG 2.1 SC 1.4.3, normal text needs ≥4.5:1; large text (≥18pt regular or ≥14pt bold) needs ≥3:1.
text-dim(#8a93a5) on paper-base (#fbf9f4) is 3.1:1 — passes large-text, fails normal-text. Almost every consumer in the codebase is small text (10–14px hints / timestamps / labels), so the migration is overwhelmingly one-way totext-muted. The four retainedtext-dimuses are all non-text (decorative dots and a hover border), where SC 1.4.11 governs and the 3:1 floor is met.Found during audit, deferred to follow-ups
.channel-header-tierraw-hex pills also fail AA:.tier-bugfix#b26a3aon#fae8d8≈ 3.54:1.tier-questionsky-15%-alpha bg ≈ 3.21:1.tier-feature#2a7a5aon mint-soft ≈ 4.39:1 (borderline)These are tier-specific palette, not
text-dimdrift, so they're out of scope here — happy to open a focused follow-up issue.--color-text-dimno longer applies to text. Renaming to--color-decorative-dimor--color-state-dimwould clarify intent for the 4 remaining decorative consumers. Cross-file churn unrelated to contrast — deferred.Test plan
pnpm format:check— passespnpm typecheck— passespnpm test— 1098 passed / 28 skipped (no orchestrator tests touch GUI colors)pnpm build— passescd gui && pnpm build— passes (vite production build)cd gui && pnpm test— 50 passed (mention-chip + component tests; assertions are on classNames, not computed style, so the token swap is correctly invisible to them)text-mutedsiblings.@repo(primary),@otherrepo(attached), and@user(human) mentions — text should sit comfortably on the soft-pastel background.prefers-contrast: more: macOS System Settings → Accessibility → Display → "Increase contrast" → confirm the muted-text family visibly darkens in the running app.Notes for review
.stream-card.closed .stream-status .dotretains--color-text-dimatopacity: 0.5deliberately — it's a decorative dot paired with a "done" text label that was bumped totext-muted, so the readable signal is in the label and the dot is supplementary. SC 1.4.11 doesn't require 3:1 for supplementary indicators alongside labeled text.--color-mention-primary-lineis stillrgba(230, 90, 79, 0.33)(the resting coral). Now sits next to the darkened brick-red fg — visually intentional (subtle accent border, dark text), but if the line edge reads strangely in review I can darken that too.🤖 Generated with Claude Code