Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/scheme-not-schema.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@cube-dev/ui-kit': minor
---

**Breaking:** reverted the `schema` term back to `scheme`, and renamed the `<html>` attribute to match. `schema` was the wrong word — the platform calls this a color _scheme_ (`prefers-color-scheme`, the `color-scheme` property, `<meta name="color-scheme">`), 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' })`.
- `<CubeLogo>` / `<CubeFullLogo>`: 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:

- `<html data-schema="dark">` is now `<html data-scheme="dark">`, 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.
12 changes: 6 additions & 6 deletions .storybook/preview.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,13 +47,13 @@ const darkTheme = create({

configure({ testIdAttribute: 'data-qa', asyncUtilTimeout: 10000 });

// Bridge the `storybook-dark-mode` toolbar to `<html data-schema>` (the
// Bridge the `storybook-dark-mode` toolbar to `<html data-scheme>` (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');
});
}

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<Root palette={{ hue: 210, saturation: 72, themes: { danger: { hue: 12 } } }}>
Expand Down
14 changes: 7 additions & 7 deletions docs/rules/probe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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
Expand All @@ -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 `<Snippet />`, 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/<runId>/` 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 `<Root>` empty, captures, mounts the snippet, captures again and subtracts. Overlays are reported under `PORTALS` — `<Root>` 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 `<Snippet />`, 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/<runId>/` 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 `<Root>` empty, captures, mounts the snippet, captures again and subtracts. Overlays are reported under `PORTALS` — `<Root>` 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 `<Root>` 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 `<Root>` 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.)

Expand All @@ -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 `<html>` 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 `<html>` 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: <url>` — 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.

Expand Down
10 changes: 5 additions & 5 deletions scripts/chromatic-duplicates.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<html data-schema>` / `<html data-contrast>`, which the `@dark` and
// color-scheme decorators in `src/stories/decorators/withColorScheme.tsx`
// drive `<html data-scheme>` / `<html data-contrast>`, 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,
Expand Down
Loading
Loading