diff --git a/.changeset/danger-destructive-alias.md b/.changeset/danger-destructive-alias.md new file mode 100644 index 00000000..fdfae764 --- /dev/null +++ b/.changeset/danger-destructive-alias.md @@ -0,0 +1,13 @@ +--- +"@tailor-platform/app-shell": minor +--- + +`--destructive` and `--destructive-foreground` are now aliases of `--danger-solid` and `--danger-contrast`, and the shipped components read the danger roles instead of `destructive` utilities. The names, `Button variant="destructive"` and `text-destructive` keep working. + +Visible changes in the default palette: + +- Dark-mode `Button variant="destructive"` is a full `#dc2626` fill with white text. It was `#f87171` at 60% opacity. Hover uses `--danger-solid-hover`. +- `text-destructive` now resolves to the solid colour: `#dc2626` in dark mode (was `#f87171`), which is below 4.5:1 as text on the dark card. Error text in components uses `text-danger-text` (light `#ce0c17`, dark `#ff9083`). Use `text-danger-text` for text in your own code. +- Invalid borders and focus rings on `Input`, `Textarea`, `Checkbox`, `Field` and `DateField` read `--danger-solid`. The dark invalid border changes from `#f87171` to `#dc2626` (3.71:1 on the card). +- `CsvImporter` error cells use `bg-danger-surface`, matching the warning cells. +- A theme that overrides `--destructive` no longer changes the shipped components; override `--danger-solid` (and the other danger roles) instead. `cream`, `bloom` and the theme template no longer set `--destructive`; their values were the same as the default. diff --git a/decisions/semantic-color-roles.md b/decisions/semantic-color-roles.md index 23927ead..0250fe1b 100644 --- a/decisions/semantic-color-roles.md +++ b/decisions/semantic-color-roles.md @@ -83,7 +83,7 @@ This step is the status part of a two-axis palette. The rest is planned as follo | ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------ | | 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 | +| Danger action | red | `--destructive` becomes an alias of `--danger-solid` once its text and border uses move to `--danger-text` / `--danger-border` | done (2026-10-06) | | 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. @@ -108,3 +108,15 @@ The component variant names (`Badge` and `Alert` `error`, Badge `neutral`) are p - 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. + +## Update 2026-10-06: danger action + +The "Danger action" row of the target shape is done. `--destructive` and `--destructive-foreground` are aliases of `--danger-solid` and `--danger-contrast` in `default.css`, light and dark. This supersedes the first item under "Out of scope" and the Consequences bullet that says Button and `text-destructive` still follow `--destructive`. + +- Names stay. `Button variant="destructive"`, `text-destructive`, `bg-destructive` and `border-destructive` keep working and are not deprecated: they are the shadcn vocabulary consumers write. `text-danger-text` is the role to prefer for text. +- Shipped components read the danger roles directly: text uses `text-danger-text`; invalid borders use `border-danger-solid`; focus rings use `ring-danger-solid/20` (dark `/40`, toolbar `/30`); tints use `bg-danger-surface`; the static error box border uses `border-danger-border`. Nothing in `packages/core/src` uses a `destructive` utility. +- Invalid inputs keep the solid. `border-danger-border` (step a6, 1.67:1 on the card) and step 8 (2.62:1 in light mode) are below the 3:1 non-text minimum. The solid is 4.83:1 in light mode and 3.71:1 in dark mode on the card. A unit test holds the solid at 3:1 or more over `--card` and `--background`. +- Dark `Button variant="destructive"` is a full `#dc2626` fill (it was `#f87171` at 60% opacity). The white text on `#dc2626` is 4.83:1; hover is `--danger-solid-hover` (5.66:1). Radix and Primer use the mode's step 9 at full; shadcn uses `dark:bg-destructive/60`. +- `cream`, `bloom` and `_template.css` no longer set `--destructive` (their values equalled the default). `_template.css` lists it under DO NOT SET. A theme that recolours destructive actions overrides `--danger-solid`. +- Visible changes in the default palette: light `text-destructive` goes from `#dc2626` to `#ce0c17` through `text-danger-text`, and dark from `#f87171` to `#ff9083`; the dark invalid border goes from `#f87171` to `#dc2626`; CsvImporter error cells use `bg-danger-surface`, matching the warning cells (`bg-warning-surface`). +- Outside this repository: `cp-apps/apps/couturier/src/theme-tailor.css` still overrides `--destructive`, and the Theme Generator still emits `destructive` as a constant. Both stop reaching the shipped components until they move to `--danger-*`. diff --git a/docs-manifest.json b/docs-manifest.json index 163a1591..b1aabed2 100644 --- a/docs-manifest.json +++ b/docs-manifest.json @@ -65,7 +65,7 @@ "hashes": { "typeSurface": "7cb8a34c1c36eb02", "outline": "797e012e34847b80", - "snapshot": "791be2c6087af41c", + "snapshot": "5b69cb9a28bbcc2e", "outputMd": "824d1aa36bb7f194", "examples": null } @@ -225,7 +225,7 @@ "hashes": { "typeSurface": "16df9927f8394548", "outline": "200f12eb4bec959a", - "snapshot": "d607a315ffce3abc", + "snapshot": "985de4f9c817a3cf", "outputMd": "bce0e2fada1d1638", "examples": "1ee01b1e27764cd2" } @@ -259,7 +259,7 @@ "hashes": { "typeSurface": "3886bf7012f24283", "outline": "916645fe8e5baf10", - "snapshot": "5df639bf40a2bdd7", + "snapshot": "2d057b30b9401e12", "outputMd": "14f7ba23f441df38", "examples": "e8f09fba5acb116f" } @@ -676,7 +676,7 @@ "hashes": { "typeSurface": "7b374d45d53d4535", "outline": "79c9b0f08ca38027", - "snapshot": "4287a03df2854f9a", + "snapshot": "d22d9caf48bb1b3b", "outputMd": "91d901b17aed7781", "examples": null } @@ -897,7 +897,7 @@ "hashes": { "typeSurface": "7c5b64c579f508ee", "outline": "9b273bc9e509f1fe", - "snapshot": "7e0ed658f518fd9a", + "snapshot": "4742086e97cfbdee", "outputMd": "a3a6c9679af99d37", "examples": "fc036f7af21b9dd4" } @@ -1049,9 +1049,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "45dcfe4383a87ea9", + "outline": "8f4b40e6eb2e5381", "snapshot": null, - "outputMd": "2fa0b6a249d8bd4f", + "outputMd": "80b53c137713bde8", "examples": null } }, @@ -1287,9 +1287,9 @@ "symbols": [], "hashes": { "typeSurface": null, - "outline": "e63d837190e71ff1", + "outline": "ef379deb65bcba3e", "snapshot": null, - "outputMd": "112b43c697d30369", + "outputMd": "74855a46c0284abc", "examples": "4d19f4f688e0f0ac" } }, @@ -1339,7 +1339,7 @@ "hashes": { "typeSurface": "3d9225f7356a29b2", "outline": "40952524e46c333e", - "snapshot": "40f4b39567ab051b", + "snapshot": "e775f88bb9fc2565", "outputMd": "c940bc01cd573334", "examples": "0712ee03ad6ce259" } @@ -1390,7 +1390,7 @@ "hashes": { "typeSurface": "2d88f0306efcaa77", "outline": "1257a84ccd91d5c3", - "snapshot": "3b402cab18147ef9", + "snapshot": "c85c431dcd58bc43", "outputMd": "91d70b140c7bb747", "examples": null } @@ -1659,7 +1659,7 @@ "packages/core/skills/app-shell-patterns/references/concepts/modules-and-resources.md": "bf15b5460f22d165", "packages/core/skills/app-shell-patterns/references/concepts/routing-navigation.md": "8034c7f885994b29", "packages/core/skills/app-shell-patterns/references/concepts/sidebar-navigation.md": "dc6b3afc049b4148", - "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "112b43c697d30369", + "packages/core/skills/app-shell-patterns/references/concepts/styling-theming.md": "74855a46c0284abc", "packages/core/skills/app-shell-patterns/references/components/action-panel.md": "71794d4b561e4d4b", "packages/core/skills/app-shell-patterns/references/components/activity-card.md": "4f386e49380fb0b0", "packages/core/skills/app-shell-patterns/references/components/ai-chat.md": "824d1aa36bb7f194", @@ -1738,7 +1738,7 @@ "packages/core/skills/app-shell-patterns/references/patterns/interaction-toast.md": "f68bdab855730f48", "packages/core/skills/app-shell-patterns/references/patterns/list-dense-scan.md": "916baa3cd2b9cbf1", "packages/core/skills/app-shell-patterns/references/pages/document-detail.md": "717ef4e7ae835aff", - "packages/core/skills/app-shell-patterns/references/migrations.md": "013c3fef2303b096", + "packages/core/skills/app-shell-patterns/references/migrations.md": "d5ca48664fedae18", "packages/core/skills/app-shell-patterns/SKILL.md": "12527965a47cf42f" } } diff --git a/docs-src/concepts/styling-theming.docs.outline.md b/docs-src/concepts/styling-theming.docs.outline.md index c2d5d721..6acb9b57 100644 --- a/docs-src/concepts/styling-theming.docs.outline.md +++ b/docs-src/concepts/styling-theming.docs.outline.md @@ -152,7 +152,9 @@ Surfaces are named by **role**, not by depth — there is no numbered `surface-1 | `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` | | `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` | | `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` | -| `--destructive` / `--destructive-foreground` | destructive actions, errors | `bg-destructive` / `text-destructive` | +| `--destructive` / `--destructive-foreground` | alias of danger solid / contrast | `bg-destructive` / `text-destructive` | + +`--destructive` and `--destructive-foreground` are aliases of the danger `solid` and `contrast` roles ([semantic color roles](#semantic-color-roles)), so `bg-destructive` is the danger solid fill in both modes and `Button variant="destructive"` reads the danger roles directly. `text-destructive` still works and resolves to the solid. For text, prefer `text-danger-text`, which meets 4.5:1 on `--card` and `--background`; the solid is `#dc2626` in dark mode, 3.71:1 on the dark card. To recolour destructive actions, override `--danger-solid` (and the other danger roles), not `--destructive`. There are no `-hover` or `-active` brand tokens. Express interaction states with Tailwind variants and opacity — `hover:bg-primary/90`, `active:bg-primary/80` — which is what AppShell's own components do. @@ -195,7 +197,7 @@ In the default palette, `text` on `surface` and `contrast` on `solid` and `solid Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent. -`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. They no longer follow `--destructive`, which still drives `Button` and `text-destructive`. +`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. `Button variant="destructive"`, invalid input borders and error text follow `--danger-*` too, and `--destructive` is an alias of `--danger-solid`. #### Sidebar & charts @@ -654,7 +656,7 @@ These are visual-composition rules every screen must follow, regardless of patte | Intent | Pick | | ---------------------------------- | ------------------------------------------------------------------------------------------------------- | -| Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` | +| Destructive action (delete, void) | `Button variant="destructive"`; `bg-danger-solid` on custom surface; confirm in a dialog at `shadow-lg` | | Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` | | Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` | | Informational callout | `Badge variant="info"`, or `Alert variant="info"` | diff --git a/docs-src/guides/migrations.docs.outline.md b/docs-src/guides/migrations.docs.outline.md index cca666b2..47b4b637 100644 --- a/docs-src/guides/migrations.docs.outline.md +++ b/docs-src/guides/migrations.docs.outline.md @@ -15,6 +15,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an | Version | Change | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| 1.18.0 | [Destructive colours now resolve to the danger roles](#1180-destructive-colours-now-resolve-to-the-danger-roles) | | 1.17.0 | [Status and alert tokens now resolve to the colour roles](#1170-status-and-alert-tokens-now-resolve-to-the-colour-roles) | | 1.15.0 | [Base UI 1.8.0 breaks closed-`Select` and dialog-click tests](#1150-base-ui-180-breaks-closed-select-and-dialog-click-tests) | | 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) | @@ -27,6 +28,26 @@ Each entry states which versions are affected, what breaks, how to detect it, an | 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) | | before 1.0 | [Pre-1.0 breaking changes](#before-10) | +## 1.18.0: destructive colours now resolve to the danger roles + +**Applies to:** apps that show `Button variant="destructive"` in dark mode, apps that use `text-destructive`, `border-destructive` or `bg-destructive` on their own elements, and apps whose theme overrides `--destructive` or `--destructive-foreground`. + +1.18.0 makes `--destructive` and `--destructive-foreground` aliases of `--danger-solid` and `--danger-contrast`. The names, `Button variant="destructive"` and `text-destructive` keep working and are not deprecated. Nothing errors. Three things change silently: + +- **Dark-mode `Button variant="destructive"` is a full `#dc2626` fill.** It was `#f87171` at 60% opacity. The text is `--danger-contrast` (white) and the hover fill is `--danger-solid-hover` instead of 90% opacity. Light mode keeps `#dc2626`. +- **`text-destructive` resolves to the solid.** In dark mode it changes from `#f87171` to `#dc2626`, which is 3.71:1 on the dark card, below 4.5:1 for text. Components shipped with AppShell now use `text-danger-text` for error text, and invalid borders and focus rings read `--danger-solid`. In dark mode the invalid border changes from `#f87171` to `#dc2626`. +- **Themes that override `--destructive` no longer reach the components.** `Button`, invalid `Input`, `Textarea`, `Checkbox`, `Field` and `DateField` borders and rings, error text and the `CsvImporter` error cells read `--danger-*`. An override of `--destructive` still changes `bg-destructive` and `text-destructive` on your own elements, so the two reds can drift apart. The `cream` and `bloom` palettes no longer set `--destructive`; their values were the same as the default. + +How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading. + +What to change: + +- For text, replace `text-destructive` with `text-danger-text`. `text-destructive` still works; use it only where the solid colour is intended. +- For tints and borders, use `bg-danger-surface` and `border-danger-border` instead of `bg-destructive/10` and `border-destructive/30`. +- In a theme that recolours destructive actions, set `--danger-solid` (and `--danger-solid-hover`, `--danger-text`, `--danger-contrast` as needed) instead of `--destructive`. Remove `--destructive` and `--destructive-foreground` from the theme. + +The roles are documented in [Styling & theming](./concepts/styling-theming.md#brand--action). The decision record is `decisions/semantic-color-roles.md`. + ## 1.17.0: status and alert tokens now resolve to the colour roles **Applies to:** apps that use `bg-status-*`, `text-status-*` or `bg-alert-*` utilities, and apps whose theme overrides `--status-*`, `--alert-*` or `--destructive` to recolour `Badge`, `Alert`, `CsvImporter` or `MetricCard`. diff --git a/docs/concepts/styling-theming.md b/docs/concepts/styling-theming.md index 61a21f8d..af168cc8 100644 --- a/docs/concepts/styling-theming.md +++ b/docs/concepts/styling-theming.md @@ -169,7 +169,9 @@ Surfaces are named by **role**, not by depth — there is no numbered `surface-1 | `--primary` / `--primary-foreground` | primary buttons, emphasis | `bg-primary` / `text-primary-foreground` | | `--secondary` / `--secondary-foreground` | secondary buttons, neutral chips | `bg-secondary` / `text-secondary-foreground` | | `--accent` / `--accent-foreground` | hover and selected nav states | `bg-accent` / `text-accent-foreground` | -| `--destructive` / `--destructive-foreground` | destructive actions, errors | `bg-destructive` / `text-destructive` | +| `--destructive` / `--destructive-foreground` | alias of danger solid / contrast | `bg-destructive` / `text-destructive` | + +`--destructive` and `--destructive-foreground` are aliases of the danger `solid` and `contrast` roles ([semantic color roles](#semantic-color-roles)), so `bg-destructive` is the danger solid fill in both modes and `Button variant="destructive"` reads the danger roles directly. `text-destructive` still works and resolves to the solid. For text, prefer `text-danger-text`, which meets 4.5:1 on `--card` and `--background`; the solid is `#dc2626` in dark mode, 3.71:1 on the dark card. To recolour destructive actions, override `--danger-solid` (and the other danger roles), not `--destructive`. There are no `-hover` or `-active` brand tokens. Express interaction states with Tailwind variants and opacity — `hover:bg-primary/90`, `active:bg-primary/80` — which is what AppShell's own components do. @@ -212,7 +214,7 @@ In the default palette, `text` on `surface` and `contrast` on `solid` and `solid Most `surface` and `border` values are translucent tints, so an opacity modifier compounds rather than replaces — `bg-info-surface/50` halves the tint's alpha instead of setting it to 50%. Some light-mode values are opaque instead of translucent: the `warning` surface, surface-hover and border, the `info` border and the `danger` surface-hover. They do not blend with a tinted parent. All dark-mode values are translucent. -`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. They no longer follow `--destructive`, which still drives `Button` and `text-destructive`. +`Badge`, `Alert`, `CsvImporter` and `MetricCard` read these roles directly, not `--status-*` or `--alert-*`. To recolour them, override the roles. `Badge` `error` and `subtle-error`, and `Alert` `error`, follow `--danger-*`. `Button variant="destructive"`, invalid input borders and error text follow `--danger-*` too, and `--destructive` is an alias of `--danger-solid`. #### Sidebar & charts @@ -769,7 +771,7 @@ These are visual-composition rules every screen must follow, regardless of patte | Intent | Pick | | ---------------------------------- | ------------------------------------------------------------------------------------------------------- | -| Destructive action (delete, void) | `Button variant="destructive"`; `bg-destructive` on custom surfaces; confirm in a dialog at `shadow-lg` | +| Destructive action (delete, void) | `Button variant="destructive"`; `bg-danger-solid` on custom surface; confirm in a dialog at `shadow-lg` | | Non-blocking caution | `Badge variant="warning"`, or `bg-warning-surface text-warning-text` | | Confirmation / completed state | `Badge variant="success"`, or `bg-success-surface text-success-text` | | Informational callout | `Badge variant="info"`, or `Alert variant="info"` | diff --git a/docs/migrations.md b/docs/migrations.md index 4ffc9154..a0849155 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -15,6 +15,7 @@ Each entry states which versions are affected, what breaks, how to detect it, an | Version | Change | | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| 1.18.0 | [Destructive colours now resolve to the danger roles](#1180-destructive-colours-now-resolve-to-the-danger-roles) | | 1.17.0 | [Status and alert tokens now resolve to the colour roles](#1170-status-and-alert-tokens-now-resolve-to-the-colour-roles) | | 1.15.0 | [Base UI 1.8.0 breaks closed-`Select` and dialog-click tests](#1150-base-ui-180-breaks-closed-select-and-dialog-click-tests) | | 1.12.0 | [`DateField` / `DatePicker` field chrome moved to `Field.Root`](#1120-datefield--datepicker-field-chrome-moved-to-fieldroot) | @@ -27,6 +28,26 @@ Each entry states which versions are affected, what breaks, how to detect it, an | 1.0.2 | [`Toaster` no longer accepts `richColors`](#102-toaster-no-longer-accepts-richcolors) | | before 1.0 | [Pre-1.0 breaking changes](#before-10) | +## 1.18.0: destructive colours now resolve to the danger roles + +**Applies to:** apps that show `Button variant="destructive"` in dark mode, apps that use `text-destructive`, `border-destructive` or `bg-destructive` on their own elements, and apps whose theme overrides `--destructive` or `--destructive-foreground`. + +1.18.0 makes `--destructive` and `--destructive-foreground` aliases of `--danger-solid` and `--danger-contrast`. The names, `Button variant="destructive"` and `text-destructive` keep working and are not deprecated. Nothing errors. Three things change silently: + +- **Dark-mode `Button variant="destructive"` is a full `#dc2626` fill.** It was `#f87171` at 60% opacity. The text is `--danger-contrast` (white) and the hover fill is `--danger-solid-hover` instead of 90% opacity. Light mode keeps `#dc2626`. +- **`text-destructive` resolves to the solid.** In dark mode it changes from `#f87171` to `#dc2626`, which is 3.71:1 on the dark card, below 4.5:1 for text. Components shipped with AppShell now use `text-danger-text` for error text, and invalid borders and focus rings read `--danger-solid`. In dark mode the invalid border changes from `#f87171` to `#dc2626`. +- **Themes that override `--destructive` no longer reach the components.** `Button`, invalid `Input`, `Textarea`, `Checkbox`, `Field` and `DateField` borders and rings, error text and the `CsvImporter` error cells read `--danger-*`. An override of `--destructive` still changes `bg-destructive` and `text-destructive` on your own elements, so the two reds can drift apart. The `cream` and `bloom` palettes no longer set `--destructive`; their values were the same as the default. + +How to detect it: search the app for `destructive` in class names and in theme CSS, including Theme Generator output that sets `--destructive`. Compare `Button variant="destructive"`, an invalid `Input` and an error message in light and dark mode after upgrading. + +What to change: + +- For text, replace `text-destructive` with `text-danger-text`. `text-destructive` still works; use it only where the solid colour is intended. +- For tints and borders, use `bg-danger-surface` and `border-danger-border` instead of `bg-destructive/10` and `border-destructive/30`. +- In a theme that recolours destructive actions, set `--danger-solid` (and `--danger-solid-hover`, `--danger-text`, `--danger-contrast` as needed) instead of `--destructive`. Remove `--destructive` and `--destructive-foreground` from the theme. + +The roles are documented in [Styling & theming](./concepts/styling-theming.md#brand--action). The decision record is `decisions/semantic-color-roles.md`. + ## 1.17.0: status and alert tokens now resolve to the colour roles **Applies to:** apps that use `bg-status-*`, `text-status-*` or `bg-alert-*` utilities, and apps whose theme overrides `--status-*`, `--alert-*` or `--destructive` to recolour `Badge`, `Alert`, `CsvImporter` or `MetricCard`. diff --git a/packages/core/__snapshots__/src__components__ai-chat__ai-chat.test.tsx.snap b/packages/core/__snapshots__/src__components__ai-chat__ai-chat.test.tsx.snap index bce0f5fe..a6bedcf5 100644 --- a/packages/core/__snapshots__/src__components__ai-chat__ai-chat.test.tsx.snap +++ b/packages/core/__snapshots__/src__components__ai-chat__ai-chat.test.tsx.snap @@ -1,5 +1,5 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`AIChat > snapshots > default 1`] = `"
Hello there.
Hello there.
Pick a date
Required
"`; +exports[`snapshots > DateField — manually labelled + described 1`] = `"Pick a date
Required
"`; -exports[`snapshots > DatePicker — closed 1`] = `"We will never share your email.
We will never share your email.
{errorMessage}
diff --git a/packages/core/src/components/button/button.test.tsx b/packages/core/src/components/button/button.test.tsx index ec37bdd6..d18a4c3b 100644 --- a/packages/core/src/components/button/button.test.tsx +++ b/packages/core/src/components/button/button.test.tsx @@ -1,3 +1,6 @@ +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it, vi } from "vitest"; import { cleanup, render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; @@ -98,12 +101,41 @@ describe("Button", () => { expect(onClick).toHaveBeenCalled(); }); + it("destructive variant uses only bridged danger utilities", () => { + // Precompiled `astw:` utilities emit no CSS when their token is missing + // from the Tailwind bridge, so check every danger class the variant uses. + const bridge = readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "../../assets/theme.bridge.css"), + "utf8", + ); + render(); + const classes = screen.getByRole("button").className.split(/\s+/); + + expect(classes.some((c) => c.includes("destructive"))).toBe(false); + expect(classes).toContain("astw:bg-danger-solid"); + expect(classes).toContain("astw:text-danger-contrast"); + expect(classes).toContain("astw:hover:bg-danger-solid-hover"); + + const tokens = classes + .map( + (c) => + /^astw:(?:dark:)?(?:hover:|focus-visible:)?(?:bg|text|ring)-(danger-[a-z-]+)/.exec( + c, + )?.[1], + ) + .filter((t): t is string => t !== undefined); + expect(tokens.length).toBeGreaterThanOrEqual(3); + for (const token of tokens) { + expect(bridge).toContain(`--color-${token}:`); + } + }); + it("renders different variants", () => { const { rerender } = render(); expect(screen.getByRole("button").className).toContain("bg-primary"); rerender(); - expect(screen.getByRole("button").className).toContain("bg-destructive"); + expect(screen.getByRole("button").className).toContain("bg-danger-solid"); rerender(); expect(screen.getByRole("button").className).toContain("border"); diff --git a/packages/core/src/components/button/button.tsx b/packages/core/src/components/button/button.tsx index a99167cb..a4d5773f 100644 --- a/packages/core/src/components/button/button.tsx +++ b/packages/core/src/components/button/button.tsx @@ -6,7 +6,7 @@ import { cn } from "@/lib/utils"; import { mergeRefs, useToolbarItem } from "../toolbar/toolbar"; const buttonVariants = cva( - "astw:inline-flex astw:cursor-pointer astw:items-center astw:justify-center astw:gap-2 astw:whitespace-nowrap astw:rounded-md astw:text-sm astw:font-medium astw:transition-all astw:disabled:pointer-events-none astw:disabled:opacity-50 astw:[&_svg]:pointer-events-none astw:[&_svg:not([class*='size-'])]:size-4 astw:shrink-0 astw:[&_svg]:shrink-0 astw:outline-none astw:focus-visible:border-ring astw:focus-visible:ring-ring/50 astw:focus-visible:ring-[3px] astw:aria-invalid:ring-destructive/20 astw:dark:aria-invalid:ring-destructive/40 astw:aria-invalid:border-destructive", + "astw:inline-flex astw:cursor-pointer astw:items-center astw:justify-center astw:gap-2 astw:whitespace-nowrap astw:rounded-md astw:text-sm astw:font-medium astw:transition-all astw:disabled:pointer-events-none astw:disabled:opacity-50 astw:[&_svg]:pointer-events-none astw:[&_svg:not([class*='size-'])]:size-4 astw:shrink-0 astw:[&_svg]:shrink-0 astw:outline-none astw:focus-visible:border-ring astw:focus-visible:ring-ring/50 astw:focus-visible:ring-[3px] astw:aria-invalid:ring-danger-solid/20 astw:dark:aria-invalid:ring-danger-solid/40 astw:aria-invalid:border-danger-solid", { variants: { /** Visual style variant */ @@ -14,7 +14,7 @@ const buttonVariants = cva( default: "astw:bg-primary astw:text-primary-foreground astw:shadow-xs astw:hover:bg-primary/90", destructive: - "astw:bg-destructive astw:text-white astw:shadow-xs astw:hover:bg-destructive/90 astw:focus-visible:ring-destructive/20 astw:dark:focus-visible:ring-destructive/40 astw:dark:bg-destructive/60", + "astw:bg-danger-solid astw:text-danger-contrast astw:shadow-xs astw:hover:bg-danger-solid-hover astw:focus-visible:ring-danger-solid/20 astw:dark:focus-visible:ring-danger-solid/40", outline: "astw:border astw:border-border astw:bg-background astw:shadow-xs astw:hover:bg-accent astw:hover:text-accent-foreground astw:dark:bg-input/30 astw:dark:border-input astw:dark:hover:bg-input/50", secondary: diff --git a/packages/core/src/components/checkbox/checkbox.test.tsx b/packages/core/src/components/checkbox/checkbox.test.tsx index 70c8e17e..1baf2984 100644 --- a/packages/core/src/components/checkbox/checkbox.test.tsx +++ b/packages/core/src/components/checkbox/checkbox.test.tsx @@ -127,7 +127,7 @@ describe("Checkbox", () => { render({error}
} + {error &&{error}
}{error}
} + {error &&{error}
} ); } @@ -1980,7 +1980,7 @@ function TemporalFilterEditor({ value={localValueMax} onChange={setLocalValueMax} /> - {betweenError &&{betweenError}
} + {betweenError &&{betweenError}
} ) : (+ *
* {state.validity.typeMismatch * ? "Please enter a valid URL (e.g. https://example.com)" * : "This field is required."} diff --git a/packages/core/src/components/input/input.tsx b/packages/core/src/components/input/input.tsx index 1ee7d950..ab06ba45 100644 --- a/packages/core/src/components/input/input.tsx +++ b/packages/core/src/components/input/input.tsx @@ -29,7 +29,7 @@ function Input({ className, type, ref: forwardedRef, ...props }: InputProps) { className={cn( inputBaseClasses, "astw:file:text-foreground astw:file:inline-flex astw:file:h-7 astw:file:border-0 astw:file:bg-transparent astw:file:text-sm astw:file:font-medium", - "astw:aria-invalid:ring-destructive/20 astw:dark:aria-invalid:ring-destructive/40 astw:aria-invalid:border-destructive", + "astw:aria-invalid:ring-danger-solid/20 astw:dark:aria-invalid:ring-danger-solid/40 astw:aria-invalid:border-danger-solid", className, )} {...props} diff --git a/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx b/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx index 9f1d49f4..aa8c01ba 100644 --- a/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx +++ b/packages/core/src/components/internals/default-error-boundary/default-error-boundary.tsx @@ -17,7 +17,7 @@ export const DefaultErrorBoundary = () => { return (
{t("error404Body")}
@@ -41,7 +41,7 @@ export const DefaultErrorBoundary = () => { return ({message}
diff --git a/packages/core/src/components/textarea/textarea.tsx b/packages/core/src/components/textarea/textarea.tsx index 5d7802b1..914cc56f 100644 --- a/packages/core/src/components/textarea/textarea.tsx +++ b/packages/core/src/components/textarea/textarea.tsx @@ -95,8 +95,8 @@ function Textarea({ className, ...props }: TextareaProps) { // `Field.Root` or from a shadcn-style `aria-invalid` on the control. className={cn( textareaBaseClasses, - "astw:data-invalid:border-destructive astw:data-invalid:ring-destructive/20 astw:dark:data-invalid:ring-destructive/40", - "astw:aria-invalid:border-destructive astw:aria-invalid:ring-destructive/20 astw:dark:aria-invalid:ring-destructive/40", + "astw:data-invalid:border-danger-solid astw:data-invalid:ring-danger-solid/20 astw:dark:data-invalid:ring-danger-solid/40", + "astw:aria-invalid:border-danger-solid astw:aria-invalid:ring-danger-solid/20 astw:dark:aria-invalid:ring-danger-solid/40", className, )} // `Field.Control`'s props are typed against its default `` tag, so