Repository navigation
feat(core): add primitive colour scales and semantic colour roles for status colours #569
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
1a49d9e
feat(core): add hue scales and semantic colour roles
73414a2
test(core): check semantic colour contrast from default.css
2f6c43d
fix(core): read semantic colour roles in Badge
a7af910
fix(core): use semantic text roles for CsvImporter counts
b18aaf1
docs(core): document semantic colour roles
d8832c5
docs: add decision record for semantic colour roles
fb53e9d
docs(core): list semantic roles among inherited theme tokens
36c72a4
chore: add changeset for semantic colour roles
1fa66e1
test(core): hoist the linearize helper in the contrast test
d14e110
refactor(core): rename hue scales and semantic roles to the plan names
75f6f1a
fix(examples): use the success text role in the spinner showcase
1494c82
test(core): hold subtle hover text at a documented contrast floor
c564b02
docs: state that Badge reads the roles and not --status-*
2b00bd8
refactor(core): rename the on-solid role to contrast
2a2411b
refactor(core): read the semantic colour roles in Alert and CsvImporter
7c29666
feat(core): add the gray primitive scale and read it from the base te…
3dd50ab
fix(core): read the semantic text roles for MetricCard trends
996a681
docs(core): align palette guidance on colour roles and note the --sta…
4b3125f
Merge origin/main into feat/semantic-color-primitives
aac1b28
Merge remote-tracking branch 'origin/main' into feat/semantic-color-p…
057c079
refactor(core): group the primitive scales and renumber the palette s…
47ee446
docs(core): point custom surfaces at the colour roles and note how a …
3fdca4b
docs(core): state the hover floor and introduce the closing colour ex…
0e035dd
docs(decisions): note that variant names stay until a major
dc2bbcf
docs(core): deprecate --status-* and --alert-* in favour of the colou…
21f5ef5
docs(core): recommend the colour roles in the state and decision exam…
1d6733d
docs(core): separate the neutral and informational callouts; split th…
5ddff12
Merge remote-tracking branch 'origin/main' into feat/semantic-color-p…
66a2f7e
docs(migrations): add the 1.17.0 entry for the status colour roles
8525617
docs(migrations): retitle the 1.17.0 entry and keep the table width
7137fd3
docs(migrations): fix the relative link to the styling guide
c26a9c9
docs(migrations): link the example stylesheet by repository path
a17d456
Merge branch 'main' into feat/semantic-color-primitives
IzumiSy a3f46f6
Merge branch 'main' into feat/semantic-color-primitives
IzumiSy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| "@tailor-platform/app-shell": minor | ||
| --- | ||
|
|
||
| Add semantic colour roles (`--{info|success|warning|danger|neutral}-{surface|surface-hover|border|solid|solid-hover|text|contrast|indicator}`), available as Tailwind colours. `--status-*` and `--alert-*` now alias these roles, and Badge reads them, so `text` on `surface` and `contrast` on `solid` are at least 4.5:1 in the default palette, in light and dark mode. | ||
|
|
||
| ```tsx | ||
| <span className="rounded-md bg-success-surface px-2 py-0.5 text-success-text">Paid</span> | ||
| ``` | ||
|
|
||
| A gray scale (`--primitive-gray-{50..950}`, Tailwind neutral) is also added; the default palette's text and line tokens read it, with no value change. | ||
|
|
||
| Visible changes in the default palette: | ||
|
|
||
| - `--status-*` and `--alert-*` are deprecated and will be removed in the next major. `bg-status-*` and `bg-alert-*` keep working; new code uses the roles. | ||
| - `bg-status-*` and `bg-alert-*` change values. `--status-default` now follows `--muted-foreground` (it was a fixed `#737373`), so it is theme-dependent and translucent in the `cream` and `bloom` palettes. | ||
| - `--status-*` are fill colours; do not use them as text (below 4.5:1 in dark mode). Use `text-{intent}-text` instead (for example `text-status-completed` becomes `text-success-text`). | ||
| - Badge, Alert, CsvImporter and MetricCard read the semantic roles. To recolour them, override `--{intent}-*`; overriding `--status-*` or `--alert-*` no longer changes these components. | ||
| - Badge `error` and `subtle-error`, and Alert `error`, follow `--danger-*` instead of `--destructive`. Button and `text-destructive` still follow `--destructive`. | ||
| - The warning Badge text is dark instead of white. Alert description text now has the same colour as the Alert title. |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # Decision: hue scales and semantic colour roles under `--status-*` and `--alert-*` | ||
|
|
||
| > Status: **Decided for the first step. Hue scales and semantic roles are added; `--status-*` and hue `--alert-*` alias the roles; Badge, Alert, CsvImporter and MetricCard read the roles.** | ||
| > Scope: `packages/core/src/assets/themes/default.css` (hue scales, gray scale, roles, aliases), the Tailwind bridge, and the components that read the roles: Badge, Alert, CsvImporter, MetricCard. `--destructive` and the theme accent scales are not part of this step. | ||
|
|
||
| ## Context | ||
|
|
||
| `--status-*` and `--alert-*` define the same intents (success, warning, error, info) twice, with different values. Neither has a contrast threshold, and some shipped pairs fail WCAG 1.4.3 (white text on the amber warning Badge, for example). Soft backgrounds and borders were built with `color-mix(... 10%, transparent)` per component, with no shared alpha scale. | ||
|
|
||
| Tracking issues (tailor-inc/platform-planning): | ||
|
|
||
| - #1394 alpha scale for soft backgrounds and borders (primary) | ||
| - #1891 `--status-*` and `--alert-*` double-define the same intents | ||
| - #1892 no contrast thresholds; shipping pairs fail WCAG 1.4.3 | ||
| - #1429 status colours are hard to tell apart under colour vision deficiency. This step does not solve it: labels still carry the meaning. | ||
|
|
||
| ## Decision | ||
|
|
||
| ### Hue scales (internal) | ||
|
|
||
| One gray scale and four hues. Each hue has 12 steps, 12 alpha steps and a contrast colour, for light (`:root`) and dark (`.dark`): | ||
|
|
||
| `--primitive-{blue|green|amber|red}-{1..12}`, `--primitive-{hue}-a{1..12}`, `--primitive-{hue}-contrast` | ||
|
|
||
| Gray is the Tailwind `neutral` family, `--primitive-gray-{50..950}`, declared once because it is the same in both modes. It is not generated with Radix: the default palette already used these exact values (`#0a0a0a`, `#737373`, `#e5e5e5`, `#fafafa`, `#a3a3a3`), and the Radix generator cannot produce them. In the default palette `--foreground`, `--muted-foreground`, `--border` and `--input` read gray steps (light 950 / 500 / 200 / 200, dark 50 / 400 for the two text tokens). Dark `--border` and `--input` stay translucent white, and all surfaces stay literal: they are theme inputs, not scale steps. No default value changes. cream and bloom keep their own values; a theme can later pick another family (slate for their navy ink). Gray has no alpha steps. | ||
|
|
||
| They are not bridged to Tailwind. Consumers use the semantic roles, not steps. The `--primitive-` prefix keeps the two layers readable as primitive then semantic, and avoids collisions with a consumer's own copy of Radix Colors. It is not `--palette-` because "palette" already names the theme files (default, cream, bloom). The role tokens have no prefix. The existing `--semantic-shadow-*` tokens are a different kind of token and are not part of this set. | ||
|
|
||
| ### Semantic roles (public, bridged) | ||
|
|
||
| `--{info|success|warning|danger|neutral}-{role}`, bridged as `--color-{intent}-{role}` (5 intents x 8 roles = 40 utilities such as `bg-info-solid`). | ||
|
|
||
| | role | hue source | neutral source | | ||
| | ------------- | ------------------------------- | ----------------------------- | | ||
| | surface | a3 | `var(--muted)` | | ||
| | surface-hover | a4 | `var(--accent)` | | ||
| | border | a6 | `var(--border)` | | ||
| | solid | 9 | `var(--secondary)` | | ||
| | solid-hover | 10 | `var(--accent)` | | ||
| | text | 11 (warning: 12 light, 11 dark) | `var(--foreground)` | | ||
| | contrast | contrast | `var(--secondary-foreground)` | | ||
| | indicator | 9 | `var(--muted-foreground)` | | ||
|
|
||
| The full set is bridged, including roles no component uses yet (neutral solid roles, hue `border`), so the set is one coherent vocabulary and `--alert-*` can alias every role. | ||
|
|
||
| Adding a role or an intent touches four places in one commit: `default.css` (both the `:root, .dark` block and, if the role has a per-mode exception, the `.dark` rule), `theme.bridge.css`, the pinned counts in `theme-bridge.test.ts`, and the roles table in `docs-src/concepts/styling-theming.docs.outline.md`. The docs table is hand-written; only the bridge count is checked by a test. | ||
|
|
||
| The roles and the aliases are declared for `:root, .dark`, so a nested `.dark` wrapper resolves them against its own scales. The only per-mode override is `--warning-text` (step 12 in light, step 11 in dark). | ||
|
|
||
| ### Aliases | ||
|
|
||
| - `--status-default | neutral | completed | attention | danger` alias the neutral indicator and the info, success, warning and danger `solid` roles. `--status-default` therefore follows `--muted-foreground` instead of the fixed `#737373` it had: `#a3a3a3` in dark default, and translucent ink in cream and bloom. | ||
| - `--alert-{success,warning,error,info}-{background,border,foreground}` alias `surface`, `border` and `text` (`error` reads the `danger` intent). | ||
| - `--alert-{h}-foreground-muted` aliases `--alert-{h}-foreground` at full strength. The first plan used 85% of the text colour; over the surface that is 4.26:1 (info), 3.78:1 (success) and 4.23:1 (danger) in light mode, and no opacity below about 97% passes. Keeping the alias on `--alert-{h}-foreground` also keeps the override chain: a consumer who overrides the foreground gets a matching description. | ||
| - `--alert-neutral-*` keeps its values, expressed through the neutral roles. | ||
|
|
||
| ### Badge | ||
|
|
||
| Filled, subtle and outline-dot variants read the semantic roles with `astw:` utilities. `default` and `neutral` are unchanged. | ||
|
|
||
| ### Contrast contract | ||
|
|
||
| A unit test (`semantic-contrast.test.ts`) computes contrast from `default.css` for light and dark and asserts at least 4.5:1 for: `contrast` on `solid` and on `solid-hover`; `text` on `surface`; the alert foreground and description on the alert background. The last two are checked over `--card` and `--background`. | ||
|
|
||
| Not guaranteed: | ||
|
|
||
| - Hover surfaces. Light-mode text on `surface-hover` is 4.22:1 (danger, over both pages), 4.34:1 (success over `--background`) and 4.51:1 (success over `--card`). Subtle Badge hover can fall below 4.5:1 for danger and success. The test holds `text` on `surface-hover` at a floor of 4.2:1 so it cannot get worse unnoticed. This is a floor, not a pass. Changing the step is a design decision for a later change. | ||
| - The `cream` and `bloom` palettes and their tinted shell backgrounds (for example `text` on `surface` over the bloom light shell `#efe8ff` is 4.16:1 for success and 4.15:1 for danger). | ||
| - The neutral intent, which keeps the pre-existing system tokens. | ||
| - Warning `solid` against the card as a fill: 2.15:1 in light mode. `--status-attention` is unchanged (`#f59e0b`), so `text-status-attention` is still low contrast on light surfaces. CsvImporter now uses the warning text role instead. | ||
|
|
||
| ## Provenance | ||
|
|
||
| Seeds: blue `#2563eb` (Tailwind blue-600), green `#15803d` (green-700), amber `#f59e0b` (amber-500), red `#dc2626` (red-600). Each is the lightest Tailwind step with white text at 4.5:1 or more; amber is the documented exception, and its `contrast` colour is pinned to the light step 12 `#4f3515` in both modes (dark text on a bright solid). Scales come from Radix `generateRadixColors` with gray `#737373` and backgrounds `#fafafa` (light) and `#0a0a0a` (dark), generated by `primitive-lab-gen/gen.ts`. The values in `default.css` are copied from its output without edits. | ||
|
|
||
| The "alpha" steps are translucent only where the generator could express them. In light mode, warning a3, a4 and a6, info a6 and danger a4 are opaque 6-digit hex. So the warning surface, surface-hover and border, the info border and the danger surface-hover do not blend with a tinted parent. Dark mode has no opaque values among a3, a4 and a6. Fixing the generator is a later step. | ||
|
|
||
| ## Target shape | ||
|
|
||
| This step is the status part of a two-axis palette. The rest is planned as follows, so the roles added here do not need to change later: | ||
|
|
||
| | Axis | Scale | Roles | Step | | ||
| | ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ | | ||
| | Status (this step) | `--primitive-{blue,green,amber,red}-*` | `--{info,success,warning,danger}-{role}` | done | | ||
| | Ink and lines | `--primitive-gray-*` | base tokens read it (`--foreground`, `--muted-foreground`, `--border`, `--input`) | done for default; themes later | | ||
| | Danger action | red | `--destructive` becomes an alias of `--danger-solid` once its text and border uses move to `--danger-text` / `--danger-border` | next | | ||
| | Brand | an accent scale per theme (bloom and cream read blue, theme-tailor reads cyan) | `--primary`, `--ring` and brand tints read accent steps | with the Theme Generator | | ||
|
|
||
| `--status-*` and `--alert-*` stay as aliases for consumer code and are deprecated: the docs and changeset say so, and they are removed in the next major. Shipped components read the roles. An ESLint rule in `@tailor-platform/eslint-plugin-app-shell` that flags the old utilities and offers the role as a fix is a separate change. | ||
|
|
||
| The component variant names (`Badge` and `Alert` `error`, Badge `neutral`) are public API and are not renamed here. Aligning them with the intent names (`error` → `danger`) is a breaking change; it is a candidate for the next major, together with removing `--status-*` and `--alert-*`. | ||
|
|
||
| ## Out of scope | ||
|
|
||
| - `--destructive` and `--destructive-foreground` aliasing. Themes override `--destructive`, and it is used as solid, text and border at once, so it needs its own change. Button is untouched. | ||
| - Remapping surfaces or other themes onto the gray scale; gray alpha steps. | ||
| - Theme accent scales (the brand axis) and the Theme Generator. | ||
| - Bridging hue scales to Tailwind. | ||
| - New Badge or Alert variants, icons in Badge. | ||
| - Semantic-role swatches on the `/showcase/colors` page. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Names do not change: `bg-status-*`, `bg-alert-*` and `text-destructive` keep working. Values do. | ||
| - `--status-*` are now fill and indicator colours. In dark mode they are step 9 values (`#15803d`, `#dc2626`, `#2563eb`), which are below 4.5:1 as text on the dark card (success 3.57:1, danger 3.71:1, info 3.47:1). CsvImporter text and the custom-components example moved to `text-{intent}-text`. Other consumers that use `text-status-*` in dark mode will see lower contrast. | ||
| - Badge `error` and `subtle-error`, and Alert `error`, no longer follow `--destructive`. They use the danger role in both modes. Button and `text-destructive` still follow `--destructive`, so in dark mode a Badge error (`#dc2626`) and a destructive Button (`#f87171`) differ. Consumers who override `--destructive` for Alert or Badge must override `--danger-*` or `--alert-error-*` instead. | ||
| - The warning Badge text is dark (`#4f3515`) instead of white. | ||
| - Alert description text is the same colour as the Alert title in the default palette. | ||
| - Consumers who override `--status-*` or `--alert-*` keep working for their own `bg-status-*` and `bg-alert-*` utilities, which read those tokens. The shipped components (Badge, Alert, CsvImporter, MetricCard — its trend colours were literal `green-600` / `red-600` before) read the roles, not `--status-*` or `--alert-*`. To recolour them, override the roles (`--{intent}-*`); overriding `--status-*` or `--alert-*` no longer changes the components. `--status-*` and `--alert-*` remain for existing consumer code and are not used by components any more. | ||
| - cream, bloom and `_template.css` values are not edited. The header comments now list `--{intent}-{role}` among the inherited tokens. |
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work @wemra3
Could we also add a short entry to
MIGRATION.mdfor this change?Although the legacy
--status-*and--alert-*tokens remain available, this release changes their resolved values and, more importantly, changes the customization contract forBadge,Alert,CsvImporter, andMetricCard: overriding the legacy tokens or--destructiveno longer recolours those components.A migration note should call out the move to
--{intent}-*roles, recommend replacingtext-status-*withtext-{intent}-text, and note that error Badge/Alert variants now use--danger-*. This will make the upgrade impact easier to discover than the component/theming reference alone.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the review. I added a 1.17.0 entry to docs/migrations.md (via docs-src/guides/migrations.docs.outline.md): who is affected, the three silent changes (values, the recolouring contract for Badge / Alert / CsvImporter / MetricCard, and text-status-* contrast in dark mode), how to detect them, and what to change. I used 1.17.0 because main is now at 1.16.0 and the changeset is minor; I will change the number if you prefer a major bump.