v1.15.0 — categorical (non-severity) tone set - #23
Merged
Conversation
`Tone` is five points on a SEVERITY ramp. A lot of app state has no severity at all — on/off, inherited/overridden, first/duplicate, draft/published, and "which kind of thing is this" — and with only a severity ramp available it all borrowed one: "enabled" rendered as `success`, "duplicate" and "overridden" as `info`, "older version" as `warning`. The UI reports a problem where there is none. The deprecated `purple` alias was the same story in miniature. Adds `Category` as a PARALLEL set, not more members on `Tone`. Widening `Tone` would break every consumer holding its own exhaustive `Record<Tone, X>` map — a semver-major change to ship an additive feature — and severity is ordered while categories are not. - `Category` + `resolveCategory()` mirroring `resolveTone()`'s warnOnce shape - `category` prop on Badge and StatusBadge, sibling to and mutually exclusive with `variant`; passing both warns once and renders the category - nine `--color-cat-*` base/soft/line tokens, registered in tokenCatalog - stories for both, including the before/after and the against-severity grid Three is a MEASURED ceiling. A categorical hue must clear AA as text (>=4.5:1) on white, on bg and on its own soft tint; OKLCH chroma >=0.10; and OKLab dE separation from both the other categories and every severity token (>=15 normal, >=8 under protan/deutan, Machado-Oliveira-Fernandes 2009 at severity 1.0). Sweeping the full Tailwind ramp against those gates rejects most of the wheel: red/rose/pink collapse into danger (dE 6.8/8.5/11.4), orange/amber/yellow into warning (9.6/8.5/9.1), lime/green/emerald into success (10.0/8.4/6.6), sky into muted (12.5), and teal/cyan are not colours at text weight at all — dark enough for AA they measure chroma 0.059/0.066, under the floor, so they render gray. Same trap as the bright amber that could not reach 3:1 on white at any lightness in v1.14.0. What survives is one ~60 degree blue->magenta arc holding exactly three separable steps; a fourth drops the worst pair to dE ~3.0 under simulated red-green colour blindness. More kinds than three need a non-colour channel. Shipped: cat-indigo #4f46e5 (6.29:1 white / 5.87:1 bg / 5.62:1 own soft), cat-purple #581c87 (10.88 / 10.16 / 10.14), cat-magenta #a21caf (6.32 / 5.91 / 5.89). Worst pair dE 16.3 normal / 9.5 CVD; nearest severity 21.3 / 10.8. Non-breaking: `variant` and `category` lose their prop defaults so an unset prop is distinguishable from an explicitly passed one (the exclusivity check needs it); the `neutral` fallback already lived in `resolveTone()`, so every existing call site renders exactly as before. Co-Authored-By: Claude Opus 5 (1M context) <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.
Adds a categorical colour vocabulary alongside the existing severity ramp, so a component that means "this is a kind of thing" stops borrowing
success/warning/danger, which mean "this is good / needs attention / broken".Why
A survey of the consuming app found ~70 call sites across 11 semantic axes borrowing the severity ramp with no severity intended — on/off state (9 definitions, ~25 render sites, uniformly
enabled → success), lifecycle stage, entity kind, field traits, duplicate/replay, provenance. The existing escape hatch made it worse rather than better:infoin this design system is monochrome ink, so thevariant="purple" → infoworkaround rendered an essentially grey badge. The workaround was a visual no-op.The set:
indigo | purple | magenta— and why only threeThe set was not chosen by eye; candidates were swept across the full Tailwind ramp and gated on four measurements simultaneously:
--color-bg, and on its own-softbackgroundMost of the wheel fails:
danger— ΔE 6.8 / 8.5 / 11.4warning— ΔE 9.6 / 8.5 / 9.1success— ΔE 10.0 / 8.4 / 6.6--color-muted— ΔE 12.5What survives is a single ~60° blue→magenta arc holding exactly three separable steps; only 5 valid 3-sets exist in the entire space. A fourth category drops the worst pair to ΔE ≈ 3.0 under simulated red-green colour blindness — which is why the set is three and the ceiling is documented in the tokens, the catalog and the CHANGELOG rather than left for someone to discover by adding a fourth.
The first hand-picked guess, for the record, would have shipped three bugs:
azure↔violetat ΔE 0.31, teal-700 reading as grey, and lime-700 colliding withsuccess.Measured contrast (white /
--color-bg/ own-soft):cat-indigo#4f46e5— 6.29 / 5.87 / 5.62cat-purple#581c87— 10.88 / 10.16 / 10.14cat-magenta#a21caf— 6.32 / 5.91 / 5.89Measured separation: indigo↔purple 17.3 normal / 16.6 CVD · indigo↔magenta 18.2 / 9.5 · purple↔magenta 16.3 / 9.6 · nearest severity pair 21.3 / 10.8. Verified in the browser that the computed colours resolve to exactly these hexes.
Breaking changes: none
Toneis untouched at five values, andpalette: Record<Tone, string>was not widened —categoryPalette: Record<Category, string>is a separate map, so no consumer's ownRecord<Tone, X>breaks.One deliberate subtlety: the
variantprop defaults were removed fromBadgeandStatusBadge. That is load-bearing, not cosmetic — with a default in place, an unset prop is indistinguishable from an explicitvariant="neutral", which makes the tone/category mutual-exclusivity check impossible to write.resolveTone(undefined, 'neutral')already applied the identical fallback one level down, so rendered output is byte-identical for every existing call site.Tokens are namespaced
--color-cat-*rather than--color-indigo, so they cannot shadow Tailwind's own scales in a consuming app.Known limit, recorded not fixed
The consuming app's 15-kind graph palette is not salvageable as colour alone — several of its hues (amber-500, orange-500, emerald-500, rose-500) sit directly on
warning/success/danger, which is the reported bug itself. Finer kind distinction needs a non-colour channel.Separately: the existing
warninganddangerbases are themselves close — ΔE 6.7 normal, 2.9 under CVD. Both always carry a text label, so meaning is never colour-alone, but separating them would change existing rendered output. Recorded in the CHANGELOG as a decision for a major rather than acted on here.Gates
typecheck✓ ·lint✓ (0 errors, 5 warnings — exactly the v1.14.0 baseline; the 4vue/require-default-propwarnings this change introduces are suppressed inline with the reason, since the omission is deliberate) ·build✓ (incl.verify:externalsandverify:dev-warnings) ·build-storybook✓ ·test:ci✓ 226 passed, including 7 new stories.No
dark:variants and noimport.meta.envin the diff; both new warnings usewarnOnce.package.jsonbumped to 1.15.0 — the release workflow hard-fails on a tag/manifest mismatch — and the workflow's awk extraction was confirmed to pull the## v1.15.0body.🤖 Generated with Claude Code