fix(spa): stop using the primary scale as a light-tint background fill - #1091
Merged
Conversation
The `primary` scale is generated from #0033a0 by lightness offset alone and keeps full chroma at every step, so `primary-50` is not the pale wash its name implies — it resolves to rgb(118, 179, 255), a saturated mid-blue. Used as a chip, badge, icon tile or selected-row fill it reads as a blue blob behind small text, and it fails WCAG AA. The `state-*` scales ARE real tints (`state-success-50` = rgb(240, 253, 244)), which is why the pattern looked safe by analogy and wasn't. Measured against the app's own resolved tokens: text-primary-accessible on bg-primary-100 4.13:1 FAIL ...with hover:bg-primary-200 3.52:1 FAIL text-gray-500 sub-label on bg-primary-50 2.23:1 FAIL text-primary-accessible-dark on the dark half 2.63:1 FAIL Replaced 47 opaque `bg-primary-50/100/200` fills and `hover:` affordances across 25 files with the neutral-surface pattern already established by the skill-command chip: `bg-gray-100` + `text-primary-accessible` in light, `bg-gray-700` + `text-primary-50` in dark (9.63:1 / 4.74:1). Sub-labels that the new fills brought closer together were stepped with them — `text-gray-500` on `bg-gray-100` is 4.39:1, so those move to `text-gray-600`/`dark:text-gray-300`. `hover:brightness-90` goes too; it only ever existed to tame the blue. Left alone deliberately: fractional washes (`bg-primary-50/40` composites to rgb(200, 225, 255), a genuine pale tint) on large transient drag-and-drop surfaces, and every solid `bg-primary-500`+ fill with white text. Guardrails so this does not come back: branding/README.md recommended `bg-primary-50` as the decorative-tint utility — that row is what propagated the pattern, and it now documents the neutral one. The tailwind-ui skill's app-conventions reference gains the same rule with the measured numbers. Verified in both themes with computed styles: all 17 shipped pairings pass. 2868 SPA tests pass; development build is clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 finding
The
primaryscale infrontend/ai.client/src/styles/generated/brand-theme.cssis generated from Boise State blue by lightness offset alone, keeping full chroma at every step:So
primary-50is not a pale wash like a Tailwind-50normally is — it is a saturated mid-blue. Used as a chip/badge/selected-row fill it reads as a blue blob behind small text, and the text on it fails WCAG AA. Thestate-*scales are real tints (state-success-50=rgb(240, 253, 244)), which is exactly why the pattern looked safe by analogy and isn't.What was measured
Ratios computed from the app's own resolved token values (read back via
getComputedStyleon the running SPA, not from a palette table):text-primary-accessibleon the fill:hovertext-gray-500)text-primary-accessible-darkon the dark tintAll 17 shipped pairings pass in both themes; the full table is in the verification section below.
The fix
47 opaque
bg-primary-50/100/200fills andhover:affordances across 25 files, replaced with the neutral-surface pattern PR #1090 established for the skill-command chip:bg-gray-100+text-primary-accessiblebg-gray-700+text-primary-50Covered: citation badge + number pills, share/artifact-share owner chips, artifact "Latest" pill, KB engine badge, selected rows in the role dialogs, selected cards in the report/export/share/artifact-share/fine-tuning flows, dialog header icon tiles, the agents empty-state and migration step tiles, the top-users avatar, the cost-report segmented control, two admin form panels, and 11 ghost-button
hover:states (hover:brightness-90goes with them — it only ever existed to tame the blue).Two knock-on fixes the new fills exposed, since a neutral fill sits closer to its text than a blue one did:
text-gray-500onbg-gray-100is 4.39 — those sub-labels move totext-gray-600/dark:text-gray-300.dark:text-primary-accessible-darkis only 2.63 ondark:bg-gray-700, so every ghost button whose dark hover surface changed gaineddark:hover:text-primary-50.Deliberately left alone
bg-primary-50/40composites torgb(200, 225, 255), a genuine pale tint that passes comfortably. These are large transient drag-and-drop surfaces (create-training-job,create-inference-job,skill-form,knowledge-baseopt-in panel) where the blue is the signal and no small text sits on it. Happy to convert them too if you'd rather the blue go entirely.chat-input.component.html(drag-over state) — same category, and PR feat(composer): slash commands to invoke enabled skills #1090 is actively editing that file.bg-primary-500+ fill with white text — toggles, progress bars, checkboxes, status dots. Those were never the problem.Guardrails
src/branding/README.mdwas recommending the broken pattern outright —| Decorative tint (badge or panel background) | bg-primary-50, dark:bg-primary-900/30 |. That row is what propagated it. It now documents the neutral pattern, plus why the scale is not a tint ramp and whyaccessible-darkis not safe on a tinted fill. The same rule, with the measured numbers, is added to.claude/skills/tailwind-ui/references/app-conventions.md.Verification
npx ng test --no-watch— 2868 passed (241 files)npx ng build --configuration development— cleancolorScheme, body background confirmed atoklch(0.985 0.002 247.839)), with contrast read from computed styles rather than eyeballed:Branched from
developat3c2ea5d1; PR #1090's two skill-command sites are untouched.🤖 Generated with Claude Code