diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index 5ec7e507..e5a31d15 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -33,7 +33,7 @@ spacing, radii, and depth behind every component. Learn what an identity owns and when to author your own. - Use semantic surfaces and intent roles instead of palette values. + Use semantic surfaces and role-based colours instead of palette values. Follow the system setting or force light and dark scopes. diff --git a/apps/docs/content/docs/overview/color.mdx b/apps/docs/content/docs/overview/color.mdx index ec874623..cc759bba 100644 --- a/apps/docs/content/docs/overview/color.mdx +++ b/apps/docs/content/docs/overview/color.mdx @@ -1,6 +1,7 @@ --- title: Colour -description: Use semantic surfaces, content roles, and intents instead of palette values. +description: + Use semantic surfaces, content roles, and the shared role contract instead of palette values. --- Luke UI names colours by their job. A theme maps those roles to values for each identity and colour @@ -28,22 +29,58 @@ import { vars } from '@luke-ui/react/theme'; Choose the role that describes the element's purpose. Do not copy a resolved colour into application CSS, because it will not adapt when the identity or colour mode changes. -## Intent colours +## Semantic roles -Intent roles cover `neutral`, `accent`, `danger`, `info`, `success`, and `warning`. `neutral`, -`accent`, and `danger` are action intents: they render hover and pressed states across `subtle` and -`solid` surfaces, plus matching text, border, and on-solid values where needed. `info`, `success`, -and `warning` are static feedback intents: they render only a `subtle` surface, `border`, and -`text`, with no hover, pressed, solid, or on-solid state. Components choose these values for their -own states. Custom UI can use the same public roles. +Six roles cover the meanings a component needs beyond plain surfaces and text: `neutral`, `accent`, +`info`, `success`, `warning`, and `danger`. Every role offers the same capabilities, so a role's +meaning never limits what it can do: + +- `neutral`: no accent or status meaning, distinct from ordinary surfaces. +- `accent`: preferred, selected, or emphasised. Not a promise of a brand colour. +- `info`: noteworthy factual context. +- `success`: completed, valid, or a positive result. +- `warning`: needs attention but has not failed and is not dangerous. +- `danger`: harm, failure, invalidity, or a critical condition. + +Components choose the role that fits their own tone or state. Custom UI can use the same six roles. + +### Background and foreground + +Each role has a `subtle` and a `solid` background ramp, and each ramp has `rest`, `hover`, and +`pressed` states: + +```tsx +import { vars } from '@luke-ui/react/theme'; + +vars.color.background.warning.subtle.rest; +vars.color.background.warning.solid.hover; +``` + +`rest` is an explicit state rather than an implied default. A token path cannot be both a string +leaf and the parent of `hover` and `pressed`, so resting values need their own name. `solid.pressed` +deliberately reuses the `solid.hover` colour — there is no third, deeper solid rung. A component +carries a pressed look through depth, finish, or a transform instead of inventing another solid +colour. + +Foreground gives each role a resting and a stronger interactive colour, plus a colour guaranteed to +read against that role's solid backgrounds: + +```tsx +vars.color.foreground.warning.rest; +vars.color.foreground.warning.hover; +vars.color.foreground.warning.onSolid; +``` + +There is no foreground `pressed`. A press is carried by the background ramp and other cues, so text +and icons reuse `hover` for a pressed control. ```tsx import { vars } from '@luke-ui/react/theme';
Payment details need review. @@ -51,11 +88,22 @@ import { vars } from '@luke-ui/react/theme'; ``` +### Borders + +Each role also has one state-free border, for example `vars.color.border.warning`. The token +describes meaning; the component decides when to apply it, such as only while invalid or as a +permanent outline. + +A semantic border is measured for contrast but not guaranteed to reach 3:1 — see +[contrast validation](/theming/authoring#contrast-validation). Never rely on one alone to +communicate a required state such as selected, invalid, or checked. Pair it with text, an icon, or +another cue that is separately contrast-gated. + ## Disabled and loading states Use the component's disabled or loading API when it has one. Those states account for interaction @@ -63,6 +111,35 @@ and accessibility as well as colour. Disabled is a state, not a palette: interac with reduced opacity, and only field text recolors, through the public `text.disabled` role. The public `loadingSkeleton` role is available when you are building an equivalent custom element. +## Migrate from `color.intent` + +`color.intent` was the previous asymmetric contract. `neutral`, `accent`, and `danger` rendered a +full interactive ramp, while `info`, `success`, and `warning` rendered only a static subtle kit. It +has been removed: there are no `color.intent` paths and no `--luke-color-intent-*` variables left to +fall back on. If you are upgrading from a release that had them, this table is the old-to-new +reference — move each usage to its `color.background` / `color.foreground` / `color.border` +equivalent: + +| Old path | New path | +| ------------------------------------------- | ---------------------------------------- | +| `color.intent..surface.subtle` | `color.background..subtle.rest` | +| `color.intent..surface.subtleHover` | `color.background..subtle.hover` | +| `color.intent..surface.subtlePressed` | `color.background..subtle.pressed` | +| `color.intent..surface.solid` | `color.background..solid.rest` | +| `color.intent..surface.solidHover` | `color.background..solid.hover` | +| `color.intent..surface.solidPressed` | `color.background..solid.pressed` | +| `color.intent..text` | `color.foreground..rest` | +| `color.intent..textHover` | `color.foreground..hover` | +| `color.intent..onSolid` | `color.foreground..onSolid` | +| `color.intent..border` | `color.border.` | + +CSS variable names followed the same shape, so `--luke-color-intent-danger-surface-solid-hover` +became `--luke-color-background-danger-solid-hover`. + +The current contract also carries capabilities `color.intent` never had: a foreground and border for +`neutral`, and interactive backgrounds, hover foregrounds, and on-solid foregrounds for `info`, +`success`, and `warning`. + ## Continue learning diff --git a/apps/docs/content/docs/overview/styling.mdx b/apps/docs/content/docs/overview/styling.mdx index ac42625d..a37e5e8c 100644 --- a/apps/docs/content/docs/overview/styling.mdx +++ b/apps/docs/content/docs/overview/styling.mdx @@ -163,7 +163,7 @@ selectors or implementation attributes. Use Box and Sprinkles for responsive structure. - Choose semantic surfaces and intent roles for custom UI. + Choose semantic surfaces and roles for custom UI. Browse every public semantic CSS variable. diff --git a/apps/docs/content/docs/overview/theme.mdx b/apps/docs/content/docs/overview/theme.mdx index e7d97b90..8e1c75c8 100644 --- a/apps/docs/content/docs/overview/theme.mdx +++ b/apps/docs/content/docs/overview/theme.mdx @@ -66,7 +66,7 @@ generated palette, or component recipe selectors. Compare identity, colour mode, custom themes, and public tokens. - Learn the semantic roles behind surfaces, content, and intents. + Learn the semantic roles behind surfaces, content, and borders. Use the type scale supplied by the active theme. diff --git a/apps/docs/content/docs/theming/authoring.mdx b/apps/docs/content/docs/theming/authoring.mdx index 5419d500..c214da7c 100644 --- a/apps/docs/content/docs/theming/authoring.mdx +++ b/apps/docs/content/docs/theming/authoring.mdx @@ -64,35 +64,40 @@ only, and one category is not checked. ### Guaranteed at 4.5:1 -Every text/surface pair the compiler emits is a hard gate: `color.text.primary` and -`color.text.secondary` against all four elevation surfaces (`canvas`, `recessed`, `floating`, -`overlay`), `color.text.primary` against the neutral subtle trio, each action or border/text -intent's `text` (and accent's `textHover`) against the base surfaces and its own subtle trio, -feedback intents' `text` against the base surfaces and their subtle surface, and every intent's -`onSolid` text against its solid, solidHover, and solidPressed surfaces. +Every text/surface pair the compiler emits is a hard gate, 86 checks in total: + +- `color.text.primary` and `color.text.secondary` against all four elevation surfaces (`canvas`, + `recessed`, `floating`, `overlay`): 8 checks. +- For each of the six semantic roles (`neutral`, `accent`, `info`, `success`, `warning`, `danger`), + its foreground `rest` and `hover` against `canvas`, `recessed`, and that role's own `subtle` rest, + hover, and pressed backgrounds: 60 checks. +- Each role's `onSolid` foreground against its `solid` rest, hover, and pressed backgrounds: 18 + checks. ### Guaranteed at 3:1 -Two non-text pairs are also hard gates: `color.border.focus` and `color.border.control` must both -reach 3:1 against the canvas and recessed surfaces. `border.control` is a dedicated solved colour -for this boundary, not a scale-step alias that happens to pass. +Four more pairs are hard gates, at the WCAG 2.2 non-text ratio: `color.border.focus` and +`color.border.control`, each against the canvas and recessed surfaces. `border.control` is a +dedicated solved colour for this boundary, not a scale-step alias that happens to pass. That brings +the total to 90 hard gates across both ratios. If any hard gate fails, `defineTheme` throws `ThemeContrastError`. Its `failures` array identifies each colour mode and token pair, so the stylesheet is never written with an invalid combination. ### Measured only, cannot fail the build -The five `color.intent.*.border` pairs (`accent`, `danger`, `info`, `success`, `warning`) are -checked against the base surfaces at 3:1, but only as advisory diagnostics. A miss is recorded and -reported, but never thrown. These borders use the same subtle scale step as ordinary separators, so -an author can choose a neutral character whose intent borders read as gentle separators rather than -assertive boundaries. Diagnostics record whether each check is a hard gate or advisory-only -(`ContrastCheck.hard`), so tooling can tell the two apart without guessing from the token path. +Each role's border (`color.border.neutral`, `color.border.accent`, and so on for all six roles) is +checked against the canvas and recessed surfaces at 3:1, but only as advisory diagnostics — 12 +checks. A miss is recorded and reported, but never thrown. These borders use the same subtle scale +step as ordinary separators, so an author can choose a neutral character whose role borders read as +gentle separators rather than assertive boundaries. Diagnostics record whether each check is a hard +gate or advisory-only (`ContrastCheck.hard`), so tooling can tell the two apart without guessing +from the token path. -Because these pairs are not guaranteed to reach 3:1, do not rely on an intent border alone to -communicate an error, warning, or success state. Pair it with text, an icon, or a -guaranteed-contrast surface change so the state is still legible for a theme whose intent border -happens to measure under 3:1. +Because these pairs are not guaranteed to reach 3:1, never rely on a role border alone to +communicate a required state such as selected, invalid, or checked. Pair it with text, an icon, or a +guaranteed-contrast surface change so the state stays legible for a theme whose role border happens +to measure under 3:1. ### Never checked @@ -103,13 +108,13 @@ separator, never as the sole signal for state or meaning. Separately from contrast validation, `compileTheme` (and so `defineTheme`) can throw `ThemeGenerationError` when a role that must guarantee on-solid text cannot reach an accessible -solid colour. For a single-value accent, `defineTheme` holds the hue and chroma while searching for -a lightness that clears the same on-solid gate the generator enforces internally. An explicit -`{ light, dark }` accent skips that adaptation and is used verbatim for each side. If its authored -lightness sits in an on-solid dead zone (no near-white or near-black text reaches 4.5:1 against it), -`compileTheme` throws `ThemeGenerationError` naming the failing role and mode. Author an explicit -`{ light, dark }` accent when you need exact control over its lightness. Otherwise, author a single -value and let `defineTheme` adapt it. +solid colour. Every one of the six roles is gated this way. For a single-value accent, `defineTheme` +holds the hue and chroma while searching for a lightness that clears the same on-solid gate the +generator enforces internally. An explicit `{ light, dark }` accent skips that adaptation and is +used verbatim for each side. If its authored lightness sits in an on-solid dead zone (no near-white +or near-black text reaches 4.5:1 against it), `compileTheme` throws `ThemeGenerationError` naming +the failing role and mode. Author an explicit `{ light, dark }` accent when you need exact control +over its lightness. Otherwise, author a single value and let `defineTheme` adapt it. ## Next steps diff --git a/apps/docs/content/docs/theming/token-reference.mdx b/apps/docs/content/docs/theming/token-reference.mdx index fd1d156a..c7b5340f 100644 --- a/apps/docs/content/docs/theming/token-reference.mdx +++ b/apps/docs/content/docs/theming/token-reference.mdx @@ -4,8 +4,8 @@ description: Public semantic CSS variables for building custom Luke UI elements. --- `vars` is Luke UI's typed public token contract. Each path resolves to a stable `--luke-*` CSS -variable. For example, `vars.color.intent.danger.surface.solidHover` resolves to -`var(--luke-color-intent-danger-surface-solid-hover)`. +variable. For example, `vars.color.background.danger.solid.hover` resolves to +`var(--luke-color-background-danger-solid-hover)`. Use these variables when a component API or layout utility cannot express a custom element. They adapt to the active identity and colour mode. Generated palette values, component selectors, and @@ -20,8 +20,8 @@ other implementation details are not part of the public styling API. /> Choose tokens by purpose. A card might use `surface.floating`, `radius.surface`, and -`depth.resting`. A warning message might use the warning intent's subtle surface and text. Avoid -carrying resolved colour values into application CSS. +`depth.resting`. A warning message might use the warning role's subtle background and foreground. +Avoid carrying resolved colour values into application CSS. ## Tokens diff --git a/apps/docs/src/examples/box/responsive-layout.tsx b/apps/docs/src/examples/box/responsive-layout.tsx index 3209d819..e611bf26 100644 --- a/apps/docs/src/examples/box/responsive-layout.tsx +++ b/apps/docs/src/examples/box/responsive-layout.tsx @@ -19,7 +19,7 @@ function Item() { justifyContent="center" padding="400" style={{ - backgroundColor: vars.color.intent.neutral.surface.solid, + backgroundColor: vars.color.background.neutral.solid.rest, flex: 1, minBlockSize: '4rem', }} @@ -27,7 +27,7 @@ function Item() { Item diff --git a/apps/docs/src/examples/overview/intent-colours.tsx b/apps/docs/src/examples/overview/intent-colours.tsx deleted file mode 100644 index 325399b5..00000000 --- a/apps/docs/src/examples/overview/intent-colours.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { Box } from '@luke-ui/react/box'; -import { Button } from '@luke-ui/react/button'; -import { Text } from '@luke-ui/react/text'; -import { vars } from '@luke-ui/react/theme'; -import { useState } from 'react'; - -const intents = { - neutral: { - border: vars.color.border.decorative, - label: 'Neutral', - surface: vars.color.intent.neutral.surface.subtle, - text: vars.color.text.primary, - }, - accent: { - border: vars.color.intent.accent.border, - label: 'Accent', - surface: vars.color.intent.accent.surface.subtle, - text: vars.color.intent.accent.text, - }, - success: { - border: vars.color.intent.success.border, - label: 'Success', - surface: vars.color.intent.success.surface.subtle, - text: vars.color.intent.success.text, - }, - info: { - border: vars.color.intent.info.border, - label: 'Info', - surface: vars.color.intent.info.surface.subtle, - text: vars.color.intent.info.text, - }, - warning: { - border: vars.color.intent.warning.border, - label: 'Warning', - surface: vars.color.intent.warning.surface.subtle, - text: vars.color.intent.warning.text, - }, - danger: { - border: vars.color.intent.danger.border, - label: 'Danger', - surface: vars.color.intent.danger.surface.subtle, - text: vars.color.intent.danger.text, - }, -} as const; - -type Intent = keyof typeof intents; - -export default function IntentColoursExample() { - const [intent, setIntent] = useState('info'); - const selectedIntent = intents[intent]; - - return ( - - - {(Object.keys(intents) as Array).map((option) => ( - - ))} - - - - {selectedIntent.label} - - - - ); -} diff --git a/apps/docs/src/examples/overview/role-colours.tsx b/apps/docs/src/examples/overview/role-colours.tsx new file mode 100644 index 00000000..bf997435 --- /dev/null +++ b/apps/docs/src/examples/overview/role-colours.tsx @@ -0,0 +1,66 @@ +import { Box } from '@luke-ui/react/box'; +import { Button } from '@luke-ui/react/button'; +import { Text } from '@luke-ui/react/text'; +import { vars } from '@luke-ui/react/theme'; +import { useState } from 'react'; + +const roles = { + neutral: 'Neutral', + accent: 'Accent', + info: 'Info', + success: 'Success', + warning: 'Warning', + danger: 'Danger', +} as const; + +type Role = keyof typeof roles; + +export default function RoleColoursExample() { + const [role, setRole] = useState('info'); + + return ( + + + {(Object.keys(roles) as Array).map((option) => ( + + ))} + + + + + Subtle + + + + + Solid + + + + + ); +} diff --git a/apps/docs/src/lib/token-reference-generator.test.ts b/apps/docs/src/lib/token-reference-generator.test.ts index 9d222447..1a8cc47f 100644 --- a/apps/docs/src/lib/token-reference-generator.test.ts +++ b/apps/docs/src/lib/token-reference-generator.test.ts @@ -5,7 +5,9 @@ test('generates documented leaf token mappings from the public contract', () => const reference = generateTokenReference(); expect(reference).toContain("'color.surface.canvas': 'var(--luke-color-surface-canvas)';\n"); - expect(reference).toContain('Semantic colours for surfaces, content, borders, loading'); + expect(reference).toContain( + 'Semantic colours for surfaces, content, borders, and loading, plus the six shared semantic roles', + ); expect(reference).toContain("'motion.easing.exit': 'var(--luke-motion-easing-exit)';\n"); expect(reference).not.toContain('MapLeafNodes'); }); diff --git a/apps/docs/src/styles/app.css b/apps/docs/src/styles/app.css index d414ff60..185ea756 100644 --- a/apps/docs/src/styles/app.css +++ b/apps/docs/src/styles/app.css @@ -21,11 +21,11 @@ --color-fd-card: var(--luke-color-surface-resting); --color-fd-card-foreground: var(--luke-color-text-primary); --color-fd-border: var(--luke-color-border-decorative); - --color-fd-primary: var(--luke-color-intent-accent-surface-solid); - --color-fd-primary-foreground: var(--luke-color-intent-accent-on-solid); + --color-fd-primary: var(--luke-color-background-accent-solid-rest); + --color-fd-primary-foreground: var(--luke-color-foreground-accent-on-solid); --color-fd-secondary: var(--luke-color-surface-raised); --color-fd-secondary-foreground: var(--luke-color-text-primary); - --color-fd-accent: var(--luke-color-intent-neutral-surface-subtleHover); + --color-fd-accent: var(--luke-color-background-neutral-subtle-hover); --color-fd-accent-foreground: var(--luke-color-text-primary); --color-fd-ring: var(--luke-color-border-focus); --color-fd-overlay: var(--luke-color-surface-overlay); diff --git a/docs/STYLING.md b/docs/STYLING.md index 0e04ba0d..24ca82c0 100644 --- a/docs/STYLING.md +++ b/docs/STYLING.md @@ -28,8 +28,8 @@ element. Neither step injects styles at runtime. - `theme/foundation.ts`: the internal typed theme-foundation shape `defineTheme` normalises into and the curated colour, radius, and typography defaults. - `theme/color.ts`: OKLCH colour math, sRGB gamut mapping, and WCAG contrast. -- `theme/contrast-policy.ts`: the WCAG ratios, solver headroom and search step, and intent role - groups the generator, the compiler's validation matrix, and the semantic map all read. +- `theme/contrast-policy.ts`: the WCAG ratios, solver headroom and search step, and the canonical + semantic role list the generator, the compiler's validation matrix, and the semantic map all read. - `theme/scale.ts`: the private 12-step family generator (`generateFamily`), including the constrained step-9 solid-anchor search, the per-role capability guarantees, and `passesOnSolidGate`, the on-solid accessibility gate. @@ -57,11 +57,11 @@ token pair when a generated pair misses WCAG 2.2 AA contrast. A single-value acc adapted per mode through a lightness search; it throws when no lightness in the vibrant band is accessible. The raw `ThemeFoundation` object and `buildTheme` are internal only. -Every colour token is generated from a private 12-step scale per role (neutral, accent, danger, -info, success, warning) plus a mode-aware elevation surface set, then mapped onto the public colour -contract. See [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) for the pipeline, the border -and accent contrast policies, and what changed when this generator replaced the original per-token -solver. +Every colour token is generated from a private 12-step scale per role (neutral, accent, info, +success, warning, danger) plus a mode-aware elevation surface set, then mapped onto the public +colour contract. Every role gets the same background, foreground, on-solid, and border capabilities. +See [THEME_COLOUR_GENERATION.md](THEME_COLOUR_GENERATION.md) for the pipeline, the border and accent +contrast policies, and what changed when this generator replaced the original per-token solver. The semantic contract includes `font.100` through `font.900` size steps. Each step groups its font size, line height, letter spacing, and per-font Capsize trims so components cannot combine unrelated diff --git a/docs/THEME_COLOUR_GENERATION.md b/docs/THEME_COLOUR_GENERATION.md index 1855e244..73b5c526 100644 --- a/docs/THEME_COLOUR_GENERATION.md +++ b/docs/THEME_COLOUR_GENERATION.md @@ -10,10 +10,12 @@ Per colour mode, `compileTheme` (in `build-theme.ts`): 1. Resolves the source colours and the canvas anchor (`background`, split from `neutral`'s hue/chroma character — see `define-theme.ts`). -2. Generates six private 12-step OKLCH families (`neutral`, `accent`, `danger`, `info`, `success`, - `warning`) with `scale.ts`'s `generateFamily`. Each family carries steps 1-12 plus a `contrast` - on-solid colour, and guarantees only the capabilities its role's `FamilyRequirements` declares - (see the capability matrix in `scale.ts`). +2. Generates six private 12-step OKLCH families (`neutral`, `accent`, `info`, `success`, `warning`, + `danger`) with `scale.ts`'s `generateFamily`. Each family carries steps 1-12 plus a `contrast` + on-solid colour. Every role now guarantees the same capabilities (see `scale.ts`'s + `FAMILY_REQUIREMENTS`) — the public contract gives all six roles identical background, + foreground, on-solid, and border slots, so there is no role that can opt out of a capability + another role emits. 3. Derives the mode-aware elevation surfaces (`canvas`/`recessed`/`floating`/`overlay`) with `elevation.ts`'s `generateSurfaces`. `surfaces.canvas` is always exactly the resolved `background` — canvas IS the background, not a derived value. @@ -75,9 +77,9 @@ advisory group: the sole resting boundary of a form control, so it cannot use the softer step-7 aesthetic. - `color.border.decorative` is not checked. It is a deliberately subtle, Radix-style separator below 3:1. -- Every intent border (`color.intent.*.border`) is an advisory 3:1 check. A miss does not gate the - build. `color.border.focus` is the other hard gate. The keyboard-focus ring stays build-time - validated at ≥3:1. +- Every semantic role border (`color.border.`, all six roles) is an advisory 3:1 check. A miss + does not gate the build. `color.border.focus` is the other hard gate. The keyboard-focus ring + stays build-time validated at ≥3:1. Component invariant (tracked separately, [#247](https://github.com/lukebennett88/luke-ui/issues/247)): any component that uses an advisory @@ -103,8 +105,8 @@ Accent adaptation is forgiving but never sacrifices the AA on-solid guarantee: readable text. It asks whether the near-white or near-black on-solid colour the generator would choose clears the AA text ratio plus the search headroom across both solid states the engine emits: step 9 and its step-10 hover. There is no third, deeper pressed state to test. -`surface.solidPressed` reuses step 10 and carries the press through depth, finish, and transform -instead. +`background..solid.pressed` reuses step 10 and carries the press through depth, finish, and +transform instead. `defineTheme`'s `adaptAccent` pre-conditioner calls that same function rather than keeping its own copy. That gives two guarantees: @@ -118,14 +120,16 @@ generator's tone-faithful window. It can rescue accents, such as a mid-lightness generator alone would report as unsatisfiable. `contrast-policy.ts` declares the shared thresholds: the 4.5 text ratio, the 3:1 non-text ratio, the -search headroom, and the search step. It also declares the intent role groups used by both the -validation matrix and the semantic map. Previously, separate role lists allowed a role added only to -the map to emit an ungated colour, while a role added only to the compiler threw an internal error. +search headroom, and the search step. It also declares `SEMANTIC_ROLES`, the one canonical role list +used by family generation, the capability matrix, the semantic map, and the validation matrix. +Previously, separate role lists allowed a role added only to the map to emit an ungated colour, +while a role added only to the compiler threw an internal error. One list makes both sides move +together. ## `loadingSkeleton` -`color.loadingSkeleton` maps to the neutral family's step 7 (previously a lighter neutral step), for -better perceptibility of the loading state against typical surfaces. +`color.loadingSkeleton` maps to the neutral family's step 8, for better perceptibility of the +loading state against typical surfaces. ## Alpha is deferred diff --git a/packages/@luke-ui/react/src/icon/icon.stories.tsx b/packages/@luke-ui/react/src/icon/icon.stories.tsx index a0b8aca1..726a4ee0 100644 --- a/packages/@luke-ui/react/src/icon/icon.stories.tsx +++ b/packages/@luke-ui/react/src/icon/icon.stories.tsx @@ -20,8 +20,8 @@ const baseArgs = { const iconSizes: Array> = ['xsmall', 'small', 'medium', 'large']; const colors = { - accent: vars.color.intent.accent.text, - danger: vars.color.intent.danger.text, + accent: vars.color.foreground.accent.rest, + danger: vars.color.foreground.danger.rest, primary: vars.color.text.primary, secondary: vars.color.text.secondary, } as const satisfies Partial, string>>; diff --git a/packages/@luke-ui/react/src/icon/icon.visual.test.tsx b/packages/@luke-ui/react/src/icon/icon.visual.test.tsx index 743c0e17..cc51a88a 100644 --- a/packages/@luke-ui/react/src/icon/icon.visual.test.tsx +++ b/packages/@luke-ui/react/src/icon/icon.visual.test.tsx @@ -48,7 +48,7 @@ test('sizes and glyphs', async () => { for (const appearance of visualAppearances) { test(`semantic content inheritance: ${appearance.theme} ${appearance.mode}`, async () => { const scene = renderVisual( -
+
, appearance, diff --git a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx index 4f23da95..d6256469 100644 --- a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx +++ b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.stories.tsx @@ -63,7 +63,7 @@ export const Color = meta.story({ }, render: (props) => (
-
+
{colors.map((color) => ( diff --git a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.visual.test.tsx b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.visual.test.tsx index bcdc6719..3858a07c 100644 --- a/packages/@luke-ui/react/src/loading-spinner/loading-spinner.visual.test.tsx +++ b/packages/@luke-ui/react/src/loading-spinner/loading-spinner.visual.test.tsx @@ -82,7 +82,7 @@ const themeMatrixStyle = { } satisfies CSSProperties; const spinnerStyle = { - color: vars.color.intent.accent.text, + color: vars.color.foreground.accent.rest, } satisfies CSSProperties; function ThemeMatrixScope({ diff --git a/packages/@luke-ui/react/src/recipes/button.browser.test.ts b/packages/@luke-ui/react/src/recipes/button.browser.test.ts index c74e167f..65d626cd 100644 --- a/packages/@luke-ui/react/src/recipes/button.browser.test.ts +++ b/packages/@luke-ui/react/src/recipes/button.browser.test.ts @@ -57,6 +57,43 @@ test('hover raises and press recesses without scale', () => { expect(pressedTransform.startsWith('matrix(1, 0, 0, 1')).toBe(true); }); +test('subtle appearance walks rest, hover, and pressed background colours per tone', () => { + for (const tone of ['neutral', 'accent', 'danger'] as const) { + const control = mountButton({ appearance: 'subtle', tone }); + + const restingColor = getComputedStyle(control).backgroundColor; + expect(restingColor).toBe(resolveColor(control, `--luke-color-background-${tone}-subtle-rest`)); + + control.dataset.hovered = 'true'; + const hoveredColor = getComputedStyle(control).backgroundColor; + expect(hoveredColor).toBe( + resolveColor(control, `--luke-color-background-${tone}-subtle-hover`), + ); + + delete control.dataset.hovered; + control.dataset.pressed = 'true'; + const pressedColor = getComputedStyle(control).backgroundColor; + expect(pressedColor).toBe( + resolveColor(control, `--luke-color-background-${tone}-subtle-pressed`), + ); + + // Rest, hover, and pressed must be three genuinely distinct steps of the ramp. + expect(new Set([restingColor, hoveredColor, pressedColor])).toHaveLength(3); + } +}); + +test('disabled preserves subtle resting colour and suppresses hover and pressed treatment', () => { + const resting = mountButton({ appearance: 'subtle', tone: 'accent' }); + const disabled = mountButton({ appearance: 'subtle', tone: 'accent' }); + disabled.dataset.disabled = 'true'; + disabled.dataset.hovered = 'true'; + disabled.dataset.pressed = 'true'; + + expect(getComputedStyle(disabled).backgroundColor).toBe( + getComputedStyle(resting).backgroundColor, + ); +}); + test('disabled preserves resting material and ignores interaction', () => { const resting = mountButton(); const disabled = mountButton(); @@ -127,3 +164,11 @@ async function setEmulatedMedia(name?: string, value?: string) { features: name === undefined || value === undefined ? [] : [{ name, value }], }); } + +function resolveColor(scope: HTMLElement, variable: string) { + const probe = scope.appendChild(document.createElement('div')); + probe.style.backgroundColor = `var(${variable})`; + const value = getComputedStyle(probe).backgroundColor; + probe.remove(); + return value; +} diff --git a/packages/@luke-ui/react/src/recipes/button.css.ts b/packages/@luke-ui/react/src/recipes/button.css.ts index 08e687dd..1ad76327 100644 --- a/packages/@luke-ui/react/src/recipes/button.css.ts +++ b/packages/@luke-ui/react/src/recipes/button.css.ts @@ -140,70 +140,69 @@ export const button = recipe({ ...appearance( 'neutral', 'solid', - vars.color.intent.neutral.surface, - vars.color.intent.neutral.onSolid, + vars.color.background.neutral, + vars.color.foreground.neutral.onSolid, ), ...appearance( 'accent', 'solid', - vars.color.intent.accent.surface, - vars.color.intent.accent.onSolid, + vars.color.background.accent, + vars.color.foreground.accent.onSolid, ), ...appearance( 'danger', 'solid', - vars.color.intent.danger.surface, - vars.color.intent.danger.onSolid, + vars.color.background.danger, + vars.color.foreground.danger.onSolid, ), - ...appearance('neutral', 'subtle', vars.color.intent.neutral.surface, vars.color.text.primary), + ...appearance('neutral', 'subtle', vars.color.background.neutral, vars.color.text.primary), ...appearance( 'accent', 'subtle', - vars.color.intent.accent.surface, - vars.color.intent.accent.text, + vars.color.background.accent, + vars.color.foreground.accent.rest, ), ...appearance( 'danger', 'subtle', - vars.color.intent.danger.surface, - vars.color.intent.danger.text, + vars.color.background.danger, + vars.color.foreground.danger.rest, ), ghostAppearance('neutral', vars.color.text.primary), - ghostAppearance('accent', vars.color.intent.accent.text), - ghostAppearance('danger', vars.color.intent.danger.text), + ghostAppearance('accent', vars.color.foreground.accent.rest), + ghostAppearance('danger', vars.color.foreground.danger.rest), ], }); export type ButtonVariants = RecipeSelection; type Tone = 'neutral' | 'accent' | 'danger'; -type Surface = { - solid: string; - solidHover: string; - solidPressed: string; - subtle: string; - subtleHover: string; - subtlePressed: string; -}; +/** The subtle/solid background ramp shape shared by every semantic role. */ +type Background = (typeof vars.color.background)[Tone]; -function appearance(tone: Tone, appearance: 'solid' | 'subtle', surface: Surface, color: string) { - const prefix = appearance === 'solid' ? 'solid' : 'subtle'; +function appearance( + tone: Tone, + appearance: 'solid' | 'subtle', + background: Background, + color: string, +) { + const ramp = background[appearance]; return [ { style: { '@media': { '(forced-colors: active)': { backgroundImage: 'none' }, }, - backgroundColor: surface[prefix], + backgroundColor: ramp.rest, backgroundImage: vars.actionControlFinish.resting, color, selectors: { '&[data-hovered="true"]:not([data-disabled="true"]):not([data-pending="true"])': { - backgroundColor: surface[`${prefix}Hover`], + backgroundColor: ramp.hover, backgroundImage: vars.actionControlFinish.raised, }, '&[data-pressed="true"]:not([data-disabled="true"]):not([data-pending="true"])': { - backgroundColor: surface[`${prefix}Pressed`], + backgroundColor: ramp.pressed, backgroundImage: vars.actionControlFinish.recessed, }, }, @@ -214,7 +213,7 @@ function appearance(tone: Tone, appearance: 'solid' | 'subtle', surface: Surface } function ghostAppearance(tone: Tone, color: string) { - const surface = vars.color.intent[tone].surface; + const subtle = vars.color.background[tone].subtle; return { style: { backgroundColor: 'transparent', @@ -224,11 +223,11 @@ function ghostAppearance(tone: Tone, color: string) { color, selectors: { '&[data-hovered="true"]:not([data-disabled="true"]):not([data-pending="true"])': { - backgroundColor: surface.subtleHover, + backgroundColor: subtle.hover, boxShadow: vars.depth.raised, }, '&[data-pressed="true"]:not([data-disabled="true"]):not([data-pending="true"])': { - backgroundColor: surface.subtlePressed, + backgroundColor: subtle.pressed, boxShadow: vars.depth.recessed, }, }, diff --git a/packages/@luke-ui/react/src/recipes/checkbox.css.ts b/packages/@luke-ui/react/src/recipes/checkbox.css.ts index ac2a6568..156c339b 100644 --- a/packages/@luke-ui/react/src/recipes/checkbox.css.ts +++ b/packages/@luke-ui/react/src/recipes/checkbox.css.ts @@ -82,7 +82,7 @@ const checkboxConfig = { borderWidth: '1px', boxShadow: 'none', boxSizing: 'border-box', - color: vars.color.intent.accent.onSolid, + color: vars.color.foreground.accent.onSolid, display: 'inline-flex', fontSize: checkboxGlyphSize, fontWeight: vars.font.weight.heading, @@ -107,45 +107,45 @@ const checkboxConfig = { '[data-focus-visible="true"] &': focusRing(vars.color.border.focus), '[data-hovered="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &': { backgroundImage: vars.actionControlFinish.raised, - borderColor: vars.color.intent.accent.border, + borderColor: vars.color.border.accent, }, '[data-pressed="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &': { backgroundImage: vars.actionControlFinish.recessed, - borderColor: vars.color.intent.accent.border, + borderColor: vars.color.border.accent, }, '[data-indeterminate="true"] &': { - backgroundColor: vars.color.intent.accent.surface.solid, - borderColor: vars.color.intent.accent.surface.solid, + backgroundColor: vars.color.background.accent.solid.rest, + borderColor: vars.color.background.accent.solid.rest, }, '[data-indeterminate="true"] &::after': { content: '"−"', opacity: 1, }, '[data-invalid="true"] &': { - borderColor: vars.color.intent.danger.border, + borderColor: vars.color.border.danger, }, '[data-selected="true"] &': { - backgroundColor: vars.color.intent.accent.surface.solid, - borderColor: vars.color.intent.accent.surface.solid, + backgroundColor: vars.color.background.accent.solid.rest, + borderColor: vars.color.background.accent.solid.rest, }, '[data-selected="true"] &::after': { opacity: 1, }, '[data-selected="true"][data-hovered="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &, [data-indeterminate="true"][data-hovered="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &': { - backgroundColor: vars.color.intent.accent.surface.solidHover, - borderColor: vars.color.intent.accent.surface.solidHover, + backgroundColor: vars.color.background.accent.solid.hover, + borderColor: vars.color.background.accent.solid.hover, }, '[data-selected="true"][data-pressed="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &, [data-indeterminate="true"][data-pressed="true"]:not([data-disabled="true"]):not([data-readonly="true"]) &': { - backgroundColor: vars.color.intent.accent.surface.solidPressed, - borderColor: vars.color.intent.accent.surface.solidPressed, + backgroundColor: vars.color.background.accent.solid.pressed, + borderColor: vars.color.background.accent.solid.pressed, }, '[data-invalid="true"][data-selected="true"] &, [data-invalid="true"][data-indeterminate="true"] &': { - backgroundColor: vars.color.intent.danger.surface.solid, - borderColor: vars.color.intent.danger.surface.solid, - color: vars.color.intent.danger.onSolid, + backgroundColor: vars.color.background.danger.solid.rest, + borderColor: vars.color.background.danger.solid.rest, + color: vars.color.foreground.danger.onSolid, }, }, }, diff --git a/packages/@luke-ui/react/src/recipes/combobox.browser.test.ts b/packages/@luke-ui/react/src/recipes/combobox.browser.test.ts index 48c33839..55609de0 100644 --- a/packages/@luke-ui/react/src/recipes/combobox.browser.test.ts +++ b/packages/@luke-ui/react/src/recipes/combobox.browser.test.ts @@ -25,7 +25,7 @@ test('the control uses the recessed field material and semantic state grammar', const { control: hovered } = mountControl(); hovered.dataset.hovered = 'true'; expect(getComputedStyle(hovered).borderColor).toBe( - resolveColor(root, '--luke-color-intent-accent-border'), + resolveColor(root, '--luke-color-border-accent'), ); const { control: focused } = mountControl(); @@ -39,7 +39,7 @@ test('the control uses the recessed field material and semantic state grammar', const { control: invalid } = mountControl(); invalid.dataset.invalid = 'true'; expect(getComputedStyle(invalid).borderColor).toBe( - resolveColor(root, '--luke-color-intent-danger-border'), + resolveColor(root, '--luke-color-border-danger'), ); }); @@ -90,7 +90,7 @@ test('in-field actions are quiet inset squares without a permanent divider or tr action.dataset.hovered = 'true'; const hoveredStyle = getComputedStyle(action); expect(hoveredStyle.backgroundColor).toBe( - resolveColor(root, '--luke-color-intent-accent-surface-subtle-hover'), + resolveColor(root, '--luke-color-background-accent-subtle-hover'), ); expect(hoveredStyle.boxShadow).toBe('none'); expect(hoveredStyle.transform).toBe('none'); @@ -99,7 +99,7 @@ test('in-field actions are quiet inset squares without a permanent divider or tr action.dataset.pressed = 'true'; const pressedStyle = getComputedStyle(action); expect(pressedStyle.backgroundColor).toBe( - resolveColor(root, '--luke-color-intent-accent-surface-subtle-pressed'), + resolveColor(root, '--luke-color-background-accent-subtle-pressed'), ); expect(pressedStyle.boxShadow).toBe('none'); expect(pressedStyle.transform).toBe('none'); @@ -118,19 +118,39 @@ test('options keep hover, keyboard focus, selected, and disabled states distinct hovered: 'true', }); const selected = mountItem(root, { selected: 'true' }); + const selectedAndKeyboardFocused = mountItem(root, { + focusVisible: 'true', + focused: 'true', + hovered: 'true', + selected: 'true', + }); const disabled = mountItem(root, { disabled: 'true', focused: 'true' }); expect(getComputedStyle(focused).backgroundColor).toBe( - resolveColor(root, '--luke-color-intent-neutral-surface-subtle'), + resolveColor(root, '--luke-color-background-neutral-subtle-rest'), ); expect(getComputedStyle(hoveredAndFocused).backgroundColor).toBe( - resolveColor(root, '--luke-color-intent-neutral-surface-subtle-hover'), + resolveColor(root, '--luke-color-background-neutral-subtle-hover'), ); expect(getComputedStyle(keyboardFocused).backgroundColor).toBe( - resolveColor(root, '--luke-color-intent-accent-surface-subtle-hover'), + resolveColor(root, '--luke-color-background-accent-subtle-hover'), + ); + expect(getComputedStyle(selected).backgroundColor).toBe( + resolveColor(root, '--luke-color-background-accent-subtle-rest'), + ); + // Selected plus keyboard focus is its own composed step on the ramp, not a + // re-application of either state alone. + expect(getComputedStyle(selectedAndKeyboardFocused).backgroundColor).toBe( + resolveColor(root, '--luke-color-background-accent-subtle-pressed'), ); - const interactiveBackgrounds = [resting, hovered, keyboardFocused, selected].map((item) => { + const interactiveBackgrounds = [ + resting, + hovered, + keyboardFocused, + selected, + selectedAndKeyboardFocused, + ].map((item) => { return getComputedStyle(item).backgroundColor; }); expect(new Set(interactiveBackgrounds)).toHaveLength(interactiveBackgrounds.length); diff --git a/packages/@luke-ui/react/src/recipes/combobox.css.ts b/packages/@luke-ui/react/src/recipes/combobox.css.ts index a24ae92f..210d1055 100644 --- a/packages/@luke-ui/react/src/recipes/combobox.css.ts +++ b/packages/@luke-ui/react/src/recipes/combobox.css.ts @@ -83,11 +83,11 @@ const comboboxActionStyles = { '&[data-disabled="true"]': { cursor: 'not-allowed' }, '&[data-focus-visible="true"]': { ...focusRing(vars.color.border.focus) }, '&[data-hovered="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.accent.surface.subtleHover, + backgroundColor: vars.color.background.accent.subtle.hover, color: vars.color.text.primary, }, '&[data-pressed="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.accent.surface.subtlePressed, + backgroundColor: vars.color.background.accent.subtle.pressed, color: vars.color.text.primary, }, [descendantDisabledSelector]: { color: vars.color.text.disabled }, @@ -153,13 +153,13 @@ const comboboxConfig = { selectors: { [disabled]: { cursor: 'not-allowed', opacity: 0.55 }, [focusWithin]: { - borderColor: vars.color.intent.accent.border, + borderColor: vars.color.border.accent, ...focusRing(vars.color.border.focus), }, - [hover]: { borderColor: vars.color.intent.accent.border }, - [invalid]: { borderColor: vars.color.intent.danger.border }, + [hover]: { borderColor: vars.color.border.accent }, + [invalid]: { borderColor: vars.color.border.danger }, [invalidFocusWithin]: { - borderColor: vars.color.intent.danger.border, + borderColor: vars.color.border.danger, ...focusRing(vars.color.border.focus), }, [readOnly]: { @@ -390,20 +390,20 @@ const comboboxConfig = { opacity: 0.55, }, '&[data-focused="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.neutral.surface.subtle, + backgroundColor: vars.color.background.neutral.subtle.rest, }, '&[data-hovered="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.neutral.surface.subtleHover, + backgroundColor: vars.color.background.neutral.subtle.hover, }, '&[data-focus-visible="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.accent.surface.subtleHover, + backgroundColor: vars.color.background.accent.subtle.hover, }, '&[data-selected="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.accent.surface.subtle, + backgroundColor: vars.color.background.accent.subtle.rest, fontWeight: vars.font.weight.label, }, '&[data-selected="true"][data-focus-visible="true"]:not([data-disabled="true"])': { - backgroundColor: vars.color.intent.accent.surface.subtlePressed, + backgroundColor: vars.color.background.accent.subtle.pressed, }, }, }, diff --git a/packages/@luke-ui/react/src/recipes/field.browser.test.ts b/packages/@luke-ui/react/src/recipes/field.browser.test.ts new file mode 100644 index 00000000..93a238e1 --- /dev/null +++ b/packages/@luke-ui/react/src/recipes/field.browser.test.ts @@ -0,0 +1,84 @@ +import '@luke-ui/react/themes/tactile.css'; +import { afterEach, expect, test } from 'vite-plus/test'; +import { themeRootClassName } from '../theme/index.js'; +import { tactileThemeClassName } from '../themes/index.js'; +import { cx } from '../utils/index.js'; +import { field } from './field.css.js'; + +let mounted: Array = []; + +afterEach(() => { + for (const element of mounted) element.remove(); + mounted = []; +}); + +test('icon necessity indicator marks a required field with the resting danger foreground', () => { + const { root, label } = mountField({ necessityIndicator: 'icon' }); + root.dataset.required = 'true'; + + const indicator = getComputedStyle(label, '::after'); + expect(indicator.content).toBe('"*"'); + expect(indicator.color).toBe(resolveColor(root, '--luke-color-foreground-danger-rest')); +}); + +test('label necessity indicator marks a required field with "(required)" text instead of the asterisk', () => { + const { root, label } = mountField({ necessityIndicator: 'label' }); + root.dataset.required = 'true'; + + const indicator = getComputedStyle(label, '::after'); + expect(indicator.content).toBe('"(required)"'); + // Unlike icon mode, this variant reads as prose rather than a validation cue, + // so it takes the secondary text colour, not the danger foreground. + expect(indicator.color).toBe(resolveColor(root, '--luke-color-text-secondary')); +}); + +test('a non-required field renders neither necessity indicator', () => { + const { label: iconLabel } = mountField({ necessityIndicator: 'icon' }); + const { label: labelLabel } = mountField({ necessityIndicator: 'label' }); + + expect(getComputedStyle(iconLabel, '::after').content).toBe('none'); + expect(getComputedStyle(labelLabel, '::after').content).toBe('none'); +}); + +test('error tone renders the message in the resting danger foreground', () => { + const { root, message } = mountField({ tone: 'error' }); + + expect(getComputedStyle(message).color).toBe( + resolveColor(root, '--luke-color-foreground-danger-rest'), + ); +}); + +test('disabled field text takes the functional disabled colour, not a role colour', () => { + const { root, label, message } = mountField({ tone: 'error' }); + root.dataset.disabled = 'true'; + + const disabledColor = resolveColor(root, '--luke-color-text-disabled'); + expect(getComputedStyle(label).color).toBe(disabledColor); + expect(getComputedStyle(message).color).toBe(disabledColor); + // Disabled must suppress the error tone's danger foreground, not merely add to it. + expect(getComputedStyle(message).color).not.toBe( + resolveColor(root, '--luke-color-foreground-danger-rest'), + ); +}); + +function mountField(options: Parameters[0] = {}) { + const root = document.body.appendChild(document.createElement('div')); + root.className = cx(themeRootClassName, tactileThemeClassName); + root.dataset.colorMode = 'light'; + mounted.push(root); + + const label = root.appendChild(document.createElement('label')); + label.className = field(options).label(); + const message = root.appendChild(document.createElement('p')); + message.className = field(options).message(); + + return { root, label, message }; +} + +function resolveColor(root: HTMLElement, variable: string) { + const probe = root.appendChild(document.createElement('div')); + probe.style.color = `var(${variable})`; + const value = getComputedStyle(probe).color; + probe.remove(); + return value; +} diff --git a/packages/@luke-ui/react/src/recipes/field.css.ts b/packages/@luke-ui/react/src/recipes/field.css.ts index b42e77bd..06382b6a 100644 --- a/packages/@luke-ui/react/src/recipes/field.css.ts +++ b/packages/@luke-ui/react/src/recipes/field.css.ts @@ -56,7 +56,7 @@ const fieldConfig = { label: { selectors: { [`${dataRequiredSelector} &::after`]: { - color: vars.color.intent.danger.text, + color: vars.color.foreground.danger.rest, content: '"*"', marginInlineStart: vars.space[100], }, @@ -84,7 +84,7 @@ const fieldConfig = { }, error: { message: { - color: vars.color.intent.danger.text, + color: vars.color.foreground.danger.rest, }, }, }, diff --git a/packages/@luke-ui/react/src/recipes/link.css.ts b/packages/@luke-ui/react/src/recipes/link.css.ts index a4b53b8a..db6f5962 100644 --- a/packages/@luke-ui/react/src/recipes/link.css.ts +++ b/packages/@luke-ui/react/src/recipes/link.css.ts @@ -23,7 +23,7 @@ const base = styleInLayer('recipes', { transition: 'none', }, }, - color: vars.color.intent.accent.text, + color: vars.color.foreground.accent.rest, cursor: 'pointer', font: 'inherit', outlineColor: 'transparent', @@ -74,13 +74,15 @@ export const link = recipe({ }, tone: { accent: { - color: vars.color.intent.accent.text, + color: vars.color.foreground.accent.rest, selectors: { '&[data-hovered="true"]:not([data-disabled="true"])': { - color: vars.color.intent.accent.textHover, + color: vars.color.foreground.accent.hover, }, + // Press reuses the hover foreground: the shared contract carries no separate pressed + // content colour, so the stronger hover value covers both interactive states. '&[data-pressed="true"]:not([data-disabled="true"])': { - color: vars.color.intent.accent.textHover, + color: vars.color.foreground.accent.hover, }, }, }, diff --git a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts b/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts index 816cd8f4..67022da1 100644 --- a/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts +++ b/packages/@luke-ui/react/src/recipes/loading-spinner.css.ts @@ -94,13 +94,13 @@ const loadingSpinnerConfig = { }, variants: { color: { - accent: { root: { color: vars.color.intent.accent.text } }, - danger: { root: { color: vars.color.intent.danger.text } }, - info: { root: { color: vars.color.intent.info.text } }, + accent: { root: { color: vars.color.foreground.accent.rest } }, + danger: { root: { color: vars.color.foreground.danger.rest } }, + info: { root: { color: vars.color.foreground.info.rest } }, primary: { root: { color: vars.color.text.primary } }, secondary: { root: { color: vars.color.text.secondary } }, - success: { root: { color: vars.color.intent.success.text } }, - warning: { root: { color: vars.color.intent.warning.text } }, + success: { root: { color: vars.color.foreground.success.rest } }, + warning: { root: { color: vars.color.foreground.warning.rest } }, }, size: { large: { root: iconSizeVariants.large }, diff --git a/packages/@luke-ui/react/src/recipes/text-input.css.ts b/packages/@luke-ui/react/src/recipes/text-input.css.ts index 0d4472ed..c96bc362 100644 --- a/packages/@luke-ui/react/src/recipes/text-input.css.ts +++ b/packages/@luke-ui/react/src/recipes/text-input.css.ts @@ -72,17 +72,17 @@ const textInputConfig = { opacity: 0.55, }, [focusWithin]: { - borderColor: vars.color.intent.accent.border, + borderColor: vars.color.border.accent, ...focusRing(vars.color.border.focus), }, [hover]: { - borderColor: vars.color.intent.accent.border, + borderColor: vars.color.border.accent, }, [invalid]: { - borderColor: vars.color.intent.danger.border, + borderColor: vars.color.border.danger, }, [invalidFocusWithin]: { - borderColor: vars.color.intent.danger.border, + borderColor: vars.color.border.danger, ...focusRing(vars.color.border.focus), }, [readOnly]: { diff --git a/packages/@luke-ui/react/src/recipes/text.css.ts b/packages/@luke-ui/react/src/recipes/text.css.ts index 2cfdabc8..65cf0fc2 100644 --- a/packages/@luke-ui/react/src/recipes/text.css.ts +++ b/packages/@luke-ui/react/src/recipes/text.css.ts @@ -46,13 +46,13 @@ const base = styleInLayer('recipes', { }); const colorVariants = { - accent: { color: vars.color.intent.accent.text }, - danger: { color: vars.color.intent.danger.text }, - info: { color: vars.color.intent.info.text }, + accent: { color: vars.color.foreground.accent.rest }, + danger: { color: vars.color.foreground.danger.rest }, + info: { color: vars.color.foreground.info.rest }, primary: { color: vars.color.text.primary }, secondary: { color: vars.color.text.secondary }, - success: { color: vars.color.intent.success.text }, - warning: { color: vars.color.intent.warning.text }, + success: { color: vars.color.foreground.success.rest }, + warning: { color: vars.color.foreground.warning.rest }, } as const; const weightVariants = { diff --git a/packages/@luke-ui/react/src/styles/theme-root.css.ts b/packages/@luke-ui/react/src/styles/theme-root.css.ts index 894d68aa..864bc8d0 100644 --- a/packages/@luke-ui/react/src/styles/theme-root.css.ts +++ b/packages/@luke-ui/react/src/styles/theme-root.css.ts @@ -3,7 +3,7 @@ import { classSelector, lukeUiClassNames } from './class-names.js'; import { globalStyleInLayer } from './layered-style.css.js'; globalStyleInLayer('theme', classSelector(lukeUiClassNames.themeRoot), { - accentColor: vars.color.intent.accent.surface.solid, + accentColor: vars.color.background.accent.solid.rest, color: vars.color.text.primary, fontFamily: vars.font.family.body, ...vars.font[300], diff --git a/packages/@luke-ui/react/src/styles/utilities.stories.tsx b/packages/@luke-ui/react/src/styles/utilities.stories.tsx index 28d81268..dac0f544 100644 --- a/packages/@luke-ui/react/src/styles/utilities.stories.tsx +++ b/packages/@luke-ui/react/src/styles/utilities.stories.tsx @@ -105,7 +105,7 @@ export const WithRenderProp = meta.story({ padding: '400', }); const customStyle = mergeProps(buttonBox, { - style: { backgroundColor: vars.color.intent.accent.surface.solid }, + style: { backgroundColor: vars.color.background.accent.solid.rest }, }); return (