From 9f02b7faa030e7fc8ef9011a6c45fa45f627d652 Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Thu, 27 Aug 2026 15:20:57 +0200 Subject: [PATCH] refactor(useScheme): name the concept `scheme`, not `schema` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `schema` was the wrong word. The platform calls this a color *scheme* — `prefers-color-scheme`, the `color-scheme` property, `` — and in Cube's own vocabulary "schema" means the data model, so a `useSchema()` returning `'light' | 'dark'` lands next to Cloud's SQL Runner `useSchema()` as a pure homonym. This reverts the API rename shipped in 0.170.0 (#1362) and also renames the `` attribute it was built to agree with: `data-schema` becomes `data-scheme`, so `@root(scheme=…)` and `@dark` compile to `:root[data-scheme="dark"]`. The attribute was the older half of the mistake; #1362 propagated it into the API rather than fixing it. No aliases. An alias for the attribute was measured and rejected: teaching `@dark` to accept both spellings expands every arm against both attributes, taking a four-variant palette style map from 21 rules / 3.6 KB to 52 rules / 10.3 KB — a permanent 2.9x on the kit's most-used state to save a one-line edit in the one app that sets it. Co-Authored-By: Claude Opus 5 --- .changeset/scheme-not-schema.md | 20 ++ .storybook/preview.jsx | 12 +- README.md | 2 +- docs/rules/probe.md | 14 +- scripts/chromatic-duplicates.mjs | 10 +- scripts/probe.mjs | 56 +++--- src/components/GlobalStyles.tsx | 10 +- src/components/Root.tsx | 12 +- src/components/actions/Banner/Banner.tsx | 2 +- src/components/actions/Menu/styled.tsx | 2 +- src/components/content/Item/Item.stories.tsx | 2 +- .../content/ItemCard/ItemCard.docs.mdx | 2 +- .../content/PrismCode/PrismCode.docs.mdx | 4 +- src/components/data/AGENTS.md | 2 +- .../data/DataTable/DataTable.browser.test.tsx | 10 +- .../data/DataTable/DataTable.docs.mdx | 2 +- .../data/DataTable/DataTable.stories.tsx | 2 +- src/components/data/TableBase/column-tint.ts | 2 +- src/components/data/TableBase/types.ts | 6 +- src/components/fields/Checkbox/Checkbox.tsx | 2 +- .../ColorSwatchGroup/ColorSwatchGroup.tsx | 2 +- src/components/fields/Picker/Picker.docs.mdx | 2 +- .../fields/Select/Select.stories.tsx | 2 +- src/components/fields/Select/Select.tsx | 2 +- src/components/fields/color/color.ts | 2 +- src/components/layout/Board/Board.tsx | 2 +- src/components/layout/Board/WidgetHost.tsx | 4 +- .../organisms/StatsCard/StatsCard.docs.mdx | 2 +- .../other/CubeLogo/CubeLogo.docs.mdx | 18 +- .../other/CubeLogo/CubeLogo.stories.tsx | 8 +- src/components/other/CubeLogo/CubeLogo.tsx | 26 +-- .../other/CubeLogo/cube-logo.test.tsx | 12 +- .../other/NoDataIcon/NoDataIcon.stories.tsx | 10 +- .../overlays/Tooltip/Tooltip.docs.mdx | 6 +- .../overlays/Tooltip/Tooltip.stories.tsx | 2 +- src/components/overlays/Tooltip/Tooltip.tsx | 8 +- .../overlays/Tooltip/TooltipProvider.docs.mdx | 2 +- .../LoadingAnimation.stories.tsx | 10 +- src/data/item-themes.test.ts | 2 +- src/data/item-themes.ts | 28 +-- src/data/themes.ts | 2 +- src/stories/AdvancedStates.stories.tsx | 14 +- src/stories/Colors.docs.mdx | 4 +- src/stories/Colors.stories.tsx | 4 +- src/stories/Introduction.docs.mdx | 2 +- src/stories/Theming.docs.mdx | 38 ++-- src/stories/Theming.stories.tsx | 44 ++--- src/stories/Usage.docs.mdx | 38 ++-- ...orSchemaBridge.ts => colorSchemeBridge.ts} | 28 +-- ...ithColorSchema.tsx => withColorScheme.tsx} | 62 +++---- src/test/probe/config-guard.ts | 2 +- src/test/probe/harness.browser.probe.tsx | 26 +-- src/test/probe/harness.probe.tsx | 4 +- src/test/probe/io.ts | 2 +- src/tokens/color-theme.test.ts | 24 +-- src/tokens/color-theme.ts | 4 +- src/tokens/colors.ts | 18 +- src/tokens/palette-config.ts | 2 +- src/tokens/palette.test.ts | 172 +++++++++--------- src/tokens/palette.ts | 60 +++--- src/tokens/resolve.ts | 18 +- src/tokens/shadows.ts | 2 +- src/utils/react/index.ts | 10 +- ...{useSchema.test.tsx => useScheme.test.tsx} | 48 ++--- .../react/{useSchema.ts => useScheme.ts} | 56 +++--- 65 files changed, 514 insertions(+), 494 deletions(-) create mode 100644 .changeset/scheme-not-schema.md rename src/stories/decorators/{colorSchemaBridge.ts => colorSchemeBridge.ts} (53%) rename src/stories/decorators/{withColorSchema.tsx => withColorScheme.tsx} (65%) rename src/utils/react/{useSchema.test.tsx => useScheme.test.tsx} (77%) rename src/utils/react/{useSchema.ts => useScheme.ts} (84%) diff --git a/.changeset/scheme-not-schema.md b/.changeset/scheme-not-schema.md new file mode 100644 index 000000000..caa577f5e --- /dev/null +++ b/.changeset/scheme-not-schema.md @@ -0,0 +1,20 @@ +--- +'@cube-dev/ui-kit': minor +--- + +**Breaking:** reverted the `schema` term back to `scheme`, and renamed the `` attribute to match. `schema` was the wrong word — the platform calls this a color _scheme_ (`prefers-color-scheme`, the `color-scheme` property, ``), and in Cube's own vocabulary "schema" means the data model, so a `useSchema()` that returns `'light' | 'dark'` sits next to Cloud's `useSchema()` for SQL Runner as a pure homonym. This undoes the rename shipped in 0.170.0 ([#1362](https://github.com/cube-js/cube-ui-kit/pull/1362)) and fixes the older misspelling it was propagating. + +The API, back to `scheme`: + +- `renderColorTokens()` / `renderPaletteTokens()` / `RenderPaletteOptions`: the `schema` option is `scheme` again — `renderColorTokens({ scheme: 'dark' })`. +- `` / ``: the `schema` prop is `scheme` again. +- `useSchema()`, `resolveSchema()`, `subscribeSchema()` and `ColorSchema` — added in 0.170.0 — are now `useScheme()`, `resolveScheme()`, `subscribeScheme()` and `ColorScheme`. `useHighContrast()` and `resolveHighContrast()` are unchanged. +- The probe's `tokenOptions.schema` is `tokenOptions.scheme` again, and the `pnpm probe` CLI flag `--schema` is `--scheme` (`--scheme hc` still means light + high contrast). + +The DOM opt-in, renamed for the first time: + +- `` is now ``, and the tasty state is `@root(scheme=…)` — so `@dark` compiles to `:root[data-scheme="dark"]`. `data-contrast` is unchanged. + +No aliases, on either half. An alias for the attribute was measured and rejected: teaching the `@dark` state to accept both spellings takes a four-variant palette style map from 21 rules / 3.6 KB to 52 rules / 10.3 KB, because every arm has to be expanded against both attributes — a permanent 2.9× on the kit's most-used state to save a one-line edit. An app that wants a transition window can write both attributes itself; the kit reads only `data-scheme`. + +To migrate, rename the option, the prop, the four hooks and the attribute at your call sites. Apps that set the attribute from JS need the one write updated (`document.documentElement.setAttribute('data-scheme', …)`), and any hand-written CSS selecting `[data-schema]` needs the same rename. diff --git a/.storybook/preview.jsx b/.storybook/preview.jsx index 5037b7a1c..94c74c81d 100644 --- a/.storybook/preview.jsx +++ b/.storybook/preview.jsx @@ -8,7 +8,7 @@ import { create, themes } from 'storybook/theming'; import { Root } from '../src/components/Root'; import { getI18n, LOCALE_LABELS, SUPPORTED_LOCALES } from '../src/i18n'; -import { setToolbarSchema } from '../src/stories/decorators/colorSchemaBridge'; +import { setToolbarScheme } from '../src/stories/decorators/colorSchemeBridge'; // Summarizes DOM/React events before Storybook's action spies see them. Without // it, serializing a focus event over the preview channel costs ~600ms per focus @@ -47,13 +47,13 @@ const darkTheme = create({ configure({ testIdAttribute: 'data-qa', asyncUtilTimeout: 10000 }); -// Bridge the `storybook-dark-mode` toolbar to `` (the +// Bridge the `storybook-dark-mode` toolbar to `` (the // attribute the Glaze `@dark` predefined state resolves against). Subscribed // at module scope so the listener is in place before the addon emits its // initial event after manager/preview channels connect. if (typeof document !== 'undefined') { addons.getChannel().on(DARK_MODE_EVENT_NAME, (isDark) => { - setToolbarSchema(isDark ? 'dark' : 'light'); + setToolbarScheme(isDark ? 'dark' : 'light'); }); } @@ -66,7 +66,7 @@ const ThemedDocsContainer = ({ children, ...props }) => { const [isDark, setIsDark] = useState( () => typeof document !== 'undefined' && - document.documentElement.getAttribute('data-schema') === 'dark', + document.documentElement.getAttribute('data-scheme') === 'dark', ); useEffect(() => { @@ -137,7 +137,7 @@ export const parameters = { // `storybook-dark-mode` configuration. No `current` so the addon resolves // OS `prefers-color-scheme` on first load. `stylePreview: false` keeps the // addon from also injecting dark/light classes on the preview body — the - // `data-schema` attribute set by `colorSchemaBridge` is the only signal + // `data-scheme` attribute set by `colorSchemeBridge` is the only signal // we care about (see `src/components/Root.tsx` and `src/tokens/palette.ts`). darkMode: { dark: darkTheme, @@ -147,7 +147,7 @@ export const parameters = { // Storybook's `addon-backgrounds` injects `.sb-show-main { background: … !important }` // when an option is selected, which overrides the body's `#surface` fill from // `src/components/GlobalStyles.tsx`. Disable it globally so the body's - // schema-aware Glaze background shows through (dark/light). Stories can still + // scheme-aware Glaze background shows through (dark/light). Stories can still // override via `parameters.backgrounds = { disable: false, … }`. // NOTE: the addon's parameter is `disable` (not `disabled`) — the latter is // silently ignored, leaving the addon active and its toolbar still able to diff --git a/README.md b/README.md index 0ef0706c0..976325323 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ function App() { } ``` -To ship your own brand color, tune the palette seeds — every token, in every schema, re-resolves from them: +To ship your own brand color, tune the palette seeds — every token, in every scheme, re-resolves from them: ```tsx diff --git a/docs/rules/probe.md b/docs/rules/probe.md index f5c1b6c1f..f31c4fea5 100644 --- a/docs/rules/probe.md +++ b/docs/rules/probe.md @@ -6,7 +6,7 @@ Answering "what CSS does this actually produce?" used to mean hand-writing a thr ```bash pnpm probe styles '{"fill":"#purple","padding":"2x","preset":"t3"}' -pnpm probe tokens --schema dark --filter surface +pnpm probe tokens --scheme dark --filter surface pnpm probe globals pnpm probe render <<'TSX' import { Button } from '@cube-dev/ui-kit'; @@ -25,8 +25,8 @@ TSX **`tokens`** — two shapes of the same palette, labelled rather than merged: -- `resolved` is `renderColorTokens()`: flat literal values for **one** variant, chosen with `--schema light|dark` and `--hc`. The legacy aliases come back **by reference** (`'#dark': '#surface-text'`) rather than resolved — deliberately, so a region preview re-resolves them against its own tokens — and the probe labels them so you cannot read one as a color. -- `palette` is `getPaletteTokens()`: one tasty state map per token, keyed by schema (`''` / `'@dark'` / `'@hc'` / `'@dark & @hc'`). This is the **four-variant view**, and it is the one a palette change has to be diffed across — see [`docs/glaze/`](../glaze/) on why light mode alone is misleading. +- `resolved` is `renderColorTokens()`: flat literal values for **one** variant, chosen with `--scheme light|dark` and `--hc`. The legacy aliases come back **by reference** (`'#dark': '#surface-text'`) rather than resolved — deliberately, so a region preview re-resolves them against its own tokens — and the probe labels them so you cannot read one as a color. +- `palette` is `getPaletteTokens()`: one tasty state map per token, keyed by scheme (`''` / `'@dark'` / `'@hc'` / `'@dark & @hc'`). This is the **four-variant view**, and it is the one a palette change has to be diffed across — see [`docs/glaze/`](../glaze/) on why light mode alone is misleading. ```bash pnpm probe tokens --json > /tmp/tokens-before.json @@ -35,7 +35,7 @@ pnpm probe tokens --json > /tmp/tokens-after.json diff <(jq -S .palette /tmp/tokens-before.json) <(jq -S .palette /tmp/tokens-after.json) ``` -**`render`** — module-level code, then a trailing JSX expression (or an explicit `export default`). The default export is rendered by React as ``, so a snippet may use hooks — `useState` to probe a controlled input or a disclosure is ordinary, not exotic. Each run gets its own `.probe//` directory, so probing in parallel is safe; directories older than an hour are swept on the next run. It reports the markup plus **only the CSS that snippet caused**: the harness renders `` empty, captures, mounts the snippet, captures again and subtracts. Overlays are reported under `PORTALS` — `` is the `PortalProvider` target, so a `Dialog` renders as its _sibling_ and never appears in the inline markup. `--full-css` keeps the baseline; `--canonical` normalises tasty's class hashes and React's `useId` counters so two renders can be diffed byte-for-byte, **on both tiers** — a browser run is exactly where you would diff one schema or viewport against another. +**`render`** — module-level code, then a trailing JSX expression (or an explicit `export default`). The default export is rendered by React as ``, so a snippet may use hooks — `useState` to probe a controlled input or a disclosure is ordinary, not exotic. Each run gets its own `.probe//` directory, so probing in parallel is safe; directories older than an hour are swept on the next run. It reports the markup plus **only the CSS that snippet caused**: the harness renders `` empty, captures, mounts the snippet, captures again and subtracts. Overlays are reported under `PORTALS` — `` is the `PortalProvider` target, so a `Dialog` renders as its _sibling_ and never appears in the inline markup. `--full-css` keeps the baseline; `--canonical` normalises tasty's class hashes and React's `useId` counters so two renders can be diffed byte-for-byte, **on both tiers** — a browser run is exactly where you would diff one scheme or viewport against another. **`globals`** — everything on the page with only `` mounted: the `:root` token block, the body styles, `@font-face`, the keyframes. Note that only a handful of those rules are attributed to a node, and **those** are all `render` subtracts; the token block reaches the page through `useGlobalStyles` / `injectRawCSS`, so it lives on a global sheet that no per-node dump can see and `render` never had to exclude it. (Cube Cloud's console-ui hands its palette to `` through a tasty `tokens` prop instead, so there the same block _is_ node-attributed and the subtraction is what keeps ~119KB out of every answer. Same command, different reason for the same clean output.) @@ -47,15 +47,15 @@ jsdom is the default only because it is quicker: it reports the CSS tasty genera ```bash pnpm probe:browser render --computed '[data-qa="Card"]' backgroundColor padding -pnpm probe:browser render --schema dark --hc --screenshot +pnpm probe:browser render --scheme dark --hc --screenshot pnpm probe:browser render --rect '[data-qa="Card"]' ``` The same component, both tiers: `var(--surface-2-color)` / `calc(3 * var(--gap))` under `probe`, versus `rgb(248, 248, 249)` / `24px` under `probe:browser`. `--computed` and `--rect` take a CSS selector, so give the component a `qa` prop and select on `[data-qa="…"]`. -Schema and contrast are independent axes, driven through the `` attributes the `@dark` / `@hc` states resolve against — so `--schema dark --hc` reaches the fourth variant, which no single `--schema` value can express. `--schema hc` stays accepted as the spelling Cloud's probe uses and means light + high contrast. +Scheme and contrast are independent axes, driven through the `` attributes the `@dark` / `@hc` states resolve against — so `--scheme dark --hc` reaches the fourth variant, which no single `--scheme` value can express. `--scheme hc` stays accepted as the spelling Cloud's probe uses and means light + high contrast. -**Nothing is silently ignored.** `--computed`, `--rect` and `--screenshot` are rejected on the jsdom tier rather than no-oping: asking for computed values and getting none back reads as "no styles applied", the opposite of the truth. Likewise `probe:browser` refuses every mode but `render`; `--schema` / `--hc` are refused on modes that have no schema (`styles` and `globals` already report every schema at once — their state maps and `@media` blocks _are_ the per-schema answer); an unknown `--schema` is rejected by the CLI rather than reaching the token renderer, where it surfaces as a stack trace that reads like a harness bug instead of a typo; and a flag that needs a value says so instead of defaulting to off — including when the value it would have swallowed is the next flag (`--computed --schema dark`). +**Nothing is silently ignored.** `--computed`, `--rect` and `--screenshot` are rejected on the jsdom tier rather than no-oping: asking for computed values and getting none back reads as "no styles applied", the opposite of the truth. Likewise `probe:browser` refuses every mode but `render`; `--scheme` / `--hc` are refused on modes that have no scheme (`styles` and `globals` already report every scheme at once — their state maps and `@media` blocks _are_ the per-scheme answer); an unknown `--scheme` is rejected by the CLI rather than reaching the token renderer, where it surfaces as a stack trace that reads like a harness bug instead of a typo; and a flag that needs a value says so instead of defaulting to off — including when the value it would have swallowed is the next flag (`--computed --scheme dark`). A snippet that does not compile is reported the same way on both tiers: the parse error, with its file, line and code frame. That takes a detour on the browser tier, because Chromium hands the harness only `Failed to fetch dynamically imported module: ` — the real error is in the 500 body it keeps from script, so the harness re-requests the module to read it. When the module itself compiles and the break is in something it _imports_, only Vite's log names the file, so the probe prints that log under the message instead of dropping it. diff --git a/scripts/chromatic-duplicates.mjs b/scripts/chromatic-duplicates.mjs index 2a81cf278..2c0fb0b07 100644 --- a/scripts/chromatic-duplicates.mjs +++ b/scripts/chromatic-duplicates.mjs @@ -262,15 +262,15 @@ const FINGERPRINT = () => { ].filter((role) => document.querySelector(`[role="${role}"]`)); // A story can change its whole appearance without touching the root: the - // color-schema decorators in `src/stories/decorators/withColorSchema.tsx` - // drive `` / ``, which the `@dark` and + // color-scheme decorators in `src/stories/decorators/withColorScheme.tsx` + // drive `` / ``, which the `@dark` and // `@hc` predefined states resolve against. Fold them into the fingerprint so - // a `DarkSchema` story is not reported as a copy of its light twin. + // a `DarkScheme` story is not reported as a copy of its light twin. const html = document.documentElement; - const schema = `${html.getAttribute('data-schema') ?? ''}/${html.getAttribute('data-contrast') ?? ''}`; + const scheme = `${html.getAttribute('data-scheme') ?? ''}/${html.getAttribute('data-contrast') ?? ''}`; return { - html: `${schema}\n${clone.innerHTML}`, + html: `${scheme}\n${clone.innerHTML}`, text: root.innerText?.slice(0, 400) ?? '', width: finite ? Math.round(right - left) : 0, height: finite ? Math.round(bottom - top) : 0, diff --git a/scripts/probe.mjs b/scripts/probe.mjs index df048efc6..8bb29be94 100644 --- a/scripts/probe.mjs +++ b/scripts/probe.mjs @@ -40,15 +40,15 @@ pnpm probe [options] Modes styles '' CSS for a tasty styles object, under the real ui-kit config (units, recipes, presets, color tokens) - tokens Color tokens: resolved literal values for one schema, + tokens Color tokens: resolved literal values for one scheme, plus the four-variant state maps declares render HTML + the CSS this snippet caused. Snippet on stdin. globals Everything on the page with only mounted: the :root token block, body styles, @font-face, keyframes Options - --schema tokens: light | dark (default: light) - --hc tokens: resolve the high-contrast variant of --schema + --scheme tokens: light | dark (default: light) + --hc tokens: resolve the high-contrast variant of --scheme --filter tokens: only tokens whose name contains this --full-css render: do not subtract the baseline --canonical render: normalise tasty hashes and React IDs (for @@ -59,10 +59,10 @@ Options for 'pnpm probe:browser' only (these need a real browser) --computed [prop...] resolved values — jsdom reports var(...) as text --rect geometry — jsdom sizes everything at 0 --screenshot write a PNG beside the result - --schema render: drive into light | dark | hc - --hc render: high contrast, composable with --schema + --scheme render: drive into light | dark | hc + --hc render: high contrast, composable with --scheme so 'dark --hc' reaches the fourth variant - ('--schema hc' alone means light + hc) + ('--scheme hc' alone means light + hc) Notes The snippet is NOT typechecked — oxc strips types without checking them. @@ -87,8 +87,8 @@ function parseArgs(argv) { * A missing value used to land as `undefined`, which every downstream check * reads as "flag absent" — so `probe:browser render --computed` produced a * plain render and looked like an answer to a computed-values question that was - * never asked. Worse, `--computed --schema dark` took `--schema` itself as the - * selector, silently dropping the schema too. Both are the failure this tool + * never asked. Worse, `--computed --scheme dark` took `--scheme` itself as the + * selector, silently dropping the scheme too. Both are the failure this tool * refuses to commit elsewhere, so they are errors rather than defaults. */ const takeValue = (flag, index) => { @@ -122,8 +122,8 @@ function parseArgs(argv) { options.json = true; } else if (arg === '--hc') { options.highContrast = true; - } else if (arg === '--schema') { - options.schema = takeValue(arg, (i += 1)); + } else if (arg === '--scheme') { + options.scheme = takeValue(arg, (i += 1)); } else if (arg === '--filter') { options.filter = takeValue(arg, (i += 1)); } else if (arg === '--screenshot') { @@ -257,44 +257,44 @@ function main() { } } - // Which modes can actually act on a schema. Silently ignoring `--schema dark` - // on a mode that has no schema is the same failure as silently ignoring + // Which modes can actually act on a scheme. Silently ignoring `--scheme dark` + // on a mode that has no scheme is the same failure as silently ignoring // `--computed`: the answer comes back looking like the light-mode result was // the dark-mode result. - const schemaAware = browser ? ['render'] : ['tokens']; + const schemeAware = browser ? ['render'] : ['tokens']; if ( - (options.schema || options.highContrast) && - !schemaAware.includes(options.mode) + (options.scheme || options.highContrast) && + !schemeAware.includes(options.mode) ) { - const flags = [options.schema && '--schema', options.highContrast && '--hc'] + const flags = [options.scheme && '--scheme', options.highContrast && '--hc'] .filter(Boolean) .join(', '); console.error( `probe ${options.mode}: ${flags} has no effect here.\n` + (options.mode === 'styles' || options.mode === 'globals' - ? `'${options.mode}' reports what tasty generated for every schema at once — the state ` + - `maps and @media blocks in its output ARE the per-schema answer.\n` - : `The jsdom tier cannot resolve a schema's colors at all. Use 'pnpm probe tokens ` + - `--schema ' for literal values, or 'pnpm probe:browser render --schema ' ` + + ? `'${options.mode}' reports what tasty generated for every scheme at once — the state ` + + `maps and @media blocks in its output ARE the per-scheme answer.\n` + : `The jsdom tier cannot resolve a scheme's colors at all. Use 'pnpm probe tokens ` + + `--scheme ' for literal values, or 'pnpm probe:browser render --scheme ' ` + `for what a browser computes.\n`), ); process.exit(1); } - // Validated here rather than left to the palette. An unknown schema reaches + // Validated here rather than left to the palette. An unknown scheme reaches // `renderColorTokens`, misses the variant lookup and throws inside the token // renderer — surfacing as a vitest stack trace about `Object.keys(undefined)`, // which reads as a harness bug rather than as a typo in the flag. - const schemas = browser ? ['light', 'dark', 'hc'] : ['light', 'dark']; + const schemes = browser ? ['light', 'dark', 'hc'] : ['light', 'dark']; - if (options.schema && !schemas.includes(options.schema)) { + if (options.scheme && !schemes.includes(options.scheme)) { console.error( - `probe: unknown --schema "${options.schema}". Expected ${schemas.join(' | ')}.` + + `probe: unknown --scheme "${options.scheme}". Expected ${schemes.join(' | ')}.` + (browser ? `\n('hc' is light + high contrast, the spelling Cube Cloud's probe uses. ` + - `For dark + high contrast, pass '--schema dark --hc'.)` + `For dark + high contrast, pass '--scheme dark --hc'.)` : `\nAdd --hc for the high-contrast variant of either.`), ); process.exit(1); @@ -342,7 +342,7 @@ function main() { if (options.mode === 'tokens') { input.tokenOptions = { - schema: options.schema ?? 'light', + scheme: options.scheme ?? 'light', highContrast: Boolean(options.highContrast), }; } @@ -375,7 +375,7 @@ function main() { // file's contents in through `define` at config-load time, so anything added // after the write never reaches the harness. if (browser) { - input.schema = options.schema; + input.scheme = options.scheme; input.highContrast = Boolean(options.highContrast); input.computed = options.computed; input.computedProps = options.computedProps; @@ -545,7 +545,7 @@ function print(result, options, harnessLog = '') { ([name]) => !options.filter || name.includes(options.filter), ); - console.log(`# resolved — ${result.schema} (${entries.length} tokens)`); + console.log(`# resolved — ${result.scheme} (${entries.length} tokens)`); for (const [name, value] of entries) { // Legacy aliases come back by reference, not resolved — flagged so a // '#surface-text' value is not read as a literal color. diff --git a/src/components/GlobalStyles.tsx b/src/components/GlobalStyles.tsx index 8cb0ea9c7..01823a050 100644 --- a/src/components/GlobalStyles.tsx +++ b/src/components/GlobalStyles.tsx @@ -20,8 +20,8 @@ interface GlobalStylesProps { /** * Body styles applied via useGlobalStyles. * - * `fill` and `color` use schema-aware Glaze tokens so the whole page flips - * automatically when `` (or `prefers-color-scheme: dark`) + * `fill` and `color` use scheme-aware Glaze tokens so the whole page flips + * automatically when `` (or `prefers-color-scheme: dark`) * is active — see `src/tokens/palette.ts` and `src/components/Root.tsx`. */ const BODY_STYLES: Styles = { @@ -93,10 +93,10 @@ const STATIC_CSS = ` /* * Prism syntax highlighting. * - * All token colors come from the schema-aware Glaze \`code-*\` palette + * All token colors come from the scheme-aware Glaze \`code-*\` palette * (defined in src/tokens/palette.ts). Each \`code-*\` token has \`mode: 'auto'\` * with a numeric contrast floor of 4.5 against \`#surface\` so every token - * reads at WCAG AA in light AND dark schemas (AAA in high-contrast). + * reads at WCAG AA in light AND dark schemes (AAA in high-contrast). * Diff insertion / deletion re-use the adaptive \`success-*\` and \`danger-*\` * ramps for both the line bg and the token color. */ @@ -161,7 +161,7 @@ const STATIC_CSS = ` white-space: normal; } - /* Diff (re-uses success/danger ramps; both bg and text adapt to schema) */ + /* Diff (re-uses success/danger ramps; both bg and text adapt to scheme) */ .token.inserted-sign { background-color: var(--success-bg-color); color: var(--success-text-color); diff --git a/src/components/Root.tsx b/src/components/Root.tsx index 7599c9fb1..7445df1cb 100644 --- a/src/components/Root.tsx +++ b/src/components/Root.tsx @@ -17,7 +17,7 @@ import { NavigationAdapter } from '../providers/navigation.types'; import { TrackingProps, TrackingProvider } from '../providers/TrackingProvider'; import { PaletteConfig, setPaletteConfig } from '../tokens/palette-config'; import { EventBusProvider } from '../utils/react/useEventBus'; -import { AMBIENT_PREDEFINED_STATES } from '../utils/react/useSchema'; +import { AMBIENT_PREDEFINED_STATES } from '../utils/react/useScheme'; import { extractStyles } from '../utils/styles'; import { TASTY_VERSION, VERSION } from '../version'; @@ -28,14 +28,14 @@ import { PortalProvider } from './portal'; import type { i18n as I18nInstance } from 'i18next'; -// Color-schema aliases for the Glaze-generated palette (see `src/tokens/palette.ts`). +// Color-scheme aliases for the Glaze-generated palette (see `src/tokens/palette.ts`). // Attribute opt-in wins over system preference: -// → forces the dark schema -// → forces the high-contrast schema +// → forces the dark scheme +// → forces the high-contrast scheme // Otherwise falls back to the user's `prefers-color-scheme` / `prefers-contrast`. // -// The strings live in `useSchema.ts`, which also reads the same two conditions -// from JS (`useSchema()` / `useHighContrast()`) — one definition, so the CSS and +// The strings live in `useScheme.ts`, which also reads the same two conditions +// from JS (`useScheme()` / `useHighContrast()`) — one definition, so the CSS and // the JS answers cannot drift apart. setGlobalPredefinedStates(AMBIENT_PREDEFINED_STATES); diff --git a/src/components/actions/Banner/Banner.tsx b/src/components/actions/Banner/Banner.tsx index 4c672fb3c..818fceb8f 100644 --- a/src/components/actions/Banner/Banner.tsx +++ b/src/components/actions/Banner/Banner.tsx @@ -63,7 +63,7 @@ const BannerElement = tasty(Item, { // shape rather than the borderless default. // // `current` is the right color source: a banner labels itself `#white` in both -// schemas (see `BannerLinkElement` and `*_PRIMARY_STYLES`), so the action mixes +// schemes (see `BannerLinkElement` and `*_PRIMARY_STYLES`), so the action mixes // its chip, border and label from that white — label cr 4.3-5.0 against every // banner theme, with the chip a subtle 1.06 off the banner. That is what an // action on a saturated surface should be: the banner carries the emphasis, the diff --git a/src/components/actions/Menu/styled.tsx b/src/components/actions/Menu/styled.tsx index 2a76e37a6..400ab013b 100644 --- a/src/components/actions/Menu/styled.tsx +++ b/src/components/actions/Menu/styled.tsx @@ -22,7 +22,7 @@ export const StyledMenuWrapper = tasty({ '': '', // Use the design-system `$shadow` token (resolves to // `0 .5x 2x #shadow-md`) so the popover shadow follows the - // schema-aware Glaze `#shadow-md` color. The previous literal + // scheme-aware Glaze `#shadow-md` color. The previous literal // `0px 5px 15px #dark.05` baked in `#dark` (now aliased to the // adaptive `#surface-text`) which inverts to a *light* shadow on // dark surfaces. diff --git a/src/components/content/Item/Item.stories.tsx b/src/components/content/Item/Item.stories.tsx index 0f1f6b75d..073863e48 100644 --- a/src/components/content/Item/Item.stories.tsx +++ b/src/components/content/Item/Item.stories.tsx @@ -1267,7 +1267,7 @@ DescriptionWithTypes.parameters = { docs: { description: { story: - "Demonstrates how descriptions work with all Item type variants. The description inherits the color schema and adapts to each type's visual style, ensuring consistent readability across different type configurations.", + "Demonstrates how descriptions work with all Item type variants. The description inherits the color scheme and adapts to each type's visual style, ensuring consistent readability across different type configurations.", }, }, }; diff --git a/src/components/content/ItemCard/ItemCard.docs.mdx b/src/components/content/ItemCard/ItemCard.docs.mdx index 9c81f1788..27f415d1b 100644 --- a/src/components/content/ItemCard/ItemCard.docs.mdx +++ b/src/components/content/ItemCard/ItemCard.docs.mdx @@ -64,7 +64,7 @@ Inherits all styling from `Item`. See [Item documentation](/docs/content-item--d ### Themes -Use the `theme` prop to apply a semantic color schema: +Use the `theme` prop to apply a semantic color scheme: - `default` — Standard card appearance - `success` — Green theme for positive outcomes diff --git a/src/components/content/PrismCode/PrismCode.docs.mdx b/src/components/content/PrismCode/PrismCode.docs.mdx index 0078c2fbe..4ca023af7 100644 --- a/src/components/content/PrismCode/PrismCode.docs.mdx +++ b/src/components/content/PrismCode/PrismCode.docs.mdx @@ -34,7 +34,7 @@ Supports [Base properties](/docs/getting-started-base-properties--docs). ## Theme -Syntax colors are driven by a dedicated, schema-aware palette of `code-*` tokens. Each token uses a different hue so token classes stay visually separable, and every token meets at least **WCAG AA (4.5:1)** contrast against `#surface` in light, dark, and high-contrast schemas (AAA in HC). +Syntax colors are driven by a dedicated, scheme-aware palette of `code-*` tokens. Each token uses a different hue so token classes stay visually separable, and every token meets at least **WCAG AA (4.5:1)** contrast against `#surface` in light, dark, and high-contrast schemes (AAA in HC). | Token | Hue | Used for | | --- | --- | --- | @@ -46,7 +46,7 @@ Syntax colors are driven by a dedicated, schema-aware palette of `code-*` tokens | `#code-function` | pink | functions, class names, regex, variables | | `#code-attribute` | cyan | attribute names, properties, selectors, built-ins, keys | -Diff insertion (`+`) and deletion (`-`) intentionally re-use the existing `#success-*` / `#danger-*` ramps — both the line background tint and the foreground color adapt to the active schema. +Diff insertion (`+`) and deletion (`-`) intentionally re-use the existing `#success-*` / `#danger-*` ramps — both the line background tint and the foreground color adapt to the active scheme. ## Examples diff --git a/src/components/data/AGENTS.md b/src/components/data/AGENTS.md index 7b3f1b0d6..4f2142b89 100644 --- a/src/components/data/AGENTS.md +++ b/src/components/data/AGENTS.md @@ -9,7 +9,7 @@ Nothing under this directory may reference `measure`, `dimension`, `pivot`, `dri The escape hatches that keep this line holdable: - `column.header.menu` / `column.header.actions` take an opaque `ReactNode`, so a caller can mount an arbitrarily complex column menu without the kit knowing what is in it. `onColumnMenuAction(action, columnKey)` reports the pressed key back as a bare string. The single exception is the reserved sort keys, and only because sorting is a capability the table itself owns — `pin` and `hide` are deliberately NOT reserved, because there is no column pin/visibility state behind them to drive. -- `column.color` takes a hue rather than a resolved colour, so the kit derives the ramp and the contrast floor per schema instead of trusting a caller's hex pair. See `column-tint.ts` and `src/tokens/color-theme.ts`. +- `column.color` takes a hue rather than a resolved colour, so the kit derives the ramp and the contrast floor per scheme instead of trusting a caller's hex pair. See `column-tint.ts` and `src/tokens/color-theme.ts`. - `renderCellMenu`, `column.render`, `column.cellStyles` and `renderRow` cover the rest. If something seems to need domain knowledge in here, it needs a new generic prop instead. diff --git a/src/components/data/DataTable/DataTable.browser.test.tsx b/src/components/data/DataTable/DataTable.browser.test.tsx index 11d867b04..e39105a54 100644 --- a/src/components/data/DataTable/DataTable.browser.test.tsx +++ b/src/components/data/DataTable/DataTable.browser.test.tsx @@ -590,7 +590,7 @@ describe('DataTable column menu visibility', () => { * The three things Cube Cloud's ag-grid version gets wrong are the three things * asserted here: banding survives inside a tinted column, the fill stays opaque * so a pinned column still occludes, and the text clears WCAG AA in every - * schema rather than only the one it was picked in. + * scheme rather than only the one it was picked in. */ describe('DataTable column colors', () => { const TINTED: CubeDataTableColumn[] = [ @@ -706,9 +706,9 @@ describe('DataTable column colors', () => { it.each([ ['light', {}], - ['dark', { schema: 'dark' }], + ['dark', { scheme: 'dark' }], ['high contrast', { contrast: 'high' }], - ['dark high contrast', { schema: 'dark', contrast: 'high' }], + ['dark high contrast', { scheme: 'dark', contrast: 'high' }], ] as const)('clears WCAG AA in %s', async (_name, attrs) => { const root = document.documentElement; @@ -722,7 +722,7 @@ describe('DataTable column colors', () => { )!; const [even, odd] = cellsOf('revenue'); - // Both bands and the header. Glaze re-solves the tone per schema against a + // Both bands and the header. Glaze re-solves the tone per scheme against a // real contrast floor, which is exactly what a hex pair picked once in // light mode cannot do. for (const element of [even, odd, header]) { @@ -733,7 +733,7 @@ describe('DataTable column colors', () => { ).toBeGreaterThanOrEqual(4.5); } } finally { - delete root.dataset.schema; + delete root.dataset.scheme; delete root.dataset.contrast; } }); diff --git a/src/components/data/DataTable/DataTable.docs.mdx b/src/components/data/DataTable/DataTable.docs.mdx index 821e861c3..4a7b1f25a 100644 --- a/src/components/data/DataTable/DataTable.docs.mdx +++ b/src/components/data/DataTable/DataTable.docs.mdx @@ -401,7 +401,7 @@ A stale order is safe: keys for columns that no longer exist are ignored, and a `column.color` tints a whole column — header, cells and pinned totals. It takes a palette theme name, any CSS color, a `{ hue, saturation }` seed, or a `{ fill, text }` pair for full manual control. -Every form but the last is _derived_: only the hue and saturation are kept, and the tone ramp plus an AA/AAA text floor are re-solved for light, dark and high-contrast. A column cannot end up unreadable in a schema nobody checked. +Every form but the last is _derived_: only the hue and saturation are kept, and the tone ramp plus an AA/AAA text floor are re-solved for light, dark and high-contrast. A column cannot end up unreadable in a scheme nobody checked. ```tsx const columns = [ diff --git a/src/components/data/DataTable/DataTable.stories.tsx b/src/components/data/DataTable/DataTable.stories.tsx index b2b546f29..a55d2e00d 100644 --- a/src/components/data/DataTable/DataTable.stories.tsx +++ b/src/components/data/DataTable/DataTable.stories.tsx @@ -655,7 +655,7 @@ export const PersistedColumnLayout: Story = { * * A palette theme name (`'success'`, `'note'`, …) is the cheap form. Any CSS * colour works too: only its hue and saturation are kept, and the tone ramp plus - * an AA/AAA text floor are re-solved per schema by Glaze. Flip the toolbar to + * an AA/AAA text floor are re-solved per scheme by Glaze. Flip the toolbar to * dark or high contrast and every column stays readable — which is the point, * and the thing hand-picked hex pairs get wrong. * diff --git a/src/components/data/TableBase/column-tint.ts b/src/components/data/TableBase/column-tint.ts index 893edd4f0..2adf4c6da 100644 --- a/src/components/data/TableBase/column-tint.ts +++ b/src/components/data/TableBase/column-tint.ts @@ -75,7 +75,7 @@ function isThemeName(value: string): value is CubeTableColumnTheme { * * A status theme seeded by a COLOR needs nothing special here: its resolved seed already * carries that color's hue and chroma, so a tinted column follows it for free. Only the - * two numbers are passed on — a runtime tint re-derives its own lightness per schema, so + * two numbers are passed on — a runtime tint re-derives its own lightness per scheme, so * the seed's `color` and `colorTone` have nothing to say to it and are not part of * `ColorThemeConfig`. */ diff --git a/src/components/data/TableBase/types.ts b/src/components/data/TableBase/types.ts index 1495ffbdd..d66f24121 100644 --- a/src/components/data/TableBase/types.ts +++ b/src/components/data/TableBase/types.ts @@ -233,7 +233,7 @@ export type CubeTableColumnTheme = * How a column is tinted. * * Every form but the last is *derived*: only a hue and a saturation are kept, and - * the tone ramp plus an `AA`/`AAA` text floor are re-solved per colour schema. So + * the tone ramp plus an `AA`/`AAA` text floor are re-solved per colour scheme. So * a column stays readable in light, dark and high contrast without the caller * checking — which is the part hand-picked hex pairs get wrong. */ @@ -248,7 +248,7 @@ export type CubeTableColumnColor = * Full manual control, as tasty colour strings (`'#note-surface'`, `'#purple.10'`). * * Nothing is derived and nothing is contrast-checked — this is the escape - * hatch, and readability in every schema becomes the caller's problem. + * hatch, and readability in every scheme becomes the caller's problem. * `fillBand` defaults to `fill`, which turns banding off for the column. */ | { fill: string; fillBand?: string; text?: string }; @@ -317,7 +317,7 @@ export interface CubeTableColumn { /** * Tints the column — header, cells and pinned totals — with an adaptive fill - * and a text colour solved to stay readable on it in every schema. + * and a text colour solved to stay readable on it in every scheme. * * Row banding survives: the tint carries its own band one tone step away, so * the stripe still reads down the column instead of being painted over. diff --git a/src/components/fields/Checkbox/Checkbox.tsx b/src/components/fields/Checkbox/Checkbox.tsx index 40eaa8f09..a2c13ec65 100644 --- a/src/components/fields/Checkbox/Checkbox.tsx +++ b/src/components/fields/Checkbox/Checkbox.tsx @@ -89,7 +89,7 @@ const CheckboxElement = tasty({ // The check / minus icon is always rendered inside the box, so the // default color must be transparent when the checkbox is neither // checked nor indeterminate — otherwise the white stroke shows through - // the dark `#surface` fill in dark schemas (it used to be hidden + // the dark `#surface` fill in dark schemes (it used to be hidden // accidentally by the legacy white-on-white `fill: '#white'`). color: { '': '#clear', diff --git a/src/components/fields/ColorSwatchGroup/ColorSwatchGroup.tsx b/src/components/fields/ColorSwatchGroup/ColorSwatchGroup.tsx index c276c03d3..df3666200 100644 --- a/src/components/fields/ColorSwatchGroup/ColorSwatchGroup.tsx +++ b/src/components/fields/ColorSwatchGroup/ColorSwatchGroup.tsx @@ -32,7 +32,7 @@ export type CubeColorSwatchItem = string | { color: string; label?: string }; * Two rings drawn inside the swatch, as React Aria marks selection. Outer ring * first: earlier shadows paint over later ones, so this reads as 2bw of * `#surface-text` at the edge and 2bw of `#surface` within it. Two tones that - * flip with the schema stay visible against any color the swatch holds. + * flip with the scheme stay visible against any color the swatch holds. */ const SELECTED_RING = 'inset 0 0 0 2bw #surface-text, inset 0 0 0 4bw #surface'; diff --git a/src/components/fields/Picker/Picker.docs.mdx b/src/components/fields/Picker/Picker.docs.mdx index 68efb605e..7e43015e4 100644 --- a/src/components/fields/Picker/Picker.docs.mdx +++ b/src/components/fields/Picker/Picker.docs.mdx @@ -318,7 +318,7 @@ The trigger button supports various visual styles via the `type` prop: ### Trigger Button Themes -Control the color schema with the `theme` prop: +Control the color scheme with the `theme` prop: - **`default`** — Standard theme (default) - **`danger`** — Red/destructive color (also applied automatically when `isInvalid` is set) diff --git a/src/components/fields/Select/Select.stories.tsx b/src/components/fields/Select/Select.stories.tsx index eb1324a38..04a80bac1 100644 --- a/src/components/fields/Select/Select.stories.tsx +++ b/src/components/fields/Select/Select.stories.tsx @@ -426,7 +426,7 @@ const options = [ * `surface` is a story-only arg, not a `Select` prop: it paints the container * the select sits on. `addon-backgrounds` is not installed (see the note in * `.storybook/preview.jsx`, which disables it so the body keeps its - * schema-aware `#surface` fill), so `parameters.backgrounds` does nothing here + * scheme-aware `#surface` fill), so `parameters.backgrounds` does nothing here * — the two stories that need a tinted ground paint it themselves, which also * keeps them correct in dark mode. */ diff --git a/src/components/fields/Select/Select.tsx b/src/components/fields/Select/Select.tsx index 6059256dd..bae16f086 100644 --- a/src/components/fields/Select/Select.tsx +++ b/src/components/fields/Select/Select.tsx @@ -474,7 +474,7 @@ function Select( // Forwarded so a `special` trigger tells its action which SURFACE it // sits on: `CURRENT_ITEM_STYLES` only steps up to the stronger alpha // ramp on `theme=special`, and against that dark purple base the - // light-schema alphas are almost invisible. It travels through + // light-scheme alphas are almost invisible. It travels through // context rather than as a prop because the prop is what opts an // action out of `current` back to `clear`, and this one needs to stay // `current` so its label keeps inheriting. diff --git a/src/components/fields/color/color.ts b/src/components/fields/color/color.ts index e4fe25ccb..811f319f1 100644 --- a/src/components/fields/color/color.ts +++ b/src/components/fields/color/color.ts @@ -361,7 +361,7 @@ export function detectFormat(input: string): ColorFormat | null { * Black or white — whichever the WCAG contrast ratio favors on the given * color. Keeps the popover preview label readable at any lightness. Returns a * literal hex rather than a token, because the answer is measured against this - * exact fill and must not adapt with the color schema. + * exact fill and must not adapt with the color scheme. */ export function getContrastingColor(color: ColorValue): '#000000' | '#ffffff' { const luminance = relativeLuminanceFromLinearRgb( diff --git a/src/components/layout/Board/Board.tsx b/src/components/layout/Board/Board.tsx index 582ce6653..f075d0014 100644 --- a/src/components/layout/Board/Board.tsx +++ b/src/components/layout/Board/Board.tsx @@ -166,7 +166,7 @@ const A11yLayer = tasty({ // rectangle inset by the board's resolved padding and sized to the grid content // so the padding reads as a symmetric frame on every edge. Its size, position // and gradients come from the board's position params via inline `style`. The -// fill uses the `#border` token (via its CSS var) so it adapts to the schema. +// fill uses the `#border` token (via its CSS var) so it adapts to the scheme. const GridOverlayElement = tasty({ qa: 'BoardGridOverlay', styles: { diff --git a/src/components/layout/Board/WidgetHost.tsx b/src/components/layout/Board/WidgetHost.tsx index fd94fe9da..c75438f8e 100644 --- a/src/components/layout/Board/WidgetHost.tsx +++ b/src/components/layout/Board/WidgetHost.tsx @@ -71,7 +71,7 @@ const WidgetElement = tasty({ // ring dimmed, so releasing the pointer only changes the strength of an edge // that is already there — no geometry shift, nothing to re-read. The border // is *not* dimmed with it: `#primary-border` is close to `#border` on a dark - // schema, so fading it lands below the border a widget already has and the + // scheme, so fading it lands below the border a widget already has and the // preview would read as weaker than doing nothing. border: { '': false, @@ -85,7 +85,7 @@ const WidgetElement = tasty({ '0 0 0 1bw #border', 'pre-selected': '0 0 0 1bw #primary.40', selected: '0 0 0 1bw #primary', - // `$dialog-shadow` uses Glaze `#shadow-lg`, which adapts to dark / high-contrast schemas. + // `$dialog-shadow` uses Glaze `#shadow-lg`, which adapts to dark / high-contrast schemes. 'drag | resizing': '$dialog-shadow', }, outline: { diff --git a/src/components/organisms/StatsCard/StatsCard.docs.mdx b/src/components/organisms/StatsCard/StatsCard.docs.mdx index 97352fee4..6a9319294 100644 --- a/src/components/organisms/StatsCard/StatsCard.docs.mdx +++ b/src/components/organisms/StatsCard/StatsCard.docs.mdx @@ -26,7 +26,7 @@ Extends Card with: - `placeContent` — `space-between` - `gap` — `1x` - `border` — none -- `shadow` — `$shadow` (design-system elevation token, schema-aware via `#shadow-md`) +- `shadow` — `$shadow` (design-system elevation token, scheme-aware via `#shadow-md`) - `padding` — `2.5x` ### Base Properties diff --git a/src/components/other/CubeLogo/CubeLogo.docs.mdx b/src/components/other/CubeLogo/CubeLogo.docs.mdx index 9ea09d8b0..f3c9cef41 100644 --- a/src/components/other/CubeLogo/CubeLogo.docs.mdx +++ b/src/components/other/CubeLogo/CubeLogo.docs.mdx @@ -43,35 +43,35 @@ Every path is drawn with `currentColor`. Inheriting is the default and usually c ``` -There are **no baked-in brand hexes**, which is what lets the marks work on any surface and in any schema. +There are **no baked-in brand hexes**, which is what lets the marks work on any surface and in any scheme. -## Light and dark schemas +## Light and dark schemes The mark is two different drawings rather than one drawing recoloured — the dark variant is filled differently so it holds its weight against a dark surface. Both are rendered, and the `@dark` state swaps them in CSS. That means the swap costs no re-render, is correct during SSR and before hydration, and needs no prop in the normal case. It also means a snapshot of the DOM always contains both paths; assert on `[data-element="LightMark"]` / `[data-element="DarkMark"]` rather than on a single `path`. -### Pinning a schema +### Pinning a scheme -`@dark` is a _state_, and it is resolved against the **document**. Where the background is known but the document schema does not describe it, pass `schema` to pin one mark: +`@dark` is a _state_, and it is resolved against the **document**. Where the background is known but the document scheme does not describe it, pass `scheme` to pin one mark: ```jsx {/* A fixed-dark panel inside a light app */} - + {/* A themed region — `tokens` overrides token *values*, so it cannot reach a state */} - - + + ``` -Use it only when you know the surface. Omitting it is the right default: a pinned schema stops following the user's own light/dark preference. +Use it only when you know the surface. Omitting it is the right default: a pinned scheme stops following the user's own light/dark preference. ## Properties -- **`schema`** `'light' | 'dark'` — Pin one mark instead of letting the `@dark` state pick. Omit it unless you know the surface; see [Pinning a schema](#pinning-a-schema) +- **`scheme`** `'light' | 'dark'` — Pin one mark instead of letting the `@dark` state pick. Omit it unless you know the surface; see [Pinning a scheme](#pinning-a-scheme) - **`size`** — Maps onto `font-size`. Both axes on `CubeLogo`, height only on `CubeFullLogo` - **`color`** — Any color token. Both marks are drawn with `currentColor`, so this is inherited by default diff --git a/src/components/other/CubeLogo/CubeLogo.stories.tsx b/src/components/other/CubeLogo/CubeLogo.stories.tsx index 57a0eb121..0f72e0bf6 100644 --- a/src/components/other/CubeLogo/CubeLogo.stories.tsx +++ b/src/components/other/CubeLogo/CubeLogo.stories.tsx @@ -70,16 +70,16 @@ export const Colors: Story = { * `@dark` follows the *document*, so a panel whose background does **not** follow * it has to pin the mark itself. Both fills here are fixed (`#white` is a tasty * named color, `#surface-inverse` is `mode: 'fixed'`), so flipping the toolbar - * schema switch must leave both panels exactly as they are. + * scheme switch must leave both panels exactly as they are. */ -export const PinnedSchema: Story = { +export const PinnedScheme: Story = { render: () => ( - + - + ), diff --git a/src/components/other/CubeLogo/CubeLogo.tsx b/src/components/other/CubeLogo/CubeLogo.tsx index d9410726a..7d014a0c4 100644 --- a/src/components/other/CubeLogo/CubeLogo.tsx +++ b/src/components/other/CubeLogo/CubeLogo.tsx @@ -9,7 +9,7 @@ import { CubeIconProps, Icon } from '../../../icons/Icon'; * The mark is two *different drawings*, not one drawing recoloured: the light * variant is an outlined cube, the dark variant is filled differently so it keeps * its weight against a dark surface. Both are rendered and swapped by the global - * `@dark` state rather than picked in JS, so the logo follows the schema without a + * `@dark` state rather than picked in JS, so the logo follows the scheme without a * re-render and works in SSR. * * Both marks are drawn with `currentColor`, so colour comes from the surrounding @@ -56,7 +56,7 @@ const MarkPaths = () => ( ); -const SCHEMA_SWAP = { +const SCHEME_SWAP = { LightMark: { display: { '': 'block', '@dark': 'none' }, }, @@ -68,13 +68,13 @@ const SCHEMA_SWAP = { /** * Pin one mark instead of letting the `@dark` state choose. * - * Needed wherever the background is known but the *document* schema does not + * Needed wherever the background is known but the *document* scheme does not * describe it: a fixed-dark panel in a light app (`#surface-inverse`, the * `special` theme), an exported image, or a themed region — `tokens` overrides * token *values*, and `@dark` is a state, so a region preview cannot reach the * swap on its own. */ -const FORCED_SCHEMA = { +const FORCED_SCHEME = { light: { LightMark: { display: 'block' }, DarkMark: { display: 'none' }, @@ -88,14 +88,14 @@ const FORCED_SCHEMA = { export interface CubeLogoProps extends CubeIconProps { /** * Force the light or dark mark. Omit it — the default — to follow the document - * schema in CSS, which costs no re-render and is correct during SSR. + * scheme in CSS, which costs no re-render and is correct during SSR. */ - schema?: 'light' | 'dark'; + scheme?: 'light' | 'dark'; } -/** Caller styles win; a forced schema only overrides the swap itself. */ -function resolveLogoStyles({ schema, styles }: CubeLogoProps) { - return schema ? mergeStyles(FORCED_SCHEMA[schema], styles) : styles; +/** Caller styles win; a forced scheme only overrides the swap itself. */ +function resolveLogoStyles({ scheme, styles }: CubeLogoProps) { + return scheme ? mergeStyles(FORCED_SCHEME[scheme], styles) : styles; } /** @@ -104,14 +104,14 @@ function resolveLogoStyles({ schema, styles }: CubeLogoProps) { */ const CubeLogoElement = tasty(Icon, { qa: 'CubeLogo', - styles: SCHEMA_SWAP, + styles: SCHEME_SWAP, }); export const CubeLogo = forwardRef(function CubeLogo( props: CubeLogoProps, ref: ForwardedRef, ) { - const { schema, styles, ...rest } = props; + const { scheme, styles, ...rest } = props; return ( , ) { - const { schema, styles, ...rest } = props; + const { scheme, styles, ...rest } = props; return ( ', () => { - it('renders both schema marks so the swap is CSS-only', () => { + it('renders both scheme marks so the swap is CSS-only', () => { renderWithRoot(); const el = screen.getByTestId('CubeLogo'); @@ -20,8 +20,8 @@ describe('', () => { it.each([ ['light', 'block', 'none'], ['dark', 'none', 'block'], - ] as const)('pins the %s mark when asked', (schema, light, dark) => { - renderWithRoot(); + ] as const)('pins the %s mark when asked', (scheme, light, dark) => { + renderWithRoot(); const el = screen.getByTestId('CubeLogo'); const mark = (name: string) => getComputedStyle(el.querySelector(`[data-element="${name}"]`)!).display; @@ -30,10 +30,10 @@ describe('', () => { expect(mark('DarkMark')).toBe(dark); }); - it('keeps schema off the DOM', () => { - renderWithRoot(); + it('keeps scheme off the DOM', () => { + renderWithRoot(); - expect(screen.getByTestId('CubeLogo')).not.toHaveAttribute('schema'); + expect(screen.getByTestId('CubeLogo')).not.toHaveAttribute('scheme'); }); it('drives sizing from the size prop', () => { diff --git a/src/components/other/NoDataIcon/NoDataIcon.stories.tsx b/src/components/other/NoDataIcon/NoDataIcon.stories.tsx index 3edfa6242..2ba900ae7 100644 --- a/src/components/other/NoDataIcon/NoDataIcon.stories.tsx +++ b/src/components/other/NoDataIcon/NoDataIcon.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryObj } from '@storybook/react-vite'; import { - withDarkSchema, + withDarkScheme, withHighContrast, -} from '../../../stories/decorators/withColorSchema'; +} from '../../../stories/decorators/withColorScheme'; import { Text } from '../../content/Text'; import { Flow } from '../../layout/Flow'; import { Space } from '../../layout/Space'; @@ -64,11 +64,11 @@ export const WithLoadingAnimation: Story = { /** * The three faces are pinned by a contrast floor against `#surface`, so they - * hold the same separation from the page in every schema rather than flattening + * hold the same separation from the page in every scheme rather than flattening * out in dark — this story and the next are what that claim is checked against. */ -export const DarkSchema: Story = { - decorators: [withDarkSchema], +export const DarkScheme: Story = { + decorators: [withDarkScheme], }; export const HighContrast: Story = { diff --git a/src/components/overlays/Tooltip/Tooltip.docs.mdx b/src/components/overlays/Tooltip/Tooltip.docs.mdx index 095ed5fad..220bedf02 100644 --- a/src/components/overlays/Tooltip/Tooltip.docs.mdx +++ b/src/components/overlays/Tooltip/Tooltip.docs.mdx @@ -49,7 +49,7 @@ Display container for tooltip content. Has a directional arrow dependent on its - **`showIcon`** `boolean` — Controls whether an icon should be displayed in the tooltip - **`placement`** `PlacementAxis` (default: `'top'`) — Position of the tooltip relative to its trigger. Options: `top`, `bottom`, `left`, `right` - **`isMaterial`** `boolean` — Enables material-style tooltip with auto pointer events -- **`isLight`** `boolean` — Uses light color schema for the tooltip +- **`isLight`** `boolean` — Uses light color scheme for the tooltip - **`isOpen`** `boolean` — Controlled open state of the tooltip - **`defaultOpen`** `boolean` — Whether the tooltip is open by default (uncontrolled) - **`onOpenChange`** `(isOpen: boolean) => void` — Callback when the tooltip open state changes @@ -85,7 +85,7 @@ These properties allow direct style application without using the `styles` prop: The `mods` property on the tooltip accepts the following modifiers you can override: - **`material`** `boolean` — Enables material-style tooltip with pointer events -- **`light`** `boolean` — Uses light color schema instead of dark +- **`light`** `boolean` — Uses light color scheme instead of dark - **`open`** `boolean` — Indicates whether the tooltip is currently visible ## Usage Patterns @@ -157,7 +157,7 @@ Control where the tooltip appears relative to its trigger: The tooltip is capped at `36x` wide (or the viewport minus `4x`, whichever is smaller) and wraps its content to stay inside that cap. Runs with no spaces — URLs, tokens, identifiers, error strings — are broken mid-word rather than allowed to overflow the bubble. ```jsx - + ``` diff --git a/src/components/overlays/Tooltip/Tooltip.stories.tsx b/src/components/overlays/Tooltip/Tooltip.stories.tsx index fb001dd17..6207001d5 100644 --- a/src/components/overlays/Tooltip/Tooltip.stories.tsx +++ b/src/components/overlays/Tooltip/Tooltip.stories.tsx @@ -109,7 +109,7 @@ const UnbreakableContentTemplate: Story = (args) => ( - https://cubecloud.example.com/deployments/12345/schema/files/model/cubes/very_long_cube_name.yml + https://cubecloud.example.com/deployments/12345/scheme/files/model/cubes/very_long_cube_name.yml ); diff --git a/src/components/overlays/Tooltip/Tooltip.tsx b/src/components/overlays/Tooltip/Tooltip.tsx index 67878da53..aba786e5b 100644 --- a/src/components/overlays/Tooltip/Tooltip.tsx +++ b/src/components/overlays/Tooltip/Tooltip.tsx @@ -28,18 +28,18 @@ export type { AriaTooltipProps }; const TooltipElement = tasty({ styles: { display: 'block', - // The DEFAULT (dark) tooltip is intentionally schema-invariant: + // The DEFAULT (dark) tooltip is intentionally scheme-invariant: // `#surface-inverse.85` + `#white` keep it the "always dark" tooltip in - // light, dark, and high-contrast schemas — matching the legacy `#dark.85` + // light, dark, and high-contrast schemes — matching the legacy `#dark.85` // + `#white` design. // // The `light` variant uses the adaptive surface tokens (`#surface` + - // `#surface-text-soft`) so it follows the page schema. This restores the + // `#surface-text-soft`) so it follows the page scheme. This restores the // softer legacy `#dark-02` look in light mode (cr≈9.2) and stays AA-safe // in dark mode (the previous fixed `#surface-inverse` was much darker // than the legacy `#dark-02`, and pairing the fixed `#white` fill with // the adaptive `#surface-text-soft` text — the LEGACY_ALIASES port of - // `#dark-02` — would collapse to cr≈1.8 in dark schemas; using `#surface` + // `#dark-02` — would collapse to cr≈1.8 in dark schemes; using `#surface` // for the fill keeps both ends of the pair adapting together). fill: { '': '#surface-inverse.85', diff --git a/src/components/overlays/Tooltip/TooltipProvider.docs.mdx b/src/components/overlays/Tooltip/TooltipProvider.docs.mdx index 3ae79361c..00be860de 100644 --- a/src/components/overlays/Tooltip/TooltipProvider.docs.mdx +++ b/src/components/overlays/Tooltip/TooltipProvider.docs.mdx @@ -23,7 +23,7 @@ A convenience wrapper that combines `TooltipTrigger` and `Tooltip` into a single - **`delay`** `number` (default: `250`) — The delay time in ms for the tooltip to show up - **`closeDelay`** `number` (default: `500`) — The delay time in ms for the tooltip to close - **`isMaterial`** `boolean` — Enables material-style tooltip with auto pointer events -- **`isLight`** `boolean` — Uses light color schema for the tooltip +- **`isLight`** `boolean` — Uses light color scheme for the tooltip - **`isOpen`** `boolean` — Controlled open state of the tooltip - **`defaultOpen`** `boolean` — Whether the tooltip is open by default (uncontrolled) - **`onOpenChange`** `(isOpen: boolean) => void` — Callback when the tooltip open state changes diff --git a/src/components/status/LoadingAnimation/LoadingAnimation.stories.tsx b/src/components/status/LoadingAnimation/LoadingAnimation.stories.tsx index d11957ad8..0a1bb5738 100644 --- a/src/components/status/LoadingAnimation/LoadingAnimation.stories.tsx +++ b/src/components/status/LoadingAnimation/LoadingAnimation.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryFn } from '@storybook/react-vite'; import { - withDarkSchema, + withDarkScheme, withHighContrast, -} from '../../../stories/decorators/withColorSchema'; +} from '../../../stories/decorators/withColorScheme'; import { baseProps } from '../../../stories/lists/baseProps'; import { @@ -32,9 +32,9 @@ Large.args = { size: 'large', }; -export const DarkSchema = Template.bind({}); -DarkSchema.args = {}; -DarkSchema.decorators = [withDarkSchema]; +export const DarkScheme = Template.bind({}); +DarkScheme.args = {}; +DarkScheme.decorators = [withDarkScheme]; export const HighContrast = Template.bind({}); HighContrast.args = {}; diff --git a/src/data/item-themes.test.ts b/src/data/item-themes.test.ts index 5de706028..35851e9c7 100644 --- a/src/data/item-themes.test.ts +++ b/src/data/item-themes.test.ts @@ -48,7 +48,7 @@ describe('ITEM_VARIANTS', () => { * authored number goes up. See `CURRENT_OUTLINE_STYLES`. * * And `current.clear` steps its enabled states through the custom properties - * of `CURRENT_ITEM_RAMP` — one per schema and surface — while its disabled + * of `CURRENT_ITEM_RAMP` — one per scheme and surface — while its disabled * chip is a plain alpha, so the two are not comparable as strings at all. */ const CURRENT = [ diff --git a/src/data/item-themes.ts b/src/data/item-themes.ts index 0d5b527be..9d49bdbaf 100644 --- a/src/data/item-themes.ts +++ b/src/data/item-themes.ts @@ -81,8 +81,8 @@ export const DEFAULT_PRIMARY_STYLES: Styles = { // The brand ramp `accent-surface` → `-2` → `-3` gives a monotonically // increasing contrast against `#surface` (cr ≈ 4.5 → 4.8 → 5.2 in light, // similar in dark), so hover and pressed read visibly *darker* than the - // default state in both schemas. Disabled uses the brand-tinted, - // schema-symmetric chip (`accent-disabled-surface` cr ≈ 1.4 vs surface) + // default state in both schemes. Disabled uses the brand-tinted, + // scheme-symmetric chip (`accent-disabled-surface` cr ≈ 1.4 vs surface) // so the muted state stays identifiable as a brand color. fill: { '': '#surface #primary-accent-surface', @@ -786,10 +786,10 @@ export const NOTE_ITEM_STYLES: Styles = { // Every color here resolves to a fixed-mode value (built-in `#white`, the // standalone `#special-*` theme in `src/tokens/palette.ts`, or `transparent`). // `mode: 'fixed'` makes the resolved OKHSL identical in light, dark, and -// high-contrast, so the special theme renders the same regardless of schema. +// high-contrast, so the special theme renders the same regardless of scheme. // The only intentionally adaptive colors are `VALIDATION_STYLES.border` // (`#danger-accent-text` / `#success-accent-text`) — validation state is allowed to follow -// the active schema. +// the active scheme. export const SPECIAL_PRIMARY_STYLES: Styles = { // Focus ring uses `#special-accent-text` — a fixed-mode dark-purple that // stays identical across light/dark/HC, matching the special theme's @@ -838,7 +838,7 @@ export const SPECIAL_OUTLINE_STYLES: Styles = { // alpha distinct sidesteps the collision. // // Focus ring uses the fixed-mode `#special-accent-text` so the indicator - // stays schema-invariant alongside the rest of the special theme — see + // stays scheme-invariant alongside the rest of the special theme — see // `SPECIAL_PRIMARY_STYLES.outline` for the full rationale. outline: { '': '0 #special-accent-text.0', @@ -916,7 +916,7 @@ export const SPECIAL_CLEAR_STYLES: Styles = { // the white-alpha variants are solved for. // // Focus ring uses the fixed-mode `#special-accent-text` so the indicator - // stays schema-invariant alongside the rest of the special theme — see + // stays scheme-invariant alongside the rest of the special theme — see // `SPECIAL_PRIMARY_STYLES.outline` for the full rationale. outline: { '': '0 #special-accent-text.0', @@ -1020,8 +1020,8 @@ export const SPECIAL_ITEM_STYLES: Styles = { // written inline in `fill`. Two reasons: // // 1. Unlike the brand tokens, `#current` alphas do NOT adapt to the color -// schema, so one ramp cannot serve both. Each step therefore carries the base -// entry for the light schema and an `@dark` counterpart. +// scheme, so one ramp cannot serve both. Each step therefore carries the base +// entry for the light scheme and an `@dark` counterpart. // 2. Writing both ramps straight into one `fill` map would put twelve alpha // values in a single state-map, and Tasty's `mergeEntriesByValue` pass // coalesces any two equal value strings into one OR-entry at the group's max @@ -1030,7 +1030,7 @@ export const SPECIAL_ITEM_STYLES: Styles = { // the constraint that `SPECIAL_OUTLINE_STYLES` documents the hard way. // // THE DARK STEPS ARE DERIVED, NOT AUTHORED. The same alpha is not the same step -// in both schemas, and the direction is the opposite of what it looks like: near +// in both schemes, and the direction is the opposite of what it looks like: near // the dark end of the scale a small sRGB move is a large perceptual one, so a // light tint on a dark surface reads STRONGER than the same tint of a dark label // on a light page. Each `@dark` value is therefore solved so its OKHST *tone* @@ -1050,11 +1050,11 @@ export const SPECIAL_ITEM_STYLES: Styles = { // because tasty computes the mix percentage as `parseFloat(alpha) * 100`, and // `.132` lands on `13.200000000000001%` in the emitted CSS. // -// Measured against `#surface` / `#surface-text` in each schema, which is the only +// Measured against `#surface` / `#surface-text` in each scheme, which is the only // tractable calibration: `current` paints from an arbitrary inherited color over // an arbitrary container, so a single ramp cannot be exact for all of them. The // neutral page pair is the common case, and matching it is what keeps the two -// schemas recognisably the same ramp. Re-derive with Glaze's `oklabToOkhsl` + +// schemes recognisably the same ramp. Re-derive with Glaze's `oklabToOkhsl` + // `okhslToOkhst` if the neutral tokens move. // // Note that the label keeps its own margin throughout: the weakest dark step @@ -1289,7 +1289,7 @@ export const CURRENT_OUTLINE_2_STYLES: Styles = { // The states are the second fill layer. There is no lighter or darker sibling // of an arbitrary inherited color to step to — the brand ramps walk // `accent-surface` → `-2` → `-3` — so hover and pressed lay a translucent -// `#black` over the same base instead, which darkens in both schemas and so +// `#black` over the same base instead, which darkens in both schemes and so // keeps the same monotonic direction the brand primaries have. // // The label CANNOT go through `color`. `#current` compiles to the literal @@ -1305,9 +1305,9 @@ export const CURRENT_PRIMARY_STYLES: Styles = { ...CURRENT_FOCUS_RING, // Every other `primary` rims its fill with a lighter sibling // (`accent-surface-border` over `accent-surface`, cr 1.48 against it in both - // schemas). An arbitrary inherited color has no such sibling, so the rim comes + // schemes). An arbitrary inherited color has no such sibling, so the rim comes // from the same token the label does — the one color guaranteed to sit on the - // opposite side of the fill in either schema, and the one a container can + // opposite side of the fill in either scheme, and the one a container can // redirect, so the rim cannot come apart from the label it edges. `.25` // measures cr 1.82 in light and 1.55 in dark against the fill: the brand rim's // presence, a shade more so in light, where `current` has no other edge cue. diff --git a/src/data/themes.ts b/src/data/themes.ts index db27bf1bd..e83247804 100644 --- a/src/data/themes.ts +++ b/src/data/themes.ts @@ -13,7 +13,7 @@ // top. We use `#-accent-surface` (fixed mode) which is // anchored to the fixed-white `#-accent-surface-text` // with `contrast: [4.5, 7]`, so white-text-on-fill is -// guaranteed WCAG AA (4.5) / AAA (7) in every schema — the +// guaranteed WCAG AA (4.5) / AAA (7) in every scheme — the // same brand-pill design `special` uses. (Pre-glaze this slot // resolved to `#-accent-text`, which is anchored to // surface with mode 'auto' for "readable text on surface" — diff --git a/src/stories/AdvancedStates.stories.tsx b/src/stories/AdvancedStates.stories.tsx index a077b16eb..d7c802807 100644 --- a/src/stories/AdvancedStates.stories.tsx +++ b/src/stories/AdvancedStates.stories.tsx @@ -223,10 +223,10 @@ Local definitions take precedence over global ones. }; // ============================================================================= -// Color Schema (Light/Dark Mode) +// Color Scheme (Light/Dark Mode) // ============================================================================= -const ColorSchemaBox = tasty({ +const ColorSchemeBox = tasty({ styles: { padding: '3x', radius: '2r', @@ -245,19 +245,19 @@ const ColorSchemaBox = tasty({ }, }); -export const ColorSchema: StoryObj = { +export const ColorScheme: StoryObj = { render: () => ( - Adapts to system color schema (prefers-color-scheme) + Adapts to system color scheme (prefers-color-scheme) - + Light mode: white background, dark text
Dark mode: dark background, white text
-
+ Change your system appearance settings to see the effect @@ -270,7 +270,7 @@ export const ColorSchema: StoryObj = { Use \`@media(prefers-color-scheme: dark)\` to style based on system color preference: \`\`\`tsx -const ColorSchemaBox = tasty({ +const ColorSchemeBox = tasty({ styles: { fill: { '': '#white', diff --git a/src/stories/Colors.docs.mdx b/src/stories/Colors.docs.mdx index 202752320..fad6bd068 100644 --- a/src/stories/Colors.docs.mdx +++ b/src/stories/Colors.docs.mdx @@ -6,7 +6,7 @@ import * as ColorsStories from './Colors.stories'; # Colors -Cube UI Kit color tokens are generated by Glaze from a shared OKHST palette. Every adaptive token includes light, dark, and high-contrast variants. Change the active schema in the Storybook toolbar to review each example. +Cube UI Kit color tokens are generated by Glaze from a shared OKHST palette. Every adaptive token includes light, dark, and high-contrast variants. Change the active scheme in the Storybook toolbar to review each example. The colors shown here come from the default seeds. Those seeds — brand hue, saturation, per-status hues — are tunable at runtime; see [Theming](/docs/getting-started-theming--docs) for a live playground. @@ -70,7 +70,7 @@ Syntax tokens are calibrated as a group against `#surface`. Use the semantic tok ## Borders and shadows -Borders use the `1bw` design-system width. Shadow geometry comes from `src/tokens/shadows.ts`; the `#shadow-sm`, `#shadow-md`, and `#shadow-lg` colors adapt with the active schema. +Borders use the `1bw` design-system width. Shadow geometry comes from `src/tokens/shadows.ts`; the `#shadow-sm`, `#shadow-md`, and `#shadow-lg` colors adapt with the active scheme. diff --git a/src/stories/Colors.stories.tsx b/src/stories/Colors.stories.tsx index 22f9b2c94..c3a578780 100644 --- a/src/stories/Colors.stories.tsx +++ b/src/stories/Colors.stories.tsx @@ -650,7 +650,7 @@ export const CodeSyntax: Story = { render: () => ( @@ -684,7 +684,7 @@ export const BordersAndShadows: Story = { render: () => (
Borders diff --git a/src/stories/Introduction.docs.mdx b/src/stories/Introduction.docs.mdx index 0395c7bb7..5f6b5d761 100644 --- a/src/stories/Introduction.docs.mdx +++ b/src/stories/Introduction.docs.mdx @@ -21,7 +21,7 @@ Cube UI Kit is built on two independent open-source projects: - [Tasty](https://tasty.style) is the declarative styling engine. It turns token-aware style objects, responsive values, and component states into conflict-free CSS. - [Glaze](https://github.com/tenphi/glaze) generates the semantic color palette. A small set of brand and status seeds resolves into light, dark, and high-contrast token sets. -Use the [Theme Builder](/story/getting-started-theming--theme-builder) to tune a palette and preview the result across schemas and contrast levels. +Use the [Theme Builder](/story/getting-started-theming--theme-builder) to tune a palette and preview the result across schemes and contrast levels. ## Get started diff --git a/src/stories/Theming.docs.mdx b/src/stories/Theming.docs.mdx index 53c3913e9..bce339122 100644 --- a/src/stories/Theming.docs.mdx +++ b/src/stories/Theming.docs.mdx @@ -6,7 +6,7 @@ import * as ThemingStories from './Theming.stories'; # Theming -The whole palette is generated by [Glaze](https://github.com/tenphi/glaze) from a handful of seeds, one per **zone**: the accent, the base, and each of the four status themes. Those seeds are tunable at runtime — change one and every token re-resolves, in light, dark, and high-contrast schemas at once. +The whole palette is generated by [Glaze](https://github.com/tenphi/glaze) from a handful of seeds, one per **zone**: the accent, the base, and each of the four status themes. Those seeds are tunable at runtime — change one and every token re-resolves, in light, dark, and high-contrast schemes at once. Every zone takes the same seed, and it comes in two forms: a **color**, which is usually what you actually have, or the **numbers** behind one. Never both — a zone is seeded one way or the other. See [One seed, two ways to spell it](#one-seed-two-ways-to-spell-it). @@ -139,7 +139,7 @@ First, **exactness is scoped to the light, normal-contrast variant.** That is th Sizing those two the same is a mistake worth naming, because this palette made it. In light `surface` **is** white, so one measurement is both constraints at once and Lc 45 looks right for the pair. In dark the page is near-black and the same number becomes a demand that a filled shape reach text contrast against it — which nothing in the palette meets, the shipped fill included. Since a floor can only lighten, the surplus flattened the dark half of the tone axis onto a single value: every brand darker than the floor came out the same colour. The page floor is now sized for a shape and the label is guaranteed separately, by a cap on the seed's tone. -They are APCA rather than WCAG on purpose. A single WCAG ratio means two different things depending on the schema: measured across twelve hues, a fill sitting exactly at 3:1 comes out at Lc 56 in light but only Lc 23 in dark. That is why light brands kept getting crushed while dark ones sailed through the same rule. **A consequence to state plainly: the emitted fill can sit below WCAG 3:1.** `#0EA5E9` renders at 2.77:1 against a white page and is correct there — the Lc is the guarantee, not the ratio. +They are APCA rather than WCAG on purpose. A single WCAG ratio means two different things depending on the scheme: measured across twelve hues, a fill sitting exactly at 3:1 comes out at Lc 56 in light but only Lc 23 in dark. That is why light brands kept getting crushed while dark ones sailed through the same rule. **A consequence to state plainly: the emitted fill can sit below WCAG 3:1.** `#0EA5E9` renders at 2.77:1 against a white page and is correct there — the Lc is the guarantee, not the ratio. High contrast does **not** escalate the page floor. It is a request for separation over brand, but not for separation from the page: the same fill carries the label, and driving it off the page drives the label off it. The shipped ladder makes the same trade and lands the other side of it, ending up with _less_ page separation in high contrast than in normal, because it darkens the fill toward its label. What does escalate is the label cap, which searches the high-contrast variants too. @@ -179,7 +179,7 @@ setPaletteConfig({ surfaceMode: 'tinted', base: { saturation: 25 } }); Two tones is not a lightness change you would name — it is _room_. Everything below `surface` is positioned relative to it, so the ladder, the borders and the text ramp all follow, and the text's `['AA','AAA']` floors re-solve against the new background rather than drifting. The `code-*` family follows too: its mirrored `surface` exists to be the page, so it tracks both the tone and the base seed. -**The tinted surfaces move with it.** A status theme's `surface` — and a runtime tint's, from `getColorTheme()` — is authored as an offset from the page's rather than as an absolute tone, and that offset is exactly the two tones `tinted` shifts by. Anchored absolutely they would land on the page's own new tone and a `note` banner would stop reading as a banner at all; anchored to the page they keep the separation the offset was chosen for, in both schemas. They also pick up a little more chroma there, being further from the extreme — so `tinted` makes a status surface easier to see, not harder. +**The tinted surfaces move with it.** A status theme's `surface` — and a runtime tint's, from `getColorTheme()` — is authored as an offset from the page's rather than as an absolute tone, and that offset is exactly the two tones `tinted` shifts by. Anchored absolutely they would land on the page's own new tone and a `note` banner would stop reading as a banner at all; anchored to the page they keep the separation the offset was chosen for, in both schemes. They also pick up a little more chroma there, being further from the extreme — so `tinted` makes a status surface easier to see, not harder. The tint stays subtle by construction. Near white the sRGB gamut has very little chroma to give, so `tinted` buys a surface that reads as warm or cool rather than one that reads as colored — which is the whole ask. For a genuinely colored panel, reach for a status theme's `surface` instead. @@ -343,7 +343,7 @@ The preview deliberately shows all three surface levels with their own text ramp ## Previewing a theme in a region -`setPaletteConfig()` re-themes the whole document, which is no use for a theme picker — you want several themes visible at once, or a dark preview inside a light page. `renderColorTokens()` does that: it resolves the palette for **one** config and **one** schema and returns flat literal values, ready to apply to a subtree through a tasty `tokens` prop. +`setPaletteConfig()` re-themes the whole document, which is no use for a theme picker — you want several themes visible at once, or a dark preview inside a light page. `renderColorTokens()` does that: it resolves the palette for **one** config and **one** scheme and returns flat literal values, ready to apply to a subtree through a tasty `tokens` prop. ```tsx import { renderColorTokens, tasty } from '@cube-dev/ui-kit'; @@ -351,20 +351,20 @@ import { renderColorTokens, tasty } from '@cube-dev/ui-kit'; const Region = tasty({ styles: { fill: '#surface', color: '#surface-text' } }); const preview = useMemo( - () => renderColorTokens({ accent: { hue: 210 }, schema: 'dark' }), + () => renderColorTokens({ accent: { hue: 210 }, scheme: 'dark' }), [], ); …everything in here renders in that theme…; ``` -The reason this needs its own API: the document palette emits **state maps** (`{ '': …, '@dark': …, '@hc': … }`), so a page can only ever show one schema at a time — that is what `@dark` means. Collapsing the palette to a chosen schema removes the conditionality, so several themes can coexist. +The reason this needs its own API: the document palette emits **state maps** (`{ '': …, '@dark': …, '@hc': … }`), so a page can only ever show one scheme at a time — that is what `@dark` means. Collapsing the palette to a chosen scheme removes the conditionality, so several themes can coexist. | Option | Meaning | | --- | --- | -| every `PaletteConfig` field | merged over the **current** config, so `{ schema: 'dark' }` previews the active theme in dark | -| `schema` | `'light'` (default) or `'dark'` | -| `highContrast` | resolve that schema's high-contrast variant. Default `false` | +| every `PaletteConfig` field | merged over the **current** config, so `{ scheme: 'dark' }` previews the active theme in dark | +| `scheme` | `'light'` (default) or `'dark'` | +| `highContrast` | resolve that scheme's high-contrast variant. Default `false` | Nothing is applied globally — the live palette and the stored config are untouched, so previews are safe to render anywhere. @@ -372,10 +372,10 @@ Note the one place the merge/replace split matters: this call **layers** over th Notes: -- Real components work inside a region. Every color in the kit compiles to a CSS custom property, so overriding those properties on one element re-colors its whole subtree with no schema attribute and no second ``. +- Real components work inside a region. Every color in the kit compiles to a CSS custom property, so overriding those properties on one element re-colors its whole subtree with no scheme attribute and no second ``. - The legacy aliases come along **by reference** (`'#dark': '#surface-text'`), and so do the shadow tokens and scrollbar colors, whose values embed a palette color. Tasty re-declares them on the region so each `var()` resolves against the region's own values; resolving them up front would freeze them to the outer theme instead. - Typography, spacing, sizes and layout are **not** included — nothing in their values references a color, so the region inherits them from ``. -- `contrastLevel` works per region, so the whole 0–100 ramp can be shown at once — something a document cannot do, since it is only ever at one level. Level `0` reproduces the normal tier and `100` the high-contrast tier exactly, in both schemas. `highContrast: true` still returns the genuine high-contrast resolution at any level below `100`, because the level moves the baseline and the tier escalates from where `'auto'` would put it; at `100` the two coincide and it returns the same colors as the normal variant. Note that tokens carrying a contrast _floor_ (most text) can sit still over part of the ramp — the floor is solved, not approximated, so the value only moves once the interpolated target does. Tokens positioned by a plain tone pair, like `#border`, step at every level. +- `contrastLevel` works per region, so the whole 0–100 ramp can be shown at once — something a document cannot do, since it is only ever at one level. Level `0` reproduces the normal tier and `100` the high-contrast tier exactly, in both schemes. `highContrast: true` still returns the genuine high-contrast resolution at any level below `100`, because the level moves the baseline and the tier escalates from where `'auto'` would put it; at `100` the two coincide and it returns the same colors as the normal variant. Note that tokens carrying a contrast _floor_ (most text) can sit still over part of the ramp — the floor is solved, not approximated, so the value only moves once the interpolated target does. Tokens positioned by a plain tone pair, like `#border`, step at every level. - Resolving a palette costs a few milliseconds. The last config is memoized, but `useMemo` on the caller side if you drive it from state. `renderPaletteTokens()` is the same thing without the aliases and companion tokens, if you want only the Glaze palette. @@ -409,22 +409,22 @@ subscribePaletteConfig(() => { `getPaletteConfig()` always returns the fully resolved form, so `getPaletteConfig().themes.danger.saturation` is a number even if nobody ever set it. `DEFAULT_PALETTE_CONFIG` is the shipped baseline in the same shape. -## Reading the ambient schema +## Reading the ambient scheme -The palette config is what the app _asked for_; the schema and contrast tier are what the viewer is _seeing_ — `` / `prefers-color-scheme`, and `` / `prefers-contrast`. They are not part of the config, so `getPaletteConfig()` says nothing about them: +The palette config is what the app _asked for_; the scheme and contrast tier are what the viewer is _seeing_ — `` / `prefers-color-scheme`, and `` / `prefers-contrast`. They are not part of the config, so `getPaletteConfig()` says nothing about them: ```tsx -import { useHighContrast, useSchema } from '@cube-dev/ui-kit'; +import { useHighContrast, useScheme } from '@cube-dev/ui-kit'; -const schema = useSchema(); // 'light' | 'dark' +const scheme = useScheme(); // 'light' | 'dark' const isHighContrast = useHighContrast(); // boolean ``` -Both resolve exactly as the `@dark` / `@hc` states do — attribute opt-in first, media query only when the attribute is absent — and re-render when either input changes. Outside React: `resolveSchema()`, `resolveHighContrast()` and `subscribeSchema(listener)`, which fires on a change to either condition. +Both resolve exactly as the `@dark` / `@hc` states do — attribute opt-in first, media query only when the attribute is absent — and re-render when either input changes. Outside React: `resolveScheme()`, `resolveHighContrast()` and `subscribeScheme(listener)`, which fires on a change to either condition. -Reach for these only where CSS cannot go. Styling belongs in a state map (`{ '': …, '@dark': …, '@hc': … }`), which repaints on a schema flip with no re-render at all. What the states cannot reach is a surface that takes values rather than CSS — a Vega spec, a CodeMirror or Monaco theme, an iframe — or a control whose value _is_ the condition, like the Light/Dark and Normal/High-contrast selectors over the [theme builder](#theme-builder). +Reach for these only where CSS cannot go. Styling belongs in a state map (`{ '': …, '@dark': …, '@hc': … }`), which repaints on a scheme flip with no re-render at all. What the states cannot reach is a surface that takes values rather than CSS — a Vega spec, a CodeMirror or Monaco theme, an iframe — or a control whose value _is_ the condition, like the Light/Dark and Normal/High-contrast selectors over the [theme builder](#theme-builder). -Note the distinction from `contrastLevel`: the level is a config seed the app supplies and can read back, while high contrast is a viewing condition the document is in. They also pair up — `renderColorTokens({ schema, highContrast })` takes both, so a region can be previewed in the conditions the page is actually showing. +Note the distinction from `contrastLevel`: the level is a config seed the app supplies and can read back, while high contrast is a viewing condition the document is in. They also pair up — `renderColorTokens({ scheme, highContrast })` takes both, so a region can be previewed in the conditions the page is actually showing. ## `` @@ -451,7 +451,7 @@ This is a convenience wrapper over `setPaletteConfig()`, not a separate scope. * It also owns the accent's `saturation` outright — pinned to `100`, so a number passed _explicitly alongside_ it is inert and warned about (a number passed on its own turns pastel off instead). And it caps a color seed short of itself: the ceiling sits below where a saturated color lands, so `#EF4444` resolves to `#c47069`. Both are visible in the [Theme builder](#theme-builder), the first as a disabled slider and the second as the requested/resolved chips. -**Re-seeding costs about 10 ms.** Rebuilding the eight themes and re-solving ~156 tokens across four schema variants is not free, though it is inside a frame. A manual `contrastLevel` costs the same — it runs the same four passes — except at level `100`, which skips the two high-contrast ones because the normal pass already produced those values. Setting the config to a value it already holds is free — it does not even bump the version. If you are driving it from a slider, let the input coalesce to frames (a pointer-driven `onChange` already does) rather than writing on every event. +**Re-seeding costs about 10 ms.** Rebuilding the eight themes and re-solving ~156 tokens across four scheme variants is not free, though it is inside a frame. A manual `contrastLevel` costs the same — it runs the same four passes — except at level `100`, which skips the two high-contrast ones because the normal pass already produced those values. Setting the config to a value it already holds is free — it does not even bump the version. If you are driving it from a slider, let the input coalesce to frames (a pointer-driven `onChange` already does) rather than writing on every event. **If you call Glaze directly, tell the kit.** `glaze.configure({ darkTone })` and friends invalidate Glaze's own caches, but the kit's token maps are memoized against the palette config version: diff --git a/src/stories/Theming.stories.tsx b/src/stories/Theming.stories.tsx index 9f1e3944b..18bc4df68 100644 --- a/src/stories/Theming.stories.tsx +++ b/src/stories/Theming.stories.tsx @@ -34,14 +34,14 @@ import { useHighContrast, usePaletteConfig, usePaletteVersion, - useSchema, + useScheme, } from '../index'; import type { Meta, StoryObj } from '@storybook/react-vite'; import type { Styles, Tokens } from '@tenphi/tasty'; import type { ReactNode } from 'react'; import type { - ColorSchema, + ColorScheme, PaletteConfig, PaletteNumericSeed, PaletteSeed, @@ -185,7 +185,7 @@ function useResetOnUnmount() { useEffect(() => resetPaletteConfig, []); } -/** Light-schema value of a token, straight out of the resolved palette. */ +/** Light-scheme value of a token, straight out of the resolved palette. */ function resolvedValue(name: string): string { const token = getPaletteTokens()[name] as Record | undefined; @@ -300,7 +300,7 @@ function ColorResolution({ resolved }: { resolved?: Tokens }) { // The preview's own tokens when there are any, so the chip answers "what did I get in // the variant I am looking at". `resolvedValue` only ever reports the document's - // light schema, which is the wrong answer inside a dark preview. + // light scheme, which is the wrong answer inside a dark preview. const valueOf = (name: string) => (resolved?.[name] as string | undefined) ?? resolvedValue(name); @@ -1179,7 +1179,7 @@ function CodePanel() { with numbers, and a muted palette would wash the whole block out. Tune it on its own with themes.code.saturation. Every token still keeps an AA/AAA floor against the real surface, so it stays - readable in every schema. + readable in every scheme. @@ -1984,10 +1984,10 @@ const PREVIEW_NAV = [ function ThemePreview({ tokens, - schema, + scheme, }: { tokens: Tokens; - schema: ColorSchema; + scheme: ColorScheme; }) { const [tab, setTab] = useState(PREVIEW_TABS[0]); @@ -1996,8 +1996,8 @@ function ThemePreview({ {/* The mark is two drawings swapped by the `@dark` state, which follows the *document* — tokens override token values, not states, so the preview has - to pin the schema explicitly. Its colour is a token and needs no help. */} - + to pin the scheme explicitly. Its colour is a token and needs no help. */} + Quarterly Revenue Draft @@ -2123,18 +2123,18 @@ function ThemePreview({ } function ThemeBuilderPage() { - // Schema and contrast tier are viewing conditions, not theme settings — the + // Scheme and contrast tier are viewing conditions, not theme settings — the // same theme renders in all four of them. The switches start on whatever the // page is already showing and keep tracking it until someone presses one, so // a light preview is never stranded inside a dark page. - const documentSchema = useSchema(); + const documentScheme = useScheme(); const documentHighContrast = useHighContrast(); // `null` while the switch is still following the document. Deriving the shown // value from "override ?? document" is what lets a two-option control follow // without an `Auto` option standing for the third state — and `auto` could not // be previewed anyway: it is a *preference*, and a flat token value renders one // concrete variant. - const [schemaOverride, setSchemaOverride] = useState( + const [schemeOverride, setSchemeOverride] = useState( null, ); const [contrastOverride, setContrastOverride] = useState( @@ -2142,14 +2142,14 @@ function ThemeBuilderPage() { ); const version = usePaletteVersion(); - const schema = schemaOverride ?? documentSchema; + const scheme = schemeOverride ?? documentScheme; const isHighContrast = contrastOverride ?? documentHighContrast; // The level lives in the palette config now, so it needs no mention here: it // is part of the theme being built, and both of these pick it up. const tokens = useMemo( - () => renderColorTokens({ schema: schema, highContrast: isHighContrast }), - [schema, isHighContrast, version], + () => renderColorTokens({ scheme: scheme, highContrast: isHighContrast }), + [scheme, isHighContrast, version], ); // Resolved separately for the controls, which the *document* paints. Both @@ -2158,10 +2158,10 @@ function ThemeBuilderPage() { const documentTokens = useMemo( () => renderColorTokens({ - schema: documentSchema, + scheme: documentScheme, highContrast: documentHighContrast, }), - [documentSchema, documentHighContrast, version], + [documentScheme, documentHighContrast, version], ); return ( @@ -2182,10 +2182,10 @@ function ThemeBuilderPage() { setSchemaOverride(value as ColorSchema)} + value={scheme} + onChange={(value) => setSchemeOverride(value as ColorScheme)} > Light Dark @@ -2213,7 +2213,7 @@ function ThemeBuilderPage() { /> )} - + @@ -2246,7 +2246,7 @@ export const Playground: Story = { {DEFAULT_PALETTE_CONFIG.hue}° / saturation{' '} {DEFAULT_PALETTE_CONFIG.saturation}; the hue slider steps by 1°, so use Reset to get back to the exact shipped value. Flip the toolbar - dark-mode switch at any point — both schemas are generated from the + dark-mode switch at any point — both schemes are generated from the same seed. } diff --git a/src/stories/Usage.docs.mdx b/src/stories/Usage.docs.mdx index 47557a74c..cdc8a82b8 100644 --- a/src/stories/Usage.docs.mdx +++ b/src/stories/Usage.docs.mdx @@ -73,7 +73,7 @@ Defined in `src/tokens/sizes.ts`. Used for component heights and icon sizes. ## Shadow Tokens -Defined in `src/tokens/shadows.ts`. Shadow colors (`#shadow-sm`, `#shadow-md`, `#shadow-lg`) are generated by Glaze and adapt automatically to dark / high-contrast schemas. +Defined in `src/tokens/shadows.ts`. Shadow colors (`#shadow-sm`, `#shadow-md`, `#shadow-lg`) are generated by Glaze and adapt automatically to dark / high-contrast schemes. | Token | Value | | ---------------- | ------------------------- | @@ -96,39 +96,39 @@ Defined in `src/tokens/layout.ts`. All color tokens use `#name` syntax in styles. Opacity variants use `#name.NN` (e.g. `#surface-text.06` for 6% opacity). -The palette is generated by [`@tenphi/glaze`](https://github.com/tenphi/glaze) (see `src/tokens/palette.ts`). Every color token is emitted as a state map with `light`, `@dark`, and `@hc` (high-contrast) variants, so the entire UI Kit adapts to dark / high-contrast schemas automatically. +The palette is generated by [`@tenphi/glaze`](https://github.com/tenphi/glaze) (see `src/tokens/palette.ts`). Every color token is emitted as a state map with `light`, `@dark`, and `@hc` (high-contrast) variants, so the entire UI Kit adapts to dark / high-contrast schemes automatically. The seeds it is generated from — brand hue, saturation, per-status hues, `pastel`, `contrastLevel` — are tunable at runtime. See [Theming](/docs/getting-started-theming--docs). -### Color Schemas +### Color Schemes The `@dark` and `@hc` predefined states are wired up in `src/components/Root.tsx` to support both an attribute opt-in and the user's system preference: ```html - - + + - + - + ``` -When the attribute is absent, the schema falls back to `@media (prefers-color-scheme: dark)` and `@media (prefers-contrast: more)`. +When the attribute is absent, the scheme falls back to `@media (prefers-color-scheme: dark)` and `@media (prefers-contrast: more)`. -#### Reading the schema from JS +#### Reading the scheme from JS For styling, use the state map — `{ '': light, '@dark': dark, '@hc': hc }`. Two cases it cannot serve: a surface the stylesheet never reaches (a Vega spec, a CodeMirror / Monaco theme, an iframe), and a control whose value _is_ the ambient condition (a Light/Dark preview selector). For those, read the same two conditions from JS: ```tsx -import { useHighContrast, useSchema } from '@cube-dev/ui-kit'; +import { useHighContrast, useScheme } from '@cube-dev/ui-kit'; -const schema = useSchema(); // 'light' | 'dark' — the JS answer to `@dark` +const scheme = useScheme(); // 'light' | 'dark' — the JS answer to `@dark` const isHighContrast = useHighContrast(); // boolean — the JS answer to `@hc` ``` -Both follow the attribute opt-in first and the media query second, exactly as the states do, and re-render when either changes. Outside React: `resolveSchema()`, `resolveHighContrast()` and `subscribeSchema(listener)` (fires on either condition; re-read in the listener). This is the conditions half of the gap [`resolveTokenValue()`](#resolving-tokens-outside-css) closes for token _values_. +Both follow the attribute opt-in first and the media query second, exactly as the states do, and re-render when either changes. Outside React: `resolveScheme()`, `resolveHighContrast()` and `subscribeScheme(listener)` (fires on either condition; re-read in the listener). This is the conditions half of the gap [`resolveTokenValue()`](#resolving-tokens-outside-css) closes for token _values_. Root-level only — there is no element-scoped or generic state reader, on purpose. Tasty's state vocabulary mixes element-local states (`hovered`, `pressed`, `disabled`) with ambient ones (`@media(…)`, `@root(…)`), and only the ambient half is answerable without an element. @@ -173,7 +173,7 @@ Tokens a **container** sets to steer a component that cannot resolve the color o ```jsx - + ``` @@ -181,7 +181,7 @@ Setting it moves the label, the icon slots and the chip's rim together. It also ### Accent System -Each themed color (default, primary, success, danger, warning, note) provides an `accent-*` family anchored to a fixed white "accent surface text". These tokens stay recognizable across schemas (used for branded buttons, CTAs, etc.): +Each themed color (default, primary, success, danger, warning, note) provides an `accent-*` family anchored to a fixed white "accent surface text". These tokens stay recognizable across schemes (used for branded buttons, CTAs, etc.): | Token (default theme; prefixed in others) | Role | | --- | --- | @@ -222,10 +222,10 @@ resetPaletteConfig(); Also available: `getPaletteConfig()`, `subscribePaletteConfig()`, the `usePaletteConfig()` hook, a `` prop, `DEFAULT_PALETTE_CONFIG`, and `invalidatePaletteTokens()` for when you drive `glaze.configure(...)` yourself after mount. The palette is process-global — one palette per process, not per tree or per request. -To theme a _region_ instead of the document — a theme preview, or a dark panel inside a light page — use `renderColorTokens()`. It resolves one config and one schema to flat values you apply via a `tokens` prop, without touching the live palette: +To theme a _region_ instead of the document — a theme preview, or a dark panel inside a light page — use `renderColorTokens()`. It resolves one config and one scheme to flat values you apply via a `tokens` prop, without touching the live palette: ```tsx - + ``` @@ -344,13 +344,13 @@ resolveTokenValues(['#surface', '#surface-text', '#border']); // keyed by the to resolvePresetValues('s3'); // { fontFamily: 'ui-monospace, …', fontSize: '14px', lineHeight: '20px', … } ``` -`useTokenValue()`, `useTokenValues()` and `usePresetValues()` are the same three readers as hooks, re-resolved whenever the palette is re-seeded or the schema / contrast tier flips. The object `useTokenValues()` returns keeps its identity while the values do, so it is safe as a dependency of the theme object built from it. +`useTokenValue()`, `useTokenValues()` and `usePresetValues()` are the same three readers as hooks, re-resolved whenever the palette is re-seeded or the scheme / contrast tier flips. The object `useTokenValues()` returns keeps its identity while the values do, so it is safe as a dependency of the theme object built from it. -These resolve token _values_. For the ambient _conditions_ themselves — which schema and contrast tier the document is in — see [Reading the schema from JS](#reading-the-schema-from-js). +These resolve token _values_. For the ambient _conditions_ themselves — which scheme and contrast tier the document is in — see [Reading the scheme from JS](#reading-the-scheme-from-js). So these helpers do not inspect the value. They read `$tokens-applied`, a marker the token block declares alongside the tokens, which answers the question that matters — _are the kit's tokens in effect on this element?_ Off that surface every read returns `null` (or `fallback`) and warns once in development; on it, the computed value is taken as the truth. -All six take `{ element, fallback }`. Pass `element` to read a _local_ override instead of the document — a subtree carrying its own `tokens` prop (`renderColorTokens()`), or one under a `data-schema` / `data-contrast` that differs from the document's. Pass `fallback` for the value to use when a token cannot be resolved, including during SSR, where there is no DOM to read. +All six take `{ element, fallback }`. Pass `element` to read a _local_ override instead of the document — a subtree carrying its own `tokens` prop (`renderColorTokens()`), or one under a `data-scheme` / `data-contrast` that differs from the document's. Pass `fallback` for the value to use when a token cannot be resolved, including during SSR, where there is no DOM to read. **Why `getComputedStyle` by hand does not work.** `Root` declares the token block on ``, so ``, a detached node, and a tree that has not mounted `Root` yet are all outside it — and a token read from outside does not come back empty. Tasty registers `@property` rules with real `initial-value`s, so `--gap` reads `4px` there rather than the kit's `8px`, `--radius` reads `6px`, `--transition` reads `80ms`. The value looks entirely ordinary and is simply wrong. Inspecting it cannot help in the other direction either: `#clear` really is `transparent` and `$h2-letter-spacing` really is `0px`, indistinguishable from an unset property of the same type. diff --git a/src/stories/decorators/colorSchemaBridge.ts b/src/stories/decorators/colorSchemeBridge.ts similarity index 53% rename from src/stories/decorators/colorSchemaBridge.ts rename to src/stories/decorators/colorSchemeBridge.ts index c0b920107..2e8bed3b1 100644 --- a/src/stories/decorators/colorSchemaBridge.ts +++ b/src/stories/decorators/colorSchemeBridge.ts @@ -1,47 +1,47 @@ /** - * Single owner of the `` attribute used by the + * Single owner of the `` attribute used by the * Glaze-generated `@dark` predefined state in `src/components/Root.tsx`. * * Two writers route through here: * - The `storybook-dark-mode` addon's toolbar toggle (via the * `DARK_MODE` channel event in `.storybook/preview.jsx`). - * - The per-story `withColorSchema` decorator in this folder. + * - The per-story `withColorScheme` decorator in this folder. * - * `overrideSchema` always wins over `toolbarSchema`, so a story explicitly + * `overrideScheme` always wins over `toolbarScheme`, so a story explicitly * forced into dark/light cannot be clobbered by the addon's async init. * * NOTE: lives under `src/stories/decorators/` (not `.storybook/`) so the * decorator can import it without crossing the TypeScript include boundary. */ -export type Schema = 'light' | 'dark'; +export type Scheme = 'light' | 'dark'; -let toolbarSchema: Schema | null = null; -let overrideSchema: Schema | null = null; +let toolbarScheme: Scheme | null = null; +let overrideScheme: Scheme | null = null; const apply = (): void => { if (typeof document === 'undefined') return; - const next = overrideSchema ?? toolbarSchema; + const next = overrideScheme ?? toolbarScheme; if (next == null) { - document.documentElement.removeAttribute('data-schema'); + document.documentElement.removeAttribute('data-scheme'); } else { - document.documentElement.setAttribute('data-schema', next); + document.documentElement.setAttribute('data-scheme', next); } }; /** Set by the `storybook-dark-mode` channel listener in `preview.jsx`. */ -export const setToolbarSchema = (schema: Schema | null): void => { - toolbarSchema = schema; +export const setToolbarScheme = (scheme: Scheme | null): void => { + toolbarScheme = scheme; apply(); }; /** - * Set by the per-story `withColorSchema` decorator. + * Set by the per-story `withColorScheme` decorator. * Pass `null` to release the override and fall back to the toolbar value. */ -export const setSchemaOverride = (schema: Schema | null): void => { - overrideSchema = schema; +export const setSchemeOverride = (scheme: Scheme | null): void => { + overrideScheme = scheme; apply(); }; diff --git a/src/stories/decorators/withColorSchema.tsx b/src/stories/decorators/withColorScheme.tsx similarity index 65% rename from src/stories/decorators/withColorSchema.tsx rename to src/stories/decorators/withColorScheme.tsx index 1c5eab6f1..fc491d6a7 100644 --- a/src/stories/decorators/withColorSchema.tsx +++ b/src/stories/decorators/withColorScheme.tsx @@ -1,6 +1,6 @@ import { useLayoutEffect, useRef } from 'react'; -import { setSchemaOverride } from './colorSchemaBridge'; +import { setSchemeOverride } from './colorSchemeBridge'; import type { ReactElement } from 'react'; @@ -16,11 +16,11 @@ type StoryDecorator = ( ) => ReactElement | null; /** - * Drives the `data-schema` attribute on ``. - * - `'dark'` / `'light'` — force the corresponding schema + * Drives the `data-scheme` attribute on ``. + * - `'dark'` / `'light'` — force the corresponding scheme * - `'auto'` — clear the attribute and fall back to `prefers-color-scheme` */ -export type ColorSchema = 'light' | 'dark' | 'auto'; +export type ColorScheme = 'light' | 'dark' | 'auto'; /** * Drives the `data-contrast` attribute on ``. @@ -29,42 +29,42 @@ export type ColorSchema = 'light' | 'dark' | 'auto'; */ export type ContrastMode = 'normal' | 'high' | 'auto'; -export interface WithColorSchemaOptions { - /** Color schema applied via ``. */ - schema?: ColorSchema; +export interface WithColorSchemeOptions { + /** Color scheme applied via ``. */ + scheme?: ColorScheme; /** Contrast mode applied via ``. */ contrast?: ContrastMode; } /** - * Storybook decorator that switches a story into a different color schema by - * driving the `data-schema` / `data-contrast` attributes on `` (the same + * Storybook decorator that switches a story into a different color scheme by + * driving the `data-scheme` / `data-contrast` attributes on `` (the same * attributes the global `@dark` / `@hc` predefined states resolve against — * see `src/components/Root.tsx`). * * The body itself is `fill: '#surface'` (see `src/components/GlobalStyles.tsx`), - * so flipping `data-schema` is enough to repaint the whole story canvas — no + * so flipping `data-scheme` is enough to repaint the whole story canvas — no * extra wrappers needed. * - * `data-schema` writes go through `colorSchemaBridge` so the per-story + * `data-scheme` writes go through `colorSchemeBridge` so the per-story * override always wins over the `storybook-dark-mode` toolbar (which writes * the same attribute via the channel listener in `.storybook/preview.jsx`). * `data-contrast` is unmanaged by the addon and stays a direct DOM write. * - * Implemented synchronously in `useLayoutEffect`, so the schema switches + * Implemented synchronously in `useLayoutEffect`, so the scheme switches * before paint (no flash). The bridge restores the toolbar value on unmount, * and `data-contrast` restores its prior literal value. * * @example * export const DarkVariant = MyTemplate.bind({}); - * DarkVariant.decorators = [withColorSchema({ schema: 'dark' })]; + * DarkVariant.decorators = [withColorScheme({ scheme: 'dark' })]; */ -export const withColorSchema = ( - options: WithColorSchemaOptions = {}, +export const withColorScheme = ( + options: WithColorSchemeOptions = {}, ): StoryDecorator => { - const { schema, contrast } = options; + const { scheme, contrast } = options; - const ColorSchemaDecorator: StoryDecorator = (Story) => { + const ColorSchemeDecorator: StoryDecorator = (Story) => { const previousContrastRef = useRef(null); useLayoutEffect(() => { @@ -72,10 +72,10 @@ export const withColorSchema = ( previousContrastRef.current = html.getAttribute('data-contrast'); - if (schema === 'auto') { - setSchemaOverride(null); - } else if (schema) { - setSchemaOverride(schema); + if (scheme === 'auto') { + setSchemeOverride(null); + } else if (scheme) { + setSchemeOverride(scheme); } if (contrast === 'auto') { @@ -88,8 +88,8 @@ export const withColorSchema = ( } return () => { - if (schema) { - setSchemaOverride(null); + if (scheme) { + setSchemeOverride(null); } if (contrast) { @@ -107,18 +107,18 @@ export const withColorSchema = ( return ; }; - (ColorSchemaDecorator as { displayName?: string }).displayName = - `WithColorSchema(${schema ?? 'auto'},${contrast ?? 'auto'})`; + (ColorSchemeDecorator as { displayName?: string }).displayName = + `WithColorScheme(${scheme ?? 'auto'},${contrast ?? 'auto'})`; - return ColorSchemaDecorator; + return ColorSchemeDecorator; }; -/** Convenience preset: switches the story into the dark schema. */ -export const withDarkSchema: StoryDecorator = withColorSchema({ - schema: 'dark', +/** Convenience preset: switches the story into the dark scheme. */ +export const withDarkScheme: StoryDecorator = withColorScheme({ + scheme: 'dark', }); -/** Convenience preset: switches the story into the high-contrast schema. */ -export const withHighContrast: StoryDecorator = withColorSchema({ +/** Convenience preset: switches the story into the high-contrast scheme. */ +export const withHighContrast: StoryDecorator = withColorScheme({ contrast: 'high', }); diff --git a/src/test/probe/config-guard.ts b/src/test/probe/config-guard.ts index 514e27572..fe58d1c60 100644 --- a/src/test/probe/config-guard.ts +++ b/src/test/probe/config-guard.ts @@ -36,7 +36,7 @@ export function assertConfigApplied(): void { if (!states || !('@dark' in states)) { throw new Error( 'Probe harness: the `@dark` / `@hc` predefined states are missing, so ' + - "``'s module body did not run. Schema-keyed styles would resolve " + + "``'s module body did not run. Scheme-keyed styles would resolve " + 'wrong. Check what the harness imports before `components/Root`.', ); } diff --git a/src/test/probe/harness.browser.probe.tsx b/src/test/probe/harness.browser.probe.tsx index 890793ca1..0f6534613 100644 --- a/src/test/probe/harness.browser.probe.tsx +++ b/src/test/probe/harness.browser.probe.tsx @@ -36,32 +36,32 @@ const input = JSON.parse(__PROBE_INPUT__) as ProbeInput & { computedProps?: string[]; rect?: string; screenshot?: boolean; - schema?: 'light' | 'dark' | 'hc'; + scheme?: 'light' | 'dark' | 'hc'; highContrast?: boolean; }; /** - * Drive the schema the way a host app does — through the attributes on `` + * Drive the scheme the way a host app does — through the attributes on `` * that the `@dark` / `@hc` predefined states resolve against (see `Root.tsx`). * Setting a token by hand would prove nothing about how the real cascade * behaves. * * The two axes are independent, so they are separate parameters: `@hc` is a - * contrast attribute that composes with either schema. `--schema hc` stays + * contrast attribute that composes with either scheme. `--scheme hc` stays * accepted as the spelling Cube Cloud's probe uses, where it means light + high * contrast — but it cannot express dark + high contrast, which is a real palette * variant, so `--hc` is the flag that reaches all four. */ -function applySchema(schema: string | undefined, highContrast: boolean): void { +function applyScheme(scheme: string | undefined, highContrast: boolean): void { const root = document.documentElement; - if (schema === 'dark') { - root.setAttribute('data-schema', 'dark'); + if (scheme === 'dark') { + root.setAttribute('data-scheme', 'dark'); } - if (schema === 'light' || schema === 'hc') { - root.setAttribute('data-schema', 'light'); + if (scheme === 'light' || scheme === 'hc') { + root.setAttribute('data-scheme', 'light'); } - if (schema === 'hc' || highContrast) { + if (scheme === 'hc' || highContrast) { root.setAttribute('data-contrast', 'high'); } } @@ -184,7 +184,7 @@ it('probe:browser', async () => { // pixel geometry rather than as visibly missing output. assertConfigApplied(); - applySchema(input.schema, Boolean(input.highContrast)); + applyScheme(input.scheme, Boolean(input.highContrast)); const view = render({null}); const baseline = captureCss(view.baseElement); @@ -258,7 +258,7 @@ it('probe:browser', async () => { const scope = view.baseElement.querySelector('[data-probe-scope]'); // `--canonical` applies on both tiers. It is what makes two runs comparable - // byte-for-byte, and a browser run is exactly where you would diff one schema + // byte-for-byte, and a browser run is exactly where you would diff one scheme // or viewport against another — accepting the flag and returning raw hashes // and `useId` counters would defeat the comparison silently. const normalise = (text: string) => @@ -337,8 +337,8 @@ it('probe:browser', async () => { mode: input.mode, tier: 'browser', ok: true, - schema: `${input.schema ?? 'default'}${ - input.highContrast && input.schema !== 'hc' ? ' + high contrast' : '' + scheme: `${input.scheme ?? 'default'}${ + input.highContrast && input.scheme !== 'hc' ? ' + high contrast' : '' }`, html, portalHtml, diff --git a/src/test/probe/harness.probe.tsx b/src/test/probe/harness.probe.tsx index 7275e1475..dc99ef1b2 100644 --- a/src/test/probe/harness.probe.tsx +++ b/src/test/probe/harness.probe.tsx @@ -64,7 +64,7 @@ it('probe', async () => { if (input.mode === 'tokens') { writeResult(input, { mode: 'tokens', - schema: `${input.tokenOptions?.schema ?? 'light'}${ + scheme: `${input.tokenOptions?.scheme ?? 'light'}${ input.tokenOptions?.highContrast ? ' + high contrast' : '' }`, // Flat literal values for one variant. The legacy aliases come back BY @@ -72,7 +72,7 @@ it('probe', async () => { // preview re-resolves them against its own tokens — so they are reported // separately rather than being mistaken for resolved colors. resolved: renderColorTokens(input.tokenOptions), - // The same palette as tasty state maps keyed by schema + // The same palette as tasty state maps keyed by scheme // (`'' | '@dark' | '@hc' | '@dark & @hc'`), which is the four-variant view // a palette change has to be diffed across. A different shape from the // above, so it is labelled rather than merged. diff --git a/src/test/probe/io.ts b/src/test/probe/io.ts index 18d6c7f4d..c06b52e55 100644 --- a/src/test/probe/io.ts +++ b/src/test/probe/io.ts @@ -43,7 +43,7 @@ export interface ProbeInput { */ snippetUrl?: string; styles?: Styles; - tokenOptions?: { schema?: 'light' | 'dark'; highContrast?: boolean }; + tokenOptions?: { scheme?: 'light' | 'dark'; highContrast?: boolean }; fullCss?: boolean; canonical?: boolean; } diff --git a/src/tokens/color-theme.test.ts b/src/tokens/color-theme.test.ts index 7df39bbd3..1eebc0167 100644 --- a/src/tokens/color-theme.test.ts +++ b/src/tokens/color-theme.test.ts @@ -13,7 +13,7 @@ import { setPaletteConfig, } from './palette-config'; -const SCHEMAS = ['', '@dark', '@hc', '@dark & @hc'] as const; +const SCHEMES = ['', '@dark', '@hc', '@dark & @hc'] as const; function wcag(background: string, foreground: string): number { const luminance = (value: string) => { @@ -61,7 +61,7 @@ describe('getColorTheme', () => { ); }); - it('covers all four schema variants', () => { + it('covers all four scheme variants', () => { const theme = getColorTheme({ hue: 200 }); for (const token of Object.values(theme.colors)) { @@ -71,12 +71,12 @@ describe('getColorTheme', () => { // `@media(prefers-color-scheme: dark)` keys and no high-contrast tier at // all — a silent failure, since the light values still render. expect(Object.keys(theme.tokens[token] as object).sort()).toEqual( - [...SCHEMAS].sort(), + [...SCHEMES].sort(), ); } }); - it('solves the text contrast in every schema', () => { + it('solves the text contrast in every scheme', () => { const theme = getColorTheme({ hue: 200 }); const band = theme.tokens[theme.colors['surface-2']] as Record< string, @@ -87,11 +87,11 @@ describe('getColorTheme', () => { string >; - for (const schema of SCHEMAS) { + for (const scheme of SCHEMES) { // The Cloud defect this exists to fix: there, contrast was solved once // against a white surface at pick time and never re-checked, so a dark - // schema could invert both sides into an unreadable pair. - expect(wcag(band[schema], text[schema])).toBeGreaterThanOrEqual(4.5); + // scheme could invert both sides into an unreadable pair. + expect(wcag(band[scheme], text[scheme])).toBeGreaterThanOrEqual(4.5); } }); @@ -104,10 +104,10 @@ describe('getColorTheme', () => { >; // The floor is solved against `surface-2`, which is the tighter of the two - // bands in both schemas — so `surface` clears it for free. That is the whole + // bands in both schemes — so `surface` clears it for free. That is the whole // reason the text is anchored there rather than to `surface`. - for (const schema of SCHEMAS) { - expect(wcag(base[schema], text[schema])).toBeGreaterThanOrEqual(4.5); + for (const scheme of SCHEMES) { + expect(wcag(base[scheme], text[scheme])).toBeGreaterThanOrEqual(4.5); } }); @@ -122,8 +122,8 @@ describe('getColorTheme', () => { // Banding that resolves to the same colour is not banding. This is also what // keeps the two out of tasty's value-coalescing trap when they end up in one // state map. - for (const schema of SCHEMAS) { - expect(band[schema]).not.toBe(base[schema]); + for (const scheme of SCHEMES) { + expect(band[scheme]).not.toBe(base[scheme]); } }); diff --git a/src/tokens/color-theme.ts b/src/tokens/color-theme.ts index fbcd14cf8..2b7734ded 100644 --- a/src/tokens/color-theme.ts +++ b/src/tokens/color-theme.ts @@ -28,7 +28,7 @@ export interface ColorThemeConfig { * `okhsl()`, `okhst()`, `oklch()`. * * From a colour only the HUE and SATURATION are taken; the lightness is - * discarded and re-derived per schema. That is what makes the result adaptive + * discarded and re-derived per scheme. That is what makes the result adaptive * rather than a value that happens to work in one theme. */ hue: number | string; @@ -132,7 +132,7 @@ function stableStringify(value: unknown): string { * ``` * * The tone {@link colorSeed} also reads is dropped here on purpose: a tint theme - * re-derives its lightness per schema, which is what makes it adaptive. The palette's + * re-derives its lightness per scheme, which is what makes it adaptive. The palette's * The palette's accent zone keeps the tone, because a brand fill has to *be* the * colour. * diff --git a/src/tokens/colors.ts b/src/tokens/colors.ts index e884c8f0e..016b10df7 100644 --- a/src/tokens/colors.ts +++ b/src/tokens/colors.ts @@ -36,21 +36,21 @@ const LEGACY_ALIASES: Styles = { '#dark-05': '#border', // Fixed-mode counterpart to `#dark`. Resolves to the same L≈12 surface - // but uses Glaze `mode: 'fixed'` so it does NOT invert in dark schemas. + // but uses Glaze `mode: 'fixed'` so it does NOT invert in dark schemes. // Use this whenever the design intentionally pins a dark color regardless - // of schema. Points at `#special-surface` (`mode: 'fixed'`, L=12), + // of scheme. Points at `#special-surface` (`mode: 'fixed'`, L=12), // emitted by the standalone `specialTheme` in `palette.ts` — the canonical // source of fixed-mode color tokens for `special`-variant components. '#fixed-dark': '#special-surface', // Fixed-mode counterpart to `#primary-text`. `#primary-text` is anchored // to `surface` with `mode: 'auto'`, so it flips to a *light* purple in - // dark schemas (correct on body content, which also inverts). When the + // dark schemes (correct on body content, which also inverts). When the // local fill is a fixed color instead (an always-white pill, etc.), the // adaptive text loses contrast (light purple on white) in dark mode. // Points at `#special-accent-text` (`mode: 'fixed'`, cr 6–8.5 vs fixed // white) — a dark purple readable on a white surface that stays put - // across schemas. + // across schemes. '#fixed-primary-text': '#special-accent-text', // ---- Misc neutral ---- @@ -60,7 +60,7 @@ const LEGACY_ALIASES: Styles = { '#dark-bg': '#surface-2', '#clear': 'transparent', - // Pink: independent hue, schema-static (no Glaze adaptation). Kept as a raw + // Pink: independent hue, scheme-static (no Glaze adaptation). Kept as a raw // literal rather than folded into a theme — nothing in the palette emits this // hue as a standalone token, and it is a documented public alias (see // `Usage.docs.mdx`, `tasty.config.ts`, and the `pink` key in @@ -70,7 +70,7 @@ const LEGACY_ALIASES: Styles = { // ---- Disabled state aliases ---- // `#disabled-surface` and `#disabled-surface-text` are emitted directly by - // the Glaze palette (`palette.ts`) as schema-symmetric, contrast-driven + // the Glaze palette (`palette.ts`) as scheme-symmetric, contrast-driven // tokens — no alias needed here. `#disabled` stays as a brand-tinted // backwards-compat anchor for the per-theme `#-disabled` aliases below. @@ -214,12 +214,12 @@ const COLOR_DEPENDENT_TOKENS: Styles = { }; /** - * Render every UI Kit color for one config and one schema, as flat literal + * Render every UI Kit color for one config and one scheme, as flat literal * values ready to apply to a **region** via a tasty `tokens` prop. * * ```tsx * const preview = useMemo( - * () => renderColorTokens({ hue: 210, schema: 'dark' }), + * () => renderColorTokens({ hue: 210, scheme: 'dark' }), * [], * ); * @@ -229,7 +229,7 @@ const COLOR_DEPENDENT_TOKENS: Styles = { * ``` * * Config fields merge over the *current* palette config, so - * `renderColorTokens({ schema: 'dark' })` previews the active theme in dark + * `renderColorTokens({ scheme: 'dark' })` previews the active theme in dark * without restating it. Nothing is applied globally — the live palette is * untouched. * diff --git a/src/tokens/palette-config.ts b/src/tokens/palette-config.ts index 517a71c2c..627fea528 100644 --- a/src/tokens/palette-config.ts +++ b/src/tokens/palette-config.ts @@ -177,7 +177,7 @@ export interface PaletteConfig { * and no further. * * Those floors are APCA, not WCAG, and the difference is deliberate: one WCAG ratio - * means two very different things by schema (3:1 measures Lc 56 in light but only + * means two very different things by scheme (3:1 measures Lc 56 in light but only * Lc 23 in dark), which crushed light brands while letting dark ones through. A * consequence worth stating plainly — **the emitted fill can sit below WCAG 3:1**. * `#0EA5E9` renders at 2.77:1 against a white page and is correct at that value; the diff --git a/src/tokens/palette.test.ts b/src/tokens/palette.test.ts index 6bd6928e9..5c5f70695 100644 --- a/src/tokens/palette.test.ts +++ b/src/tokens/palette.test.ts @@ -85,7 +85,7 @@ function dumpTokens(tokens: Styles): Record { return out; } -/** Pick one schema variant out of every token's state map. */ +/** Pick one scheme variant out of every token's state map. */ function variant(tokens: Styles, state: string): Record { const out: Record = {}; @@ -142,7 +142,7 @@ function hexOf(value: string): string { * * Reading the tone rather than the color is what lets a test say "this landed where the * seed asked" for a token whose hue and chroma are settled but whose lightness went - * through a schema window. + * through a scheme window. */ function toneOf(value: string): number { return ( @@ -238,7 +238,7 @@ describe('palette tokens', () => { expect(dump).toMatchSnapshot(); }); - it('emits every token in all four schema variants by default', () => { + it('emits every token in all four scheme variants by default', () => { expect(statesOf(getPaletteTokens())).toEqual([ '', '@dark', @@ -249,14 +249,14 @@ describe('palette tokens', () => { /** * The cube-face ramp's whole point is that a `LoadingAnimation` reads with the - * same weight in every schema. It is stated as a WCAG floor against `surface` - * rather than as a tone delta precisely because the dark schema resolves a + * same weight in every scheme. It is stated as a WCAG floor against `surface` + * rather than as a tone delta precisely because the dark scheme resolves a * delta inside the `darkTone` window and flattened the ramp to ~75% of its * light span. The snapshot above pins the emitted colors; this pins the * property that made them those colors, so a regression reads as "dark went * flat again" rather than as three changed oklch strings. */ - it('holds the cube faces at one contrast ratio in every schema', () => { + it('holds the cube faces at one contrast ratio in every scheme', () => { const tokens = getPaletteTokens(); const FLOORS = [1.2, 1.65, 2.4]; const HC_FLOORS = [1.35, 2.1, 3.2]; @@ -745,7 +745,7 @@ describe('setPaletteConfig', () => { const tuned = dumpTokens(getPaletteTokens()); - // Bit-identical, in all four schema variants: nothing but the code + // Bit-identical, in all four scheme variants: nothing but the code // saturation reaches these. expect(CODE_TOKENS.map((name) => tuned[name])).toEqual(before); expect(getCodeTheme().getConfig().pastel).toBe(false); @@ -1048,27 +1048,27 @@ describe('renderPaletteTokens', () => { return out; }; - expect({ ...renderPaletteTokens({ schema: 'light' }) }).toEqual( + expect({ ...renderPaletteTokens({ scheme: 'light' }) }).toEqual( variantOf(''), ); - expect({ ...renderPaletteTokens({ schema: 'dark' }) }).toEqual( + expect({ ...renderPaletteTokens({ scheme: 'dark' }) }).toEqual( variantOf('@dark'), ); expect({ - ...renderPaletteTokens({ schema: 'light', highContrast: true }), + ...renderPaletteTokens({ scheme: 'light', highContrast: true }), }).toEqual(variantOf('@hc')); expect({ - ...renderPaletteTokens({ schema: 'dark', highContrast: true }), + ...renderPaletteTokens({ scheme: 'dark', highContrast: true }), }).toEqual(variantOf('@dark & @hc')); }); it('renders a config the app is not using, without applying it', () => { const before = dumpTokens(getPaletteTokens()); - const baseline = renderPaletteTokens({ schema: 'light' }); + const baseline = renderPaletteTokens({ scheme: 'light' }); const preview = renderPaletteTokens({ accent: { hue: 30 }, - schema: 'light', + scheme: 'light', }); expect(preview['#accent-surface']).not.toBe(baseline['#accent-surface']); @@ -1080,13 +1080,13 @@ describe('renderPaletteTokens', () => { it('merges over the current config rather than the shipped defaults', () => { setPaletteConfig({ accent: { saturation: 20 } }); - expect(renderPaletteTokens({ schema: 'light' })['#accent-surface']).toBe( + expect(renderPaletteTokens({ scheme: 'light' })['#accent-surface']).toBe( getPaletteTokens()['#accent-surface']?.[''], ); }); it('is independent of the ambient contrast mode', () => { - const hc = renderPaletteTokens({ schema: 'light', highContrast: true }); + const hc = renderPaletteTokens({ scheme: 'light', highContrast: true }); // A manual global level suppresses high-contrast output in Glaze exports; // a preview must not inherit that. @@ -1095,7 +1095,7 @@ describe('renderPaletteTokens', () => { expect( renderPaletteTokens({ contrastLevel: 'auto', - schema: 'light', + scheme: 'light', highContrast: true, }), ).toEqual(hc); @@ -1107,7 +1107,7 @@ describe('renderPaletteTokens', () => { expect(glaze.getConfig().contrastLevel).toBe(60); - renderPaletteTokens({ schema: 'dark' }); + renderPaletteTokens({ scheme: 'dark' }); expect(glaze.getConfig().contrastLevel).toBe(60); }); @@ -1116,15 +1116,15 @@ describe('renderPaletteTokens', () => { // A region asking for high contrast at a mid level gets the genuine // high-contrast resolution, not a copy of its own normal variant — the level // moves the baseline, the tier escalates from wherever `'auto'` would put it. - const normal = renderPaletteTokens({ contrastLevel: 40, schema: 'light' }); + const normal = renderPaletteTokens({ contrastLevel: 40, scheme: 'light' }); const hc = renderPaletteTokens({ contrastLevel: 40, - schema: 'light', + scheme: 'light', highContrast: true, }); const autoHc = renderPaletteTokens({ contrastLevel: 'auto', - schema: 'light', + scheme: 'light', highContrast: true, }); @@ -1133,10 +1133,10 @@ describe('renderPaletteTokens', () => { }); it('emits one tier at level 100, where the two coincide', () => { - const normal = renderPaletteTokens({ contrastLevel: 100, schema: 'light' }); + const normal = renderPaletteTokens({ contrastLevel: 100, scheme: 'light' }); const hc = renderPaletteTokens({ contrastLevel: 100, - schema: 'light', + scheme: 'light', highContrast: true, }); @@ -1147,16 +1147,16 @@ describe('renderPaletteTokens', () => { // Glaze guarantees level 0 === normal and level 100 === high contrast, bit // for bit. If the level did not reach the themes in the region path, these // would silently all be the same. - for (const schema of ['light', 'dark'] as const) { - expect(renderPaletteTokens({ contrastLevel: 0, schema: schema })).toEqual( - renderPaletteTokens({ contrastLevel: 'auto', schema: schema }), + for (const scheme of ['light', 'dark'] as const) { + expect(renderPaletteTokens({ contrastLevel: 0, scheme: scheme })).toEqual( + renderPaletteTokens({ contrastLevel: 'auto', scheme: scheme }), ); expect( - renderPaletteTokens({ contrastLevel: 100, schema: schema }), + renderPaletteTokens({ contrastLevel: 100, scheme: scheme }), ).toEqual( renderPaletteTokens({ contrastLevel: 'auto', - schema: schema, + scheme: scheme, highContrast: true, }), ); @@ -1165,7 +1165,7 @@ describe('renderPaletteTokens', () => { it('interpolates between the tiers at intermediate levels', () => { const at = (contrastLevel: number | 'auto') => - renderPaletteTokens({ contrastLevel: contrastLevel, schema: 'light' }); + renderPaletteTokens({ contrastLevel: contrastLevel, scheme: 'light' }); const low = at(0); const mid = at(50); @@ -1188,10 +1188,10 @@ describe('renderPaletteTokens', () => { it('applies the level to every theme in the palette, not just the default', () => { const auto = renderPaletteTokens({ contrastLevel: 'auto', - schema: 'light', + scheme: 'light', highContrast: true, }); - const full = renderPaletteTokens({ contrastLevel: 100, schema: 'light' }); + const full = renderPaletteTokens({ contrastLevel: 100, scheme: 'light' }); // `special` is a standalone theme and the status themes are `extend()` // children — the level has to reach all of them. @@ -1213,7 +1213,7 @@ describe('renderColorTokens', () => { }); it('adds the legacy aliases by reference, not resolved', () => { - const rendered = renderColorTokens({ schema: 'dark' }); + const rendered = renderColorTokens({ scheme: 'dark' }); // Resolved palette value… expect(rendered['#surface-text']).toMatch(/^oklch\(/); @@ -1233,7 +1233,7 @@ describe('renderColorTokens', () => { }); it('re-declares the tokens whose values embed a palette color', () => { - const rendered = renderColorTokens({ schema: 'dark' }); + const rendered = renderColorTokens({ scheme: 'dark' }); // Declared on , so CSS would have frozen the outer theme's color into // them; they have to ride along by reference to re-resolve in the region. @@ -1291,12 +1291,12 @@ describe('interop with a host driving glaze directly', () => { // only input — `buildPalette` also reads Glaze's global config. Without the // version in that key, a region preview keeps serving the old palette while // the document around it has already moved. - const before = renderColorTokens({ schema: 'dark' }); + const before = renderColorTokens({ scheme: 'dark' }); glaze.configure({ darkDesaturation: 0.5 }); invalidatePaletteTokens(); - expect(renderColorTokens({ schema: 'dark' })).not.toEqual(before); + expect(renderColorTokens({ scheme: 'dark' })).not.toEqual(before); }); }); @@ -1382,7 +1382,7 @@ describe('accent color seeds', () => { }); /** - * The whole contract, as one invariant: for every brand and every schema, the fill + * The whole contract, as one invariant: for every brand and every scheme, the fill * is EITHER exactly the color asked for, OR sitting on the 3:1 floor — and it is * floored only when the color could not clear the floor by itself. * @@ -1396,7 +1396,7 @@ describe('accent color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: 'light', + scheme: 'light', }); const fill = String(tokens['#accent-surface']); const surface = String(tokens['#surface']); @@ -1430,7 +1430,7 @@ describe('accent color seeds', () => { } }); - it('lets dark adapt rather than pinning the color across schemas', () => { + it('lets dark adapt rather than pinning the color across schemes', () => { // Exactness is scoped to light / normal contrast on purpose. Dark is a // different page, and a fill pinned to one lightness across both would be a // worse `mode: 'fixed'` rather than a faithful brand — so the dark variant @@ -1441,7 +1441,7 @@ describe('accent color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: 'dark', + scheme: 'dark', }); const fill = String(tokens['#accent-surface']); @@ -1455,7 +1455,7 @@ describe('accent color seeds', () => { const dark = renderPaletteTokens({ ...EXACT, accent: '#FFD400', - schema: 'dark', + scheme: 'dark', }); expect(hexOf(String(dark['#accent-surface']))).not.toBe('#ffd400'); }); @@ -1469,7 +1469,7 @@ describe('accent color seeds', () => { const hc = renderPaletteTokens({ ...EXACT, accent: '#0EA5E9', - schema: 'light', + scheme: 'light', highContrast: true, }); @@ -1509,7 +1509,7 @@ describe('accent color seeds', () => { for (const highContrast of [false, true]) { const shipped = renderPaletteTokens({ ...EXACT, - schema: 'dark', + scheme: 'dark', highContrast, }); const reference = apcaOf( @@ -1520,7 +1520,7 @@ describe('accent color seeds', () => { const seeded = renderPaletteTokens({ ...EXACT, accent: 'okhst(280 70% 10%)', - schema: 'dark', + scheme: 'dark', highContrast, }); const separation = apcaOf( @@ -1552,19 +1552,19 @@ describe('accent color seeds', () => { // range survived in dark against light's 45. const tones = [30, 45, 60, 75]; - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { const emitted = tones.map((tone) => toneOf( String( renderPaletteTokens({ ...EXACT, accent: `okhst(280 70% ${tone}%)`, - schema, + scheme, })['#accent-surface'], ), ), ); - const label = `${schema}: ${emitted.map((t) => t.toFixed(1)).join(', ')}`; + const label = `${scheme}: ${emitted.map((t) => t.toFixed(1)).join(', ')}`; // Never inverts — a lighter seed cannot emit a darker fill. for (let i = 1; i < emitted.length; i++) { @@ -1587,16 +1587,16 @@ describe('accent color seeds', () => { // saturated hue behaves (`#FFD400` in dark high contrast lands on pure black at // 2.23 against the rest state's 7.07), and it is why the hover target is 9. for (const accentColor of BRANDS) { - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { for (const highContrast of [false, true]) { const tokens = renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: schema, + scheme: scheme, highContrast: highContrast, }); const base = String(tokens['#accent-selected-fill']); - const label = `${accentColor} ${schema}${highContrast ? ' hc' : ''}`; + const label = `${accentColor} ${scheme}${highContrast ? ' hc' : ''}`; expect( contrastOf(String(tokens['#accent-text']), base), @@ -1633,13 +1633,13 @@ describe('accent color seeds', () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); for (const accentColor of BRANDS.filter((brand) => brand !== '#FFD400')) { - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { invalidatePaletteTokens(); - renderPaletteTokens({ ...EXACT, accent: accentColor, schema: schema }); + renderPaletteTokens({ ...EXACT, accent: accentColor, scheme: scheme }); renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: schema, + scheme: scheme, highContrast: true, }); } @@ -1667,7 +1667,7 @@ describe('accent color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: 'light', + scheme: 'light', }); const drift = Math.abs( toneOf(String(tokens['#accent-text-soft'])) - @@ -1683,14 +1683,14 @@ describe('accent color seeds', () => { // collapse them into one color and delete the rest→hover intensify that // `#accent-text` exists for. for (const accentColor of BRANDS) { - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { const tokens = renderPaletteTokens({ ...EXACT, accent: accentColor, - schema: schema, + scheme: scheme, }); - expect(tokens['#accent-text'], `${accentColor} ${schema}`).not.toBe( + expect(tokens['#accent-text'], `${accentColor} ${scheme}`).not.toBe( tokens['#accent-text-soft'], ); } @@ -1711,9 +1711,9 @@ describe('accent color seeds', () => { const seeded = renderPaletteTokens({ ...EXACT, accent: '#FFD400', - schema: 'light', + scheme: 'light', }); - const baseline = renderPaletteTokens({ ...EXACT, schema: 'light' }); + const baseline = renderPaletteTokens({ ...EXACT, scheme: 'light' }); for (const name of [ '#danger-accent-surface', @@ -1766,7 +1766,7 @@ describe('accent color seeds', () => { const seed = colorSeed('#0EA5E9')!; const tokens = renderPaletteTokens({ accent: '#0EA5E9', - schema: 'light', + scheme: 'light', }); const ramp = [ '#accent-surface', @@ -1786,15 +1786,15 @@ describe('accent color seeds', () => { for (let hue = 0; hue < 360; hue += 45) { for (const tone of [20, 60, 88, 100]) { for (const saturation of [0, 60, 100]) { - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { for (const highContrast of [false, true]) { const seed = `okhst(${hue} ${saturation}% ${tone}%)`; const tokens = renderPaletteTokens({ accent: seed, - schema: schema, + scheme: scheme, highContrast: highContrast, }); - const label = `${seed} ${schema}${highContrast ? ' hc' : ''}`; + const label = `${seed} ${scheme}${highContrast ? ' hc' : ''}`; // The white label — the guarantee `accentToneCeiling` exists for, and the // one that has to hold at text strength. @@ -1834,9 +1834,9 @@ describe('accent color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, accent: '#FFD400', - schema: 'light', + scheme: 'light', }); - const baseline = renderPaletteTokens({ ...EXACT, schema: 'light' }); + const baseline = renderPaletteTokens({ ...EXACT, scheme: 'light' }); expect(hueOf(String(tokens['#special-accent-surface']))).toBeCloseTo( colorSeed('#FFD400')!.hue, @@ -1852,7 +1852,7 @@ describe('accent color seeds', () => { // `#FFD400` sits on the sRGB gamut boundary, so its saturation is exactly 100 — // which makes it the clearest witness for the clip. const seed = colorSeed('#FFD400')!; - const baseline = renderPaletteTokens({ schema: 'light' }); + const baseline = renderPaletteTokens({ scheme: 'light' }); setPaletteConfig({ base: '#FFD400' }); @@ -1869,7 +1869,7 @@ describe('accent color seeds', () => { // The base zone moved and the accent zone did not. `baseline` is captured before // the write on purpose: `renderPaletteTokens` LAYERS over the live config, so a // baseline taken afterwards would already carry the base color. - const seeded = renderPaletteTokens({ schema: 'light' }); + const seeded = renderPaletteTokens({ scheme: 'light' }); expect(seeded['#border']).not.toBe(baseline['#border']); expect(seeded['#accent-surface']).toBe(baseline['#accent-surface']); @@ -1903,7 +1903,7 @@ describe('accent color seeds', () => { // The one guarantee this must not break: a brand — or a chrome — expressed as a // color cannot re-chromatize the status themes. They inherit the palette seed, // and no color writes to it. - const baseline = renderPaletteTokens({ schema: 'light' }); + const baseline = renderPaletteTokens({ scheme: 'light' }); const statuses = Object.keys(baseline).filter((name) => /^#(success|danger|warning|note)-/.test(name), ); @@ -1917,7 +1917,7 @@ describe('accent color seeds', () => { ] as PaletteConfig[]) { setPaletteConfig(config); - const seeded = renderPaletteTokens({ schema: 'light' }); + const seeded = renderPaletteTokens({ scheme: 'light' }); for (const name of statuses) expect(seeded[name], `${JSON.stringify(config)} ${name}`).toBe( @@ -2045,12 +2045,12 @@ describe('accent color seeds', () => { // This is the divergence the Theme Builder shows as requested-vs-resolved chips. const softened = renderPaletteTokens({ accent: '#EF4444', - schema: 'light', + scheme: 'light', }); const exact = renderPaletteTokens({ ...EXACT, accent: '#EF4444', - schema: 'light', + scheme: 'light', }); expectSameColor( @@ -2137,11 +2137,11 @@ describe('accent color seeds', () => { const preview = renderPaletteTokens({ ...EXACT, accent: '#FFD400', - schema: 'light', + scheme: 'light', }); expect(preview['#accent-surface']).not.toBe( - renderPaletteTokens({ schema: 'light' })['#accent-surface'], + renderPaletteTokens({ scheme: 'light' })['#accent-surface'], ); expect(getPaletteConfig()).toEqual(DEFAULT_PALETTE_CONFIG); }); @@ -2195,7 +2195,7 @@ describe('status color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, themes: { danger: color }, - schema: 'light', + scheme: 'light', }); const fill = String(tokens['#danger-accent-surface']); const surface = String(tokens['#surface']); @@ -2213,7 +2213,7 @@ describe('status color seeds', () => { it('reaches the theme’s text and icon, hover a step past rest', () => { setPaletteConfig({ ...EXACT, themes: { danger: '#b91c1c' } }); - const tokens = renderPaletteTokens({ schema: 'light' }); + const tokens = renderPaletteTokens({ scheme: 'light' }); const seed = colorSeed('#b91c1c')!; // The rest link IS the brand — the visible payoff of a color seed — and the hover @@ -2249,12 +2249,12 @@ describe('status color seeds', () => { const seeded = renderPaletteTokens({ ...EXACT, themes: { danger: color }, - schema: 'light', + scheme: 'light', }); const numeric = renderPaletteTokens({ ...EXACT, themes: { danger: { hue: seed.hue, saturation: seed.saturation } }, - schema: 'light', + scheme: 'light', }); for (const name of Object.keys(FACTORS)) { @@ -2270,9 +2270,9 @@ describe('status color seeds', () => { const muted = renderPaletteTokens({ ...EXACT, themes: { danger: '#8d6e63' }, - schema: 'light', + scheme: 'light', }); - const unseeded = renderPaletteTokens({ ...EXACT, schema: 'light' }); + const unseeded = renderPaletteTokens({ ...EXACT, scheme: 'light' }); expect(chromaOf(String(muted['#danger-border'])) * 3).toBeLessThan( chromaOf(String(unseeded['#danger-border'])), @@ -2287,7 +2287,7 @@ describe('status color seeds', () => { const tokens = renderPaletteTokens({ ...EXACT, themes: { note: '#0EA5E9' }, - schema: 'light', + scheme: 'light', }); const fill = String(tokens['#note-accent-surface']); @@ -2304,11 +2304,11 @@ describe('status color seeds', () => { // the case that proves it: uncapped, the button would be a white label on white. for (const name of ['danger', 'success', 'warning', 'note'] as const) { for (const highContrast of [false, true]) { - for (const schema of ['light', 'dark'] as const) { + for (const scheme of ['light', 'dark'] as const) { const tokens = renderPaletteTokens({ ...EXACT, themes: { [name]: 'okhst(20 80% 96%)' }, - schema, + scheme, highContrast, }); const fill = String(tokens[`#${name}-accent-surface`]); @@ -2318,7 +2318,7 @@ describe('status color seeds', () => { // serializer's rather than the solver's. expect( apcaOf('#ffffff', fill), - `${name} ${schema}${highContrast ? ' hc' : ''}`, + `${name} ${scheme}${highContrast ? ' hc' : ''}`, ).toBeGreaterThanOrEqual(44.9); } } @@ -2326,11 +2326,11 @@ describe('status color seeds', () => { }); it('scopes a color to its own theme, in both directions', () => { - const baseline = renderPaletteTokens({ ...EXACT, schema: 'light' }); + const baseline = renderPaletteTokens({ ...EXACT, scheme: 'light' }); const seeded = renderPaletteTokens({ ...EXACT, themes: { danger: '#b91c1c' }, - schema: 'light', + scheme: 'light', }); // Nothing outside `danger` may move — not the other three statuses, not the brand @@ -2351,7 +2351,7 @@ describe('status color seeds', () => { ...EXACT, accent: '#FFD400', themes: { danger: '#b91c1c' }, - schema: 'light', + scheme: 'light', }); for (const name of [ @@ -2418,11 +2418,11 @@ describe('status color seeds', () => { const preview = renderPaletteTokens({ ...EXACT, themes: { danger: '#b91c1c' }, - schema: 'light', + scheme: 'light', }); expect(preview['#danger-accent-surface']).not.toBe( - renderPaletteTokens({ schema: 'light' })['#danger-accent-surface'], + renderPaletteTokens({ scheme: 'light' })['#danger-accent-surface'], ); expect(getPaletteConfig()).toEqual(DEFAULT_PALETTE_CONFIG); }); diff --git a/src/tokens/palette.ts b/src/tokens/palette.ts index 2b695ac6a..9d74ec2f1 100644 --- a/src/tokens/palette.ts +++ b/src/tokens/palette.ts @@ -28,11 +28,11 @@ import type { ResolvedThemeSeed, } from './palette-config'; -/** Which resolved schema variant {@link renderPaletteTokens} should return. */ +/** Which resolved scheme variant {@link renderPaletteTokens} should return. */ export interface RenderPaletteOptions extends PaletteConfig { - /** Color schema to resolve. Default: `'light'`. */ - schema?: 'light' | 'dark'; - /** Resolve the high-contrast variant of that schema. Default: `false`. */ + /** Color scheme to resolve. Default: `'light'`. */ + scheme?: 'light' | 'dark'; + /** Resolve the high-contrast variant of that scheme. Default: `false`. */ highContrast?: boolean; } @@ -174,7 +174,7 @@ function baseSaturationScale(config: ResolvedPaletteConfig): number { * so Glaze solves `apcaContrast(surface, fill)`. * * **This number is page separation and nothing else**, and getting that wrong is what - * made the dark schema unusable. It reads differently by schema — in light `surface` IS + * made the dark scheme unusable. It reads differently by scheme — in light `surface` IS * white, so the same measurement happens to be the white-label pair, which invited the * value to be set for the LABEL at Lc 45 (`large` text) with 60 in high contrast. In * dark the page is near-black and the same number is a demand that a filled shape reach @@ -214,7 +214,7 @@ function baseSaturationScale(config: ResolvedPaletteConfig): number { * dark (12 hues, spread under 2 Lc — hue is not a factor, polarity is). That is 2.4x * stricter in light than in dark, which is why light brands kept getting crushed while * dark ones sailed through under the same rule. An Lc is one number that means one - * thing in both schemas. + * thing in both schemes. * * The pair is written out with BOTH entries equal, and that is the whole reason it is a * pair: stating one level would let APCA's automatic +15 Lc enhancement fire in high @@ -247,7 +247,7 @@ const ACCENT_FILL_CONTRAST: ContrastSpec = { apca: [25, 25] }; * `[4.5, 9]` pair, whose `9` existed only because a WCAG ratio that high is unreachable * for a saturated hue against a chromatic base: `#FFD400` in dark high contrast used to * pin to pure black and come out a hover link *less* readable than its rest state. An - * Lc target is reachable in both schemas because it is polarity-aware, so the + * Lc target is reachable in both schemes because it is polarity-aware, so the * pathological case has no equivalent here. */ const ACCENT_TEXT_CONTRAST: ContrastSpec = { apca: [60, 85] }; @@ -271,7 +271,7 @@ const ACCENT_RAMP = { * Both are pinned to the caller's tone otherwise, and a pair at the same tone behind * the same floor resolves to one color — which would silently delete the rest→hover * intensify that `accent-text` exists for. Tone is contrast-uniform, so one step is - * one step in either schema. + * one step in either scheme. */ const ACCENT_TEXT_HOVER_STEP = 6; @@ -309,7 +309,7 @@ const ACCENT_LABEL_LC = 45; * The ceiling is computed on the bare seed, but the emitted `accent-surface` then goes * through the page floor, which can only LIGHTEN — and a lighter fill is a weaker white * label, so the solve eats into the margin the ceiling just established. Measured worst - * case across 3072 hue/chroma/tone/schema/tier combinations is 1.8 Lc, in dark high + * case across 3072 hue/chroma/tone/scheme/tier combinations is 1.8 Lc, in dark high * contrast where the page floor pushes hardest; 3 covers it with room. */ const ACCENT_LABEL_MARGIN = 3; @@ -349,7 +349,7 @@ function labelLcOf(variant: Parameters[0]): number { * * The search runs against Glaze's own fixed-mode resolution — the same mapping * `accent-surface` goes through — rather than reimplementing the dark tone window, and - * checks all four variants so the cap is a property of the seed and not of one schema. + * checks all four variants so the cap is a property of the seed and not of one scheme. * * Only ever lowers, so a brand already dark enough comes back untouched. */ @@ -711,7 +711,7 @@ function tintedSurfaceOverride(config: ResolvedPaletteConfig): ColorMap { * pinned totals, and `surface-2-text`. * * The text is anchored to `surface-2`, not `surface`. `surface-2` has the lower - * contrast headroom in BOTH schemas — a darker background under dark text in + * contrast headroom in BOTH schemes — a darker background under dark text in * light, a lighter one under light text in dark — so solving the floor there * clears it on both bands. The neutral ramp's own `surface-2-text` is shaped the * same way for the same reason. @@ -733,7 +733,7 @@ export function tintRecipe(config: ResolvedPaletteConfig): ColorMap { base: 'surface-2', tone: `${TEXT_TONE - TINTED_SURFACE_TONE_OFFSET - SURFACE_2_TEXT_OFFSET}`, saturation: 0.25, - // The whole point: Glaze binary-searches the tone per schema until the floor + // The whole point: Glaze binary-searches the tone per scheme until the floor // is met, so a caller cannot persist an unreadable pair. contrast: ['AA', 'AAA'], }, @@ -790,7 +790,7 @@ function accentFillColors(accent: AccentSeed): ColorMap { // ---- Accent system (theme-aware, inherited by colored themes) ---- // Everything here is anchored to a fixed white "accent-surface-text" via // `mode: 'fixed'` + relative tone deltas, so accent colors stay visually - // consistent across light/dark/high-contrast schemas (the brand color does + // consistent across light/dark/high-contrast schemes (the brand color does // not flip). The solid fills are white-text-on-brand backgrounds, so they // keep an `['AA','AAA']` contrast floor even though the chosen tone deltas // already exceed it. This leaves room for a future low-contrast scale. @@ -816,7 +816,7 @@ function accentFillColors(accent: AccentSeed): ColorMap { // Hover variant of `accent-surface` — a *fixed*-mode darker shade used as // the hover fill for solid PRIMARY-type buttons. Anchored to the same // accent-surface-text so it stays in the same hue family. The relative tone - // lands a few steps darker than the pressed state in both schemas. + // lands a few steps darker than the pressed state in both schemes. 'accent-surface-hover': { base: 'accent-surface-text', tone: '-58', @@ -974,7 +974,7 @@ function buildPalette( * Carry `contrastLevel` on the theme instances instead of relying on the * global Glaze config. Used when rendering a palette the app is not running, * so a preview cannot disturb the live one. The resolved values are the same - * either way; only which schema variants get *emitted* differs, and callers + * either way; only which scheme variants get *emitted* differs, and callers * that isolate pick their variant explicitly. */ isolateContrastLevel?: boolean; @@ -1160,7 +1160,7 @@ function buildPalette( }, // Disabled fill chip + text — both adaptive (mode 'auto') and positioned // with relative tone deltas against `surface` so the disabled state has the - // same perceived intensity in light, dark, and high-contrast schemas. No + // same perceived intensity in light, dark, and high-contrast schemes. No // numeric contrast prop is needed: tone is already on a WCAG-uniform scale. // // Tone deltas reproduce the legacy palette's disabled appearance exactly: @@ -1186,8 +1186,8 @@ function buildPalette( }, // Fixed-mode "always dark" surface for elements that intentionally stay - // inverted regardless of schema (tooltips, code blocks, popovers with their - // own dark theme, etc.). `mode: 'fixed'` bypasses the dark-schema inversion + // inverted regardless of scheme (tooltips, code blocks, popovers with their + // own dark theme, etc.). `mode: 'fixed'` bypasses the dark-scheme inversion // so the color reads as a dark surface in light, dark, and high-contrast. // Pair with `#white` (built-in) for foreground text. 'surface-inverse': { @@ -1203,7 +1203,7 @@ function buildPalette( ...accentColors(accent), // Brand-tinted disabled chip + label for PRIMARY-style buttons (solid brand - // fill). The chip is schema-symmetric (`mode: 'fixed'`) so the muted state + // fill). The chip is scheme-symmetric (`mode: 'fixed'`) so the muted state // reads the same weight in light/dark/HC; saturation is bumped so it stays // identifiable as a muted brand color. // @@ -1213,9 +1213,9 @@ function buildPalette( // relative `tone: '+15'` with `autoFlip: false`, because Glaze < 1.2.0 // re-mapped the extreme through the dark tone window, compressing the // base-to-extreme span and dropping the dark label's contrast. Glaze 1.2.0 - // (tenphi/glaze#82) instead replays the light schema's base→extreme tone + // (tenphi/glaze#82) instead replays the light scheme's base→extreme tone // shift against the base's resolved dark tone — same-signed under - // `mode: 'fixed'` — so `'max'` holds its intended separation in every schema + // `mode: 'fixed'` — so `'max'` holds its intended separation in every scheme // and the approximation is no longer needed. Inherited per theme. // // The special theme keeps its own relative `+18` pair: its `surface` is a @@ -1274,13 +1274,13 @@ function buildPalette( // follows a re-seeded brand hue without announcing it. // // **Contrast, not tone, is the spec.** A relative tone delta is uniform on - // the OKHST scale but the dark schema resolves it inside the `darkTone` + // the OKHST scale but the dark scheme resolves it inside the `darkTone` // window, which compressed the ramp to ~75% of its light span — measurably // flatter, which is exactly how it looked. Glaze has no per-color // `darkTone`, so the fix is to state the intent as a WCAG floor against - // `surface` and let each schema solve for it: the authored `tone: '-2'` is + // `surface` and let each scheme solve for it: the authored `tone: '-2'` is // deliberately short of every floor, so all three faces are pinned by the - // ratio in every schema rather than by a delta that means different things + // ratio in every scheme rather than by a delta that means different things // in each. Measured on the emitted tokens, light comes out 1.201 / 1.653 / // 2.409 and dark 1.212 / 1.666 / 2.424 — within 1% of each other, against // 1.063 / 1.320 / 1.915 vs 1.053 / 1.264 / 1.735 before. @@ -1421,7 +1421,7 @@ function buildPalette( // -------------------------------------------------------------------------- // // Standalone theme for `special`-variant components (hero CTAs, banners, etc.) - // that intentionally sit on a dark surface regardless of the active schema. + // that intentionally sit on a dark surface regardless of the active scheme. // // Every token here is `mode: 'fixed'` so the resolved value is identical in // light, dark, and high-contrast. The shape is purpose-built (not a full @@ -1447,7 +1447,7 @@ function buildPalette( // - `accent-disabled-surface` / `accent-disabled-surface-text` — // brand-tinted disabled chip + label, positioned with relative tone // deltas against the fixed dark `surface` so the disabled state is - // schema-symmetric. + // scheme-symmetric. const specialTheme = glaze(hue, saturation, instanceConfig); @@ -1641,7 +1641,7 @@ let renderKey: string | null = null; let renderVariants: Record> = {}; /** - * Resolve one schema variant of a palette to flat, literal color values. + * Resolve one scheme variant of a palette to flat, literal color values. * * Unlike {@link getPaletteTokens}, which emits state maps (`@dark` / `@hc`) for * the whole document, this collapses the palette to the single variant you ask @@ -1655,7 +1655,7 @@ let renderVariants: Record> = {}; export function renderPaletteTokens( options: RenderPaletteOptions = {}, ): Tokens { - const { schema = 'light', highContrast = false, ...config } = options; + const { scheme = 'light', highContrast = false, ...config } = options; const resolved = resolvePaletteConfig(config); const key = `${getPaletteVersion()}:${JSON.stringify(resolved)}`; @@ -1699,13 +1699,13 @@ export function renderPaletteTokens( } } - const variant = VARIANT_KEY[`${schema}:${highContrast}`]; + const variant = VARIANT_KEY[`${scheme}:${highContrast}`]; // The fallback is for `contrastLevel: 100` only. There the normal variants // already *are* the high-contrast ones, so Glaze emits a single light/dark set // rather than duplicating it — and `highContrast` correctly resolves to the same // colors. At every other level the contrast variants are present and genuinely // escalated, so the fallback is not taken. - const flat = renderVariants[variant] ?? renderVariants[schema]; + const flat = renderVariants[variant] ?? renderVariants[scheme]; const out: Tokens = {}; for (const name of Object.keys(flat)) out[`#${name}`] = flat[name]; diff --git a/src/tokens/resolve.ts b/src/tokens/resolve.ts index 5eb17b00f..789c79aee 100644 --- a/src/tokens/resolve.ts +++ b/src/tokens/resolve.ts @@ -1,7 +1,7 @@ import { useState, useSyncExternalStore } from 'react'; import { useLayoutEffect } from '../utils/react/useLayoutEffect'; -import { subscribeSchema } from '../utils/react/useSchema'; +import { subscribeScheme } from '../utils/react/useScheme'; import { warn } from '../utils/warnings'; import { getTokens } from './all-tokens'; @@ -78,7 +78,7 @@ export interface ResolveTokenOptions { * * Pass an element when the value has to reflect a *local* override — a subtree * carrying its own `tokens` prop (`renderColorTokens()`), or one under a - * `data-schema` / `data-contrast` attribute that differs from the document's. + * `data-scheme` / `data-contrast` attribute that differs from the document's. * The element's own document and view are used, so a node inside a same-origin * iframe resolves against that iframe. */ @@ -373,11 +373,11 @@ export function resolvePresetValues( /** * A token's resolved value changes for two reasons: the palette was re-seeded - * (`setPaletteConfig()`, covered by `usePaletteVersion`), or the schema / + * (`setPaletteConfig()`, covered by `usePaletteVersion`), or the scheme / * contrast tier flipped. This store covers the second. * - * The watching itself belongs to `subscribeSchema()` (`src/utils/react/useSchema.ts`), - * which owns the definition of both axes — the `data-schema` / `data-contrast` + * The watching itself belongs to `subscribeScheme()` (`src/utils/react/useScheme.ts`), + * which owns the definition of both axes — the `data-scheme` / `data-contrast` * attributes and the media queries they fall back to are the same strings * `` registers `@dark` and `@hc` from. This store only counts the changes, * so there is one observer for the document rather than one per concern. @@ -397,7 +397,7 @@ function subscribeAppearance(listener: () => void): () => void { appearanceListeners.add(listener); if (appearanceListeners.size === 1) { - stopWatchingAppearance = subscribeSchema(onAppearanceChange); + stopWatchingAppearance = subscribeScheme(onAppearanceChange); } return () => { @@ -430,7 +430,7 @@ function getServerAppearanceVersion(): number { } /** - * Re-render on a schema / contrast change. Returns the version rather than the + * Re-render on a scheme / contrast change. Returns the version rather than the * state so the snapshot is a primitive — the same reason `usePaletteVersion` * does, see `src/tokens/palette-config.ts`. */ @@ -448,7 +448,7 @@ function useAppearanceVersion(): number { /** * The value is read out of the DOM, so it re-resolves whenever the palette is - * re-seeded or the schema / contrast tier flips, and it is `fallback ?? null` + * re-seeded or the scheme / contrast tier flips, and it is `fallback ?? null` * during SSR. The layout effect is what covers the first commit: a consumer * rendered in the same pass as `` reads before the token block lands. */ @@ -512,7 +512,7 @@ function isSameRecord>( /** * {@link resolveTokenValue} as a hook: re-renders when the palette is re-seeded - * or the schema / contrast tier flips. + * or the scheme / contrast tier flips. * * ```tsx * const accent = useTokenValue('#purple'); diff --git a/src/tokens/shadows.ts b/src/tokens/shadows.ts index 0b082a19e..742bb55e6 100644 --- a/src/tokens/shadows.ts +++ b/src/tokens/shadows.ts @@ -9,7 +9,7 @@ import type { Styles } from '@tenphi/tasty'; * * The shadow colors (`#shadow-sm` / `#shadow-md` / `#shadow-lg`) are * generated by Glaze and adapt automatically to dark / high-contrast - * schemas — see `src/tokens/palette.ts`. + * schemes — see `src/tokens/palette.ts`. * * Keys use $ prefix for CSS custom properties. */ diff --git a/src/utils/react/index.ts b/src/utils/react/index.ts index 49b0dc6e9..62a732041 100644 --- a/src/utils/react/index.ts +++ b/src/utils/react/index.ts @@ -17,13 +17,13 @@ export { useCombinedRefs, mergeRefs } from './useCombinedRefs'; export { wrapNodeIfPlain } from './wrapNodeIfPlain'; export { useViewportSize } from './useViewportSize'; export { - useSchema, + useScheme, useHighContrast, - resolveSchema, + resolveScheme, resolveHighContrast, - subscribeSchema, -} from './useSchema'; -export type { ColorSchema } from './useSchema'; + subscribeScheme, +} from './useScheme'; +export type { ColorScheme } from './useScheme'; export { useQaProps } from './useQaProps'; export { useEventBus, useEventListener, EventBusProvider } from './useEventBus'; export type { EventBusListener, EventBusContextValue } from './useEventBus'; diff --git a/src/utils/react/useSchema.test.tsx b/src/utils/react/useScheme.test.tsx similarity index 77% rename from src/utils/react/useSchema.test.tsx rename to src/utils/react/useScheme.test.tsx index fc48e7d68..2c30dd2b6 100644 --- a/src/utils/react/useSchema.test.tsx +++ b/src/utils/react/useScheme.test.tsx @@ -4,10 +4,10 @@ import { renderHook } from '../../test'; import { resolveHighContrast, - resolveSchema, + resolveScheme, useHighContrast, - useSchema, -} from './useSchema'; + useScheme, +} from './useScheme'; const DARK = '(prefers-color-scheme: dark)'; const MORE_CONTRAST = '(prefers-contrast: more)'; @@ -47,7 +47,7 @@ function stubMatchMedia(initial: Record = {}) { }; } -describe('useSchema / useHighContrast', () => { +describe('useScheme / useHighContrast', () => { const originalMatchMedia = window.matchMedia; afterEach(() => { @@ -57,42 +57,42 @@ describe('useSchema / useHighContrast', () => { writable: true, value: originalMatchMedia, }); - document.documentElement.removeAttribute('data-schema'); + document.documentElement.removeAttribute('data-scheme'); document.documentElement.removeAttribute('data-contrast'); }); - describe('resolveSchema', () => { + describe('resolveScheme', () => { it('falls back to the media query when no attribute is set', () => { const set = stubMatchMedia({ [DARK]: true }); - expect(resolveSchema()).toBe('dark'); + expect(resolveScheme()).toBe('dark'); set(DARK, false); - expect(resolveSchema()).toBe('light'); + expect(resolveScheme()).toBe('light'); }); it('lets the attribute opt-in win over the system preference', () => { stubMatchMedia({ [DARK]: true }); - document.documentElement.setAttribute('data-schema', 'light'); + document.documentElement.setAttribute('data-scheme', 'light'); - expect(resolveSchema()).toBe('light'); + expect(resolveScheme()).toBe('light'); - document.documentElement.setAttribute('data-schema', 'dark'); + document.documentElement.setAttribute('data-scheme', 'dark'); - expect(resolveSchema()).toBe('dark'); + expect(resolveScheme()).toBe('dark'); }); it('reads any other attribute value as light, exactly as `@dark` does', () => { stubMatchMedia({ [DARK]: true }); // `@dark` gates the media fallback on the attribute being *absent* - // (`!@root(schema)`), so a present-but-unknown value stays light rather + // (`!@root(scheme)`), so a present-but-unknown value stays light rather // than falling through to the preference. - document.documentElement.setAttribute('data-schema', 'sepia'); + document.documentElement.setAttribute('data-scheme', 'sepia'); - expect(resolveSchema()).toBe('light'); + expect(resolveScheme()).toBe('light'); }); }); @@ -119,18 +119,18 @@ describe('useSchema / useHighContrast', () => { it('re-renders when the attribute flips', async () => { stubMatchMedia(); - const { result } = renderHook(() => useSchema()); + const { result } = renderHook(() => useScheme()); expect(result.current).toBe('light'); await act(async () => { - document.documentElement.setAttribute('data-schema', 'dark'); + document.documentElement.setAttribute('data-scheme', 'dark'); }); await waitFor(() => expect(result.current).toBe('dark')); await act(async () => { - document.documentElement.removeAttribute('data-schema'); + document.documentElement.removeAttribute('data-scheme'); }); await waitFor(() => expect(result.current).toBe('light')); @@ -139,15 +139,15 @@ describe('useSchema / useHighContrast', () => { it('re-renders when the system preference changes', async () => { const set = stubMatchMedia({ [DARK]: false, [MORE_CONTRAST]: false }); - const schema = renderHook(() => useSchema()); + const scheme = renderHook(() => useScheme()); const contrast = renderHook(() => useHighContrast()); - expect(schema.result.current).toBe('light'); + expect(scheme.result.current).toBe('light'); expect(contrast.result.current).toBe(false); await act(async () => set(DARK, true)); - expect(schema.result.current).toBe('dark'); + expect(scheme.result.current).toBe('dark'); expect(contrast.result.current).toBe(false); await act(async () => set(MORE_CONTRAST, true)); @@ -160,12 +160,12 @@ describe('useSchema / useHighContrast', () => { // The observer and the query listeners are shared and torn down when the // listener set empties, so a remount has to build them again. - renderHook(() => useSchema()).unmount(); + renderHook(() => useScheme()).unmount(); - const { result } = renderHook(() => useSchema()); + const { result } = renderHook(() => useScheme()); await act(async () => { - document.documentElement.setAttribute('data-schema', 'dark'); + document.documentElement.setAttribute('data-scheme', 'dark'); }); await waitFor(() => expect(result.current).toBe('dark')); diff --git a/src/utils/react/useSchema.ts b/src/utils/react/useScheme.ts similarity index 84% rename from src/utils/react/useSchema.ts rename to src/utils/react/useScheme.ts index 66c058648..80afabc95 100644 --- a/src/utils/react/useSchema.ts +++ b/src/utils/react/useScheme.ts @@ -1,7 +1,7 @@ import { useSyncExternalStore } from 'react'; /** - * The ambient viewing conditions — the color schema and the contrast tier the + * The ambient viewing conditions — the color scheme and the contrast tier the * document is showing right now. * * This module is the single owner of the definition: it builds the `@dark` / @@ -11,7 +11,7 @@ import { useSyncExternalStore } from 'react'; * * **For styling, do not use this.** `{ '': light, '@dark': dark, '@hc': hc }` is * the answer, and branching styles in JS gives up the conditionality that lets a - * schema flip repaint without a re-render. Two cases the state map cannot serve: + * scheme flip repaint without a re-render. Two cases the state map cannot serve: * * 1. **Surfaces the stylesheet does not reach** — a Vega spec, a CodeMirror or * Monaco theme, a third-party iframe. They take values, not CSS, so `@dark` @@ -34,12 +34,12 @@ import { useSyncExternalStore } from 'react'; /** * The `data-*` attribute names the opt-in uses, and the media queries it falls - * back to. `@root(schema=dark)` compiles to `:root[data-schema="dark"]`, hence + * back to. `@root(scheme=dark)` compiles to `:root[data-scheme="dark"]`, hence * the `data-` prefix on the DOM side and the bare key on the tasty side. */ -const SCHEMA_KEY = 'schema'; +const SCHEME_KEY = 'scheme'; const CONTRAST_KEY = 'contrast'; -const SCHEMA_ATTR = `data-${SCHEMA_KEY}`; +const SCHEME_ATTR = `data-${SCHEME_KEY}`; const CONTRAST_ATTR = `data-${CONTRAST_KEY}`; const DARK_QUERY = '(prefers-color-scheme: dark)'; const HIGH_CONTRAST_QUERY = '(prefers-contrast: more)'; @@ -49,18 +49,18 @@ const HIGH_CONTRAST_QUERY = '(prefers-contrast: more)'; * `` via `setGlobalPredefinedStates()`. * * The attribute opt-in wins over the system preference, and the fallback is - * gated on the attribute being *absent* (`!@root(schema)`) rather than on it - * being some other value — so `` stays light inside a - * dark OS, which is the whole point of an opt-in. {@link resolveSchema} and + * gated on the attribute being *absent* (`!@root(scheme)`) rather than on it + * being some other value — so `` stays light inside a + * dark OS, which is the whole point of an opt-in. {@link resolveScheme} and * {@link resolveHighContrast} read the same way. */ export const AMBIENT_PREDEFINED_STATES = { - '@dark': `@root(${SCHEMA_KEY}=dark) | (!@root(${SCHEMA_KEY}) & @media${DARK_QUERY})`, + '@dark': `@root(${SCHEME_KEY}=dark) | (!@root(${SCHEME_KEY}) & @media${DARK_QUERY})`, '@hc': `@root(${CONTRAST_KEY}=high) | (!@root(${CONTRAST_KEY}) & @media${HIGH_CONTRAST_QUERY})`, } as const; -/** The color schema the document resolves to — the two arms of the `@dark` state. */ -export type ColorSchema = 'light' | 'dark'; +/** The color scheme the document resolves to — the two arms of the `@dark` state. */ +export type ColorScheme = 'light' | 'dark'; // ============================================================================ // Reading @@ -85,14 +85,14 @@ function rootAttribute(name: string): string | null { } /** - * The document's current color schema, read once — the JS answer to `@dark`. + * The document's current color scheme, read once — the JS answer to `@dark`. * * Outside React (a chart spec built in a module, an editor theme registered at - * import time). In React use {@link useSchema}, which also re-renders on change. + * import time). In React use {@link useScheme}, which also re-renders on change. * Returns `'light'` with no DOM. */ -export function resolveSchema(): ColorSchema { - const attribute = rootAttribute(SCHEMA_ATTR); +export function resolveScheme(): ColorScheme { + const attribute = rootAttribute(SCHEME_ATTR); if (attribute !== null) { return attribute === 'dark' ? 'dark' : 'light'; @@ -169,7 +169,7 @@ function startWatching(): () => void { observer.observe(document.documentElement, { attributes: true, - attributeFilter: [SCHEMA_ATTR, CONTRAST_ATTR], + attributeFilter: [SCHEME_ATTR, CONTRAST_ATTR], }); teardowns.push(() => observer.disconnect()); @@ -179,15 +179,15 @@ function startWatching(): () => void { } /** - * Subscribe to ambient condition changes — either the schema or the contrast + * Subscribe to ambient condition changes — either the scheme or the contrast * tier. Returns an unsubscribe function. * - * The listener takes no argument: re-read with {@link resolveSchema} / + * The listener takes no argument: re-read with {@link resolveScheme} / * {@link resolveHighContrast}, which is what the hooks below do. For non-React * consumers that own a surface the stylesheet cannot reach — re-theming a Monaco * instance, re-rendering a chart. */ -export function subscribeSchema(listener: () => void): () => void { +export function subscribeScheme(listener: () => void): () => void { listeners.add(listener); if (!stopWatching) { @@ -210,27 +210,27 @@ export function subscribeSchema(listener: () => void): () => void { /** * Snapshots are primitives, so React bails out on an unchanged value and no - * memoization is needed — a contrast change re-runs a `useSchema()` reader's + * memoization is needed — a contrast change re-runs a `useScheme()` reader's * `getSnapshot` and stops there. */ -const getServerSchema = (): ColorSchema => 'light'; +const getServerScheme = (): ColorScheme => 'light'; const getServerHighContrast = () => false; /** - * The document's color schema, kept live — `'light'` or `'dark'`. + * The document's color scheme, kept live — `'light'` or `'dark'`. * * ```tsx - * const schema = useSchema(); + * const scheme = useScheme(); * - * buildSpec(schema), [schema])} />; + * buildSpec(scheme), [scheme])} />; * ``` * - * Follows both the `` opt-in and `prefers-color-scheme`, + * Follows both the `` opt-in and `prefers-color-scheme`, * exactly as the `@dark` state does. Under SSR it renders `'light'` and * re-renders with the real value after hydration. */ -export function useSchema(): ColorSchema { - return useSyncExternalStore(subscribeSchema, resolveSchema, getServerSchema); +export function useScheme(): ColorScheme { + return useSyncExternalStore(subscribeScheme, resolveScheme, getServerScheme); } /** @@ -243,7 +243,7 @@ export function useSchema(): ColorSchema { */ export function useHighContrast(): boolean { return useSyncExternalStore( - subscribeSchema, + subscribeScheme, resolveHighContrast, getServerHighContrast, );