@unpunnyfuns/swatchbook-blocks 0.60.9 → 0.62.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import './style.css';
2
- import Color from "colorjs.io";
2
+ import { COLOR_FORMATS } from "@unpunnyfuns/swatchbook-core/color-formats";
3
+ import { formatColor, parseColor } from "@unpunnyfuns/swatchbook-core/format-color";
3
4
  import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
6
  import { getVariance, listPaths, resolveAllAt } from "@unpunnyfuns/swatchbook-core/graph";
@@ -7,188 +8,18 @@ import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
7
8
  import { SWATCHBOOK_STYLE_ELEMENT_ID, ensureStyleElement } from "@unpunnyfuns/swatchbook-core/style-element";
8
9
  import { tupleToName } from "@unpunnyfuns/swatchbook-core/themes";
9
10
  import { addons } from "storybook/preview-api";
10
- import { axes, css, cssVarPrefix, defaultTuple, diagnostics, listing, presets, tokenGraph } from "virtual:swatchbook/tokens";
11
11
  import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
12
12
  import { matchPath } from "@unpunnyfuns/swatchbook-core/match-path";
13
13
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
14
14
  import cx from "clsx";
15
- //#region src/format-color.ts
16
- const COLOR_FORMATS = [
17
- "hex",
18
- "rgb",
19
- "hsl",
20
- "oklch",
21
- "raw"
22
- ];
23
- const DEFAULT_FALLBACK = "—";
24
- /**
25
- * Convert Terrazzo's normalized color payload into a display string in the
26
- * requested format. Pure function — never throws; returns `{ value: '—' }`
27
- * for unrecognized input so calling blocks don't need try/catch.
28
- */
29
- function formatColor(value, format, fallback = DEFAULT_FALLBACK) {
30
- const normalized = coerce(value);
31
- if (!normalized) return {
32
- value: stringifyFallback(value, fallback),
33
- outOfGamut: false
34
- };
35
- if (format === "raw") return {
36
- value: compactJson(normalized),
37
- outOfGamut: false
38
- };
39
- const color = toColor(normalized);
40
- if (!color) return {
41
- value: stringifyFallback(value, fallback),
42
- outOfGamut: false
43
- };
44
- const alpha = typeof normalized.alpha === "number" ? normalized.alpha : 1;
45
- if (format === "hex") return formatHex(color, alpha);
46
- if (format === "rgb") return formatRgb(color, alpha);
47
- if (format === "hsl") return formatHsl(color, alpha);
48
- return formatOklch(color, alpha);
49
- }
50
- function coerce(value) {
51
- if (!value || typeof value !== "object") return null;
52
- const v = value;
53
- const colorSpace = typeof v.colorSpace === "string" ? v.colorSpace : void 0;
54
- const components = Array.isArray(v.components) ? v.components : Array.isArray(v.channels) ? v.channels : void 0;
55
- if (!colorSpace || !components) {
56
- if (typeof v.hex === "string") return {
57
- colorSpace: "srgb",
58
- components: hexToComponents(v.hex)
59
- };
60
- return null;
61
- }
62
- const alpha = typeof v.alpha === "number" ? v.alpha : void 0;
63
- const hexVal = v["hex"];
64
- const hex = typeof hexVal === "string" ? hexVal : void 0;
65
- return {
66
- colorSpace,
67
- components,
68
- ...alpha !== void 0 && { alpha },
69
- ...hex !== void 0 && { hex }
70
- };
71
- }
72
- function hexToComponents(hex) {
73
- const h = hex.replace("#", "");
74
- const expanded = h.length === 3 || h.length === 4 ? h.split("").map((c) => c + c).join("") : h;
75
- return [
76
- parseInt(expanded.slice(0, 2), 16) / 255,
77
- parseInt(expanded.slice(2, 4), 16) / 255,
78
- parseInt(expanded.slice(4, 6), 16) / 255
79
- ];
80
- }
15
+ //#region src/internal/styles.tsx
81
16
  /**
82
- * Map Terrazzo's canonical CSS Color 4 space identifiers to the shorter
83
- * identifiers colorjs.io registers. Only the ones that differ need an entry.
17
+ * Chrome-style primitives shared across every block. Kept as JS exports
18
+ * for the inline-style sites that still compose them into per-block style
19
+ * objects (e.g. TokenNavigator's `typePill` that builds on the shared
20
+ * pill base). The pure direct-reference chrome — surface wrapper, caption,
21
+ * empty-state — lives in `styles.css` and is applied via class names.
84
22
  */
85
- const COLORJS_SPACE_ALIASES = {
86
- "display-p3": "p3",
87
- "a98-rgb": "a98rgb",
88
- "prophoto-rgb": "prophoto"
89
- };
90
- function toColor(normalized) {
91
- const source = normalized.components ?? normalized.channels ?? [];
92
- const coords = [
93
- numberOrZero(source[0]),
94
- numberOrZero(source[1]),
95
- numberOrZero(source[2])
96
- ];
97
- const space = COLORJS_SPACE_ALIASES[normalized.colorSpace] ?? normalized.colorSpace;
98
- try {
99
- return new Color(space, coords, normalized.alpha ?? 1);
100
- } catch {
101
- return null;
102
- }
103
- }
104
- function numberOrZero(n) {
105
- return typeof n === "number" && !Number.isNaN(n) ? n : 0;
106
- }
107
- function coord(color, i) {
108
- const c = color.coords[i];
109
- return typeof c === "number" && !Number.isNaN(c) ? c : 0;
110
- }
111
- function formatHex(color, alpha) {
112
- const srgb = color.to("srgb");
113
- if (!srgb.inGamut("srgb")) return {
114
- value: formatRgb(color, alpha).value,
115
- outOfGamut: true
116
- };
117
- const r = clamp255(coord(srgb, 0));
118
- const g = clamp255(coord(srgb, 1));
119
- const b = clamp255(coord(srgb, 2));
120
- const base = `#${toHexByte(r)}${toHexByte(g)}${toHexByte(b)}`;
121
- if (alpha >= 1) return {
122
- value: base,
123
- outOfGamut: false
124
- };
125
- return {
126
- value: `${base}${toHexByte(clamp255(alpha))}`,
127
- outOfGamut: false
128
- };
129
- }
130
- function formatRgb(color, alpha) {
131
- const srgb = color.to("srgb");
132
- const inGamut = srgb.inGamut("srgb");
133
- const body = `${Math.round(clampUnit(coord(srgb, 0)) * 255)} ${Math.round(clampUnit(coord(srgb, 1)) * 255)} ${Math.round(clampUnit(coord(srgb, 2)) * 255)}`;
134
- return {
135
- value: alpha >= 1 ? `rgb(${body})` : `rgb(${body} / ${roundAlpha(alpha)})`,
136
- outOfGamut: !inGamut
137
- };
138
- }
139
- function formatHsl(color, alpha) {
140
- const hsl = color.to("hsl");
141
- const inGamut = color.to("srgb").inGamut("srgb");
142
- const body = `${roundHue(coord(hsl, 0))} ${roundPercent(coord(hsl, 1))}% ${roundPercent(coord(hsl, 2))}%`;
143
- return {
144
- value: alpha >= 1 ? `hsl(${body})` : `hsl(${body} / ${roundAlpha(alpha)})`,
145
- outOfGamut: !inGamut
146
- };
147
- }
148
- function formatOklch(color, alpha) {
149
- const oklch = color.to("oklch");
150
- const body = `${roundTo(coord(oklch, 0), 3)} ${roundTo(coord(oklch, 1), 3)} ${roundTo(coord(oklch, 2), 2)}`;
151
- return {
152
- value: alpha >= 1 ? `oklch(${body})` : `oklch(${body} / ${roundAlpha(alpha)})`,
153
- outOfGamut: false
154
- };
155
- }
156
- function clamp255(n) {
157
- return Math.max(0, Math.min(255, Math.round(n * 255)));
158
- }
159
- function clampUnit(n) {
160
- return Math.max(0, Math.min(1, n));
161
- }
162
- function toHexByte(n) {
163
- return n.toString(16).padStart(2, "0");
164
- }
165
- function roundTo(n, digits) {
166
- const f = 10 ** digits;
167
- return Math.round(n * f) / f;
168
- }
169
- function roundHue(h) {
170
- return roundTo((h % 360 + 360) % 360, 1);
171
- }
172
- function roundPercent(n) {
173
- return Math.round(n * 10) / 10;
174
- }
175
- function roundAlpha(a) {
176
- return roundTo(a, 3);
177
- }
178
- function compactJson(value) {
179
- const parts = [`"colorSpace":${JSON.stringify(value.colorSpace)}`];
180
- const components = value.components ?? value.channels;
181
- if (components) parts.push(`"components":[${components.map((c) => c === null ? "null" : c).join(", ")}]`);
182
- if (typeof value.alpha === "number" && value.alpha !== 1) parts.push(`"alpha":${value.alpha}`);
183
- return `{ ${parts.join(", ")} }`;
184
- }
185
- function stringifyFallback(value, fallback) {
186
- if (value == null) return fallback;
187
- if (typeof value === "string" || typeof value === "number") return String(value);
188
- return fallback;
189
- }
190
- //#endregion
191
- //#region src/internal/styles.tsx
192
23
  const TEXT_MUTED = "var(--swatchbook-text-muted, CanvasText)";
193
24
  const SURFACE_RAISED = "var(--swatchbook-surface-raised, Canvas)";
194
25
  const SURFACE_MUTED = "var(--swatchbook-surface-muted, rgba(128,128,128,0.15))";
@@ -196,7 +27,7 @@ const BORDER_FAINT = `1px solid var(--swatchbook-border-default, rgba(128,128,12
196
27
  const BORDER_STRONG = `1px solid var(--swatchbook-border-default, rgba(128,128,128,0.3))`;
197
28
  /**
198
29
  * Inner content for a block's "nothing to render" state. Call sites wrap
199
- * it in their own block wrapper (which already carries `themeAttrs`), so
30
+ * it in their own block wrapper (which already carries `blockWrapperAttrs`), so
200
31
  * the message itself just needs the muted type.
201
32
  */
202
33
  function EmptyState({ children }) {
@@ -243,11 +74,6 @@ function ensureSubscribed$1() {
243
74
  channel.on("updateGlobals", onGlobals);
244
75
  channel.on("setGlobals", onGlobals);
245
76
  }
246
- /**
247
- * Subscribe at module load so the `SET_GLOBALS` emission from preview init
248
- * lands in our snapshot before any block renders. Running `useSyncExternalStore`'s
249
- * `subscribe` lazily on first hook call would miss the event in most cases.
250
- */
251
77
  ensureSubscribed$1();
252
78
  function subscribe$1(cb) {
253
79
  ensureSubscribed$1();
@@ -293,7 +119,7 @@ function useActiveTheme() {
293
119
  * Active axis tuple for the current story/docs render — `Record<axisName,
294
120
  * contextName>`. Derived from the same input as {@link ThemeContext}; split
295
121
  * out so consumers needing per-axis info (toolbar, panel, tuple-aware
296
- * blocks) don't have to reparse the composed permutation ID.
122
+ * blocks) don't have to reparse the composed theme name.
297
123
  */
298
124
  const AxesContext = createContext({});
299
125
  function useActiveAxes() {
@@ -321,49 +147,66 @@ function useColorFormat() {
321
147
  /**
322
148
  * Live token snapshot backed by the addon's preview dev-time HMR event.
323
149
  *
324
- * Blocks read the virtual module at module load; without a way to notice
325
- * changes, edits to the source token files would flow into the addon's
326
- * in-memory project but nowhere else the React tree would keep
327
- * rendering the old values until a full preview reload. This module
328
- * subscribes to `TOKENS_UPDATED_EVENT` on Storybook's channel (which the
329
- * addon preview re-broadcasts from its own HMR listener) and exposes
330
- * the latest snapshot via `useSyncExternalStore`, so hooks that read
331
- * through this module re-render in place on each token save.
150
+ * The initial snapshot is *injected* by the addon preview via
151
+ * {@link registerTokenSource} rather than imported from the addon's
152
+ * `virtual:swatchbook/tokens` build artifactso blocks carries no
153
+ * dependency on that module and imports cleanly standalone (outside
154
+ * Storybook, in unit tests, in the docs site). Until something registers
155
+ * a source, blocks render from empty defaults.
332
156
  *
333
- * Outside the preview iframe (the docs-site path, unit tests) the
334
- * channel never receives anything, and consumers keep seeing the
335
- * initial values baked into the virtual module at build time.
157
+ * For dev-time updates this module subscribes to `TOKENS_UPDATED_EVENT`
158
+ * on Storybook's channel (which the addon preview re-broadcasts from its
159
+ * own HMR listener) and exposes the latest snapshot via
160
+ * `useSyncExternalStore`, so hooks re-render in place on each token save.
336
161
  */
337
162
  const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
338
163
  let snapshot = {
339
- axes,
340
- presets,
341
- diagnostics,
342
- css,
343
- cssVarPrefix,
344
- listing: listing ?? {},
345
- tokenGraph,
346
- defaultTuple: defaultTuple ?? {},
164
+ axes: [],
165
+ presets: [],
166
+ diagnostics: [],
167
+ css: "",
168
+ cssVarPrefix: "",
169
+ listing: {},
170
+ tokenGraph: {
171
+ nodes: {},
172
+ axes: [],
173
+ axisDefaults: {},
174
+ axisContexts: {}
175
+ },
176
+ defaultTuple: {},
347
177
  version: 0
348
178
  };
349
179
  const listeners = /* @__PURE__ */ new Set();
350
180
  let subscribed = false;
181
+ function applyPatch(patch) {
182
+ snapshot = {
183
+ axes: patch.axes ?? snapshot.axes,
184
+ presets: patch.presets ?? snapshot.presets,
185
+ diagnostics: patch.diagnostics ?? snapshot.diagnostics,
186
+ css: patch.css ?? snapshot.css,
187
+ cssVarPrefix: patch.cssVarPrefix ?? snapshot.cssVarPrefix,
188
+ listing: patch.listing ?? snapshot.listing,
189
+ tokenGraph: patch.tokenGraph ?? snapshot.tokenGraph,
190
+ defaultTuple: patch.defaultTuple ?? snapshot.defaultTuple,
191
+ version: snapshot.version + 1
192
+ };
193
+ for (const cb of listeners) cb();
194
+ }
195
+ /**
196
+ * Seed the initial token snapshot. The addon preview calls this once at
197
+ * init with the build-time `virtual:swatchbook/tokens` data. Keeping the
198
+ * virtual-module read on the addon side (the package that owns it) lets
199
+ * blocks import cleanly without it. No-op fields fall back to the current
200
+ * snapshot, so a partial source is safe.
201
+ */
202
+ function registerTokenSource(source) {
203
+ applyPatch(source);
204
+ }
351
205
  function ensureSubscribed() {
352
206
  if (subscribed || typeof window === "undefined") return;
353
207
  subscribed = true;
354
208
  addons.getChannel().on(TOKENS_UPDATED_EVENT, (payload) => {
355
- snapshot = {
356
- axes: payload.axes ?? snapshot.axes,
357
- presets: payload.presets ?? snapshot.presets,
358
- diagnostics: payload.diagnostics ?? snapshot.diagnostics,
359
- css: payload.css ?? snapshot.css,
360
- cssVarPrefix: payload.cssVarPrefix ?? snapshot.cssVarPrefix,
361
- listing: payload.listing ?? snapshot.listing,
362
- tokenGraph: payload.tokenGraph ?? snapshot.tokenGraph,
363
- defaultTuple: payload.defaultTuple ?? snapshot.defaultTuple,
364
- version: snapshot.version + 1
365
- };
366
- for (const cb of listeners) cb();
209
+ applyPatch(payload);
367
210
  });
368
211
  }
369
212
  ensureSubscribed();
@@ -385,33 +228,24 @@ function useTokenSnapshot() {
385
228
  }
386
229
  //#endregion
387
230
  //#region src/internal/use-project.ts
231
+ function computeVarianceByPath(graph) {
232
+ if (!graph) return {};
233
+ const out = {};
234
+ for (const path of listPaths(graph)) out[path] = getVariance(graph, path);
235
+ return out;
236
+ }
388
237
  function ensureStylesheet(css) {
389
238
  ensureStyleElement(SWATCHBOOK_STYLE_ELEMENT_ID, css);
390
239
  }
391
- function defaultTuple$1(axes) {
240
+ function defaultTuple(axes) {
392
241
  const out = {};
393
242
  for (const axis of axes) out[axis.name] = axis.default;
394
243
  return out;
395
244
  }
396
- /**
397
- * Build a `resolveAt` accessor backed by the token graph. Returns an
398
- * empty resolver when no graph is present (test stubs, partial
399
- * snapshots). Stable identity when memoized on `tokenGraph` — the
400
- * graph is a module-level virtual-module export so its reference stays
401
- * constant for the lifetime of the iframe.
402
- */
403
245
  function makeResolveAt(graph) {
404
246
  if (!graph) return () => ({});
405
247
  return (tuple) => resolveAllAt(graph, tuple);
406
248
  }
407
- /**
408
- * Build the `resolveAt` accessor for a snapshot. Prefers the
409
- * snapshot's own `resolveAt` (the addon's preview decorator
410
- * pre-builds one at module load — see `previewResolveAt` in
411
- * `packages/addon/src/preview.tsx`), otherwise builds one from
412
- * `tokenGraph`. Hand-built snapshots can omit `resolveAt`;
413
- * the graph-backed fallback covers them.
414
- */
415
249
  function snapshotResolveAt(snapshot) {
416
250
  if (snapshot.resolveAt) return snapshot.resolveAt;
417
251
  return makeResolveAt(snapshot.tokenGraph);
@@ -441,12 +275,7 @@ function useProject() {
441
275
  if (!snapshot) return null;
442
276
  return snapshotResolveAt(snapshot);
443
277
  }, [tokenGraph, activeTheme]);
444
- const derivedVarianceByPath = useMemo(() => {
445
- if (!tokenGraph) return {};
446
- const out = {};
447
- for (const path of listPaths(tokenGraph)) out[path] = getVariance(tokenGraph, path);
448
- return out;
449
- }, [tokenGraph]);
278
+ const derivedVarianceByPath = useMemo(() => computeVarianceByPath(tokenGraph), [tokenGraph]);
450
279
  const providerData = useMemo(() => {
451
280
  if (!snapshot || !resolveAt || !axes || !activeAxes) return null;
452
281
  return {
@@ -458,12 +287,6 @@ function useProject() {
458
287
  cssVarPrefix: cssVarPrefix ?? "",
459
288
  listing: listing ?? {},
460
289
  varianceByPath: derivedVarianceByPath,
461
- tokenGraph: tokenGraph ?? {
462
- nodes: {},
463
- axes: [],
464
- axisDefaults: {},
465
- axisContexts: {}
466
- },
467
290
  resolveAt
468
291
  };
469
292
  }, [
@@ -482,37 +305,24 @@ function useProject() {
482
305
  return providerData ?? fallback;
483
306
  }
484
307
  function useVirtualModuleFallback(enabled) {
485
- const contextPermutation = useActiveTheme();
308
+ const contextThemeName = useActiveTheme();
486
309
  const contextAxes = useActiveAxes();
487
310
  const channelGlobals = useChannelGlobals();
488
- /**
489
- * Subscribe to the live token snapshot rather than reading the virtual
490
- * module's module-level exports directly. Initial values come from
491
- * `virtual:swatchbook/tokens` at load time; subsequent dev-time edits
492
- * flow through the addon's HMR channel and update this snapshot in
493
- * place so blocks re-render without a full preview reload.
494
- */
495
311
  const tokens = useTokenSnapshot();
496
312
  useEffect(() => {
497
313
  if (!enabled) return;
498
314
  ensureStylesheet(tokens.css);
499
315
  }, [enabled, tokens.css]);
500
316
  const activeAxes = useMemo(() => {
501
- return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple$1(tokens.axes);
317
+ return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple(tokens.axes);
502
318
  }, [
503
319
  contextAxes,
504
320
  channelGlobals.axes,
505
321
  tokens.axes
506
322
  ]);
507
- const activeTheme = contextPermutation || tupleToName(tokens.axes, activeAxes);
323
+ const activeTheme = contextThemeName || tupleToName(tokens.axes, activeAxes);
508
324
  const resolveAt = useMemo(() => makeResolveAt(tokens.tokenGraph), [tokens.tokenGraph]);
509
- const fallbackVarianceByPath = useMemo(() => {
510
- const graph = tokens.tokenGraph;
511
- if (!graph) return {};
512
- const out = {};
513
- for (const path of listPaths(graph)) out[path] = getVariance(graph, path);
514
- return out;
515
- }, [tokens.tokenGraph]);
325
+ const fallbackVarianceByPath = useMemo(() => computeVarianceByPath(tokens.tokenGraph), [tokens.tokenGraph]);
516
326
  return useMemo(() => ({
517
327
  activeTheme,
518
328
  activeAxes,
@@ -522,7 +332,6 @@ function useVirtualModuleFallback(enabled) {
522
332
  cssVarPrefix: tokens.cssVarPrefix,
523
333
  listing: tokens.listing,
524
334
  varianceByPath: fallbackVarianceByPath,
525
- tokenGraph: tokens.tokenGraph,
526
335
  resolveAt
527
336
  }), [
528
337
  activeTheme,
@@ -532,7 +341,6 @@ function useVirtualModuleFallback(enabled) {
532
341
  tokens.cssVarPrefix,
533
342
  tokens.listing,
534
343
  fallbackVarianceByPath,
535
- tokens.tokenGraph,
536
344
  resolveAt
537
345
  ]);
538
346
  }
@@ -590,6 +398,32 @@ function BorderSample({ path }) {
590
398
  });
591
399
  }
592
400
  //#endregion
401
+ //#region src/internal/composite-sample-format.ts
402
+ /**
403
+ * Display a composite sub-field dimension (shadow offset / blur / spread,
404
+ * border width, …) in the preview tables. Renders `—` for a missing
405
+ * sub-field and falls back to JSON for shapes it doesn't recognize.
406
+ *
407
+ * Distinct from `format-token-value`'s internal `formatDimension`, which
408
+ * formats a token's top-level value and has no `—` placeholder — these are
409
+ * the per-layer sample formatters shared by `ShadowPreview` + `BorderPreview`.
410
+ */
411
+ function formatDimension$1(raw) {
412
+ if (raw == null) return "—";
413
+ if (typeof raw === "number") return String(raw);
414
+ if (typeof raw === "string") return raw;
415
+ if (typeof raw === "object") {
416
+ const v = raw;
417
+ if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
418
+ }
419
+ return JSON.stringify(raw);
420
+ }
421
+ /** Display a composite sub-field color via the active format; `—` when absent. */
422
+ function formatSubColor(raw, format) {
423
+ if (raw == null) return "—";
424
+ return formatColor(raw, format).value;
425
+ }
426
+ //#endregion
593
427
  //#region src/internal/data-attr.ts
594
428
  /**
595
429
  * Marker attribute set on every block wrapper. Retained as a stable hook
@@ -597,14 +431,6 @@ function BorderSample({ path }) {
597
431
  * override block chrome without relying on hashed class names).
598
432
  */
599
433
  const BLOCK_ATTR = "data-swatchbook-block";
600
- /**
601
- * Opt-out class that Storybook's `.sbdocs` stylesheet uses to self-exclude
602
- * on MDX docs pages — every `.sbdocs` house rule is wrapped in
603
- * `:not(.sb-unstyled, .sb-unstyled *)`, so any descendant of a `.sb-unstyled`
604
- * container is left alone. Stamped onto every block wrapper so blocks
605
- * render identically in MDX docs and regular stories without fighting
606
- * cascade specificity.
607
- */
608
434
  const WRAPPER_CLASSES = "sb-unstyled sb-block";
609
435
  /**
610
436
  * Spread helper for the common block wrapper. Returns:
@@ -622,7 +448,7 @@ const WRAPPER_CLASSES = "sb-unstyled sb-block";
622
448
  * MDX docs house styles self-exclude the subtree, plus `sb-block`
623
449
  * which carries the shared chrome from `internal/styles.css`.
624
450
  */
625
- function themeAttrs(prefix, tuple) {
451
+ function blockWrapperAttrs(prefix, tuple) {
626
452
  return {
627
453
  ...perAxisAttrs(prefix, tuple),
628
454
  [BLOCK_ATTR]: "",
@@ -728,29 +554,14 @@ function toMagnitude(v) {
728
554
  }
729
555
  return NaN;
730
556
  }
731
- /**
732
- * Coerce a possibly-null/undefined number to 0 — `coords` returns
733
- * `(number | null)[]` and `noUncheckedIndexedAccess` adds `undefined`
734
- * on top. `typeof` narrows the union for the comparator below.
735
- */
736
557
  function safeNumber(v) {
737
558
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
738
559
  }
739
560
  function colorKey(v) {
740
- if (!v || typeof v !== "object") return null;
561
+ const color = parseColor(v);
562
+ if (!color) return null;
741
563
  try {
742
- const c = v;
743
- let source;
744
- if (typeof c.hex === "string") source = c.hex;
745
- else if (typeof c.colorSpace === "string") {
746
- const channels = Array.isArray(c.components) ? c.components : Array.isArray(c.channels) ? c.channels : void 0;
747
- if (!channels) return null;
748
- source = {
749
- space: c.colorSpace,
750
- coords: channels
751
- };
752
- } else return null;
753
- const [l, chroma, h] = new Color(source).to("oklch").coords;
564
+ const [l, chroma, h] = color.to("oklch").coords;
754
565
  return {
755
566
  l: safeNumber(l),
756
567
  c: safeNumber(chroma),
@@ -768,20 +579,6 @@ function toDisplayable(v) {
768
579
  }
769
580
  //#endregion
770
581
  //#region src/BorderPreview.tsx
771
- function formatDimension$2(raw) {
772
- if (raw == null) return "—";
773
- if (typeof raw === "number") return String(raw);
774
- if (typeof raw === "string") return raw;
775
- if (typeof raw === "object") {
776
- const v = raw;
777
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
778
- }
779
- return JSON.stringify(raw);
780
- }
781
- function formatSubColor$1(raw, format) {
782
- if (raw == null) return "—";
783
- return formatColor(raw, format).value;
784
- }
785
582
  function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
786
583
  const project = useProject();
787
584
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
@@ -807,14 +604,14 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
807
604
  ]);
808
605
  const captionText = caption ?? `${rows.length} border${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
809
606
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
810
- ...themeAttrs(cssVarPrefix, activeAxes),
607
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
811
608
  children: /* @__PURE__ */ jsx("div", {
812
609
  className: "sb-block__empty",
813
610
  children: "No border tokens match this filter."
814
611
  })
815
612
  });
816
613
  return /* @__PURE__ */ jsxs("div", {
817
- ...themeAttrs(cssVarPrefix, activeAxes),
614
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
818
615
  children: [/* @__PURE__ */ jsx("div", {
819
616
  className: "sb-block__caption",
820
617
  children: captionText
@@ -842,7 +639,7 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
842
639
  className: "sb-border-preview__breakdown-key",
843
640
  children: "width"
844
641
  }),
845
- /* @__PURE__ */ jsx("span", { children: formatDimension$2(row.value.width) }),
642
+ /* @__PURE__ */ jsx("span", { children: formatDimension$1(row.value.width) }),
846
643
  /* @__PURE__ */ jsx("span", {
847
644
  className: "sb-border-preview__breakdown-key",
848
645
  children: "style"
@@ -852,7 +649,7 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
852
649
  className: "sb-border-preview__breakdown-key",
853
650
  children: "color"
854
651
  }),
855
- /* @__PURE__ */ jsx("span", { children: formatSubColor$1(row.value.color, colorFormat) })
652
+ /* @__PURE__ */ jsx("span", { children: formatSubColor(row.value.color, colorFormat) })
856
653
  ]
857
654
  })
858
655
  ]
@@ -861,10 +658,6 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
861
658
  }
862
659
  //#endregion
863
660
  //#region src/ColorPalette.tsx
864
- /**
865
- * Count segments in the filter before the first glob (`*` / `**`).
866
- * `color.*` → 2; `color.surface.*` → 3; `color` → 1; undefined → 0.
867
- */
868
661
  function fixedPrefixLength(filter) {
869
662
  if (!filter) return 0;
870
663
  const segments = filter.split(".");
@@ -922,14 +715,14 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
922
715
  const totalCount = groups.reduce((acc, [, swatches]) => acc + swatches.length, 0);
923
716
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
924
717
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
925
- ...themeAttrs(cssVarPrefix, activeAxes),
718
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
926
719
  children: /* @__PURE__ */ jsx("div", {
927
720
  className: "sb-block__empty",
928
721
  children: "No color tokens match this filter."
929
722
  })
930
723
  });
931
724
  return /* @__PURE__ */ jsxs("div", {
932
- ...themeAttrs(cssVarPrefix, activeAxes),
725
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
933
726
  children: [/* @__PURE__ */ jsx("div", {
934
727
  className: "sb-block__caption",
935
728
  children: captionText
@@ -1115,14 +908,14 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1115
908
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleGroups.length} matching "${query.trim()}"` : "";
1116
909
  const captionText = caption ?? `${totalTokens} color${totalTokens === 1 ? "" : "s"} across ${groups.length} group${groups.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
1117
910
  if (groups.length === 0) return /* @__PURE__ */ jsx("div", {
1118
- ...themeAttrs(cssVarPrefix, activeAxes),
911
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1119
912
  children: /* @__PURE__ */ jsx("div", {
1120
913
  className: "sb-block__empty",
1121
914
  children: "No color tokens match this filter."
1122
915
  })
1123
916
  });
1124
917
  return /* @__PURE__ */ jsxs("div", {
1125
- ...themeAttrs(cssVarPrefix, activeAxes),
918
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1126
919
  children: [searchable && /* @__PURE__ */ jsx("div", {
1127
920
  className: "sb-color-table__search",
1128
921
  children: /* @__PURE__ */ jsx("input", {
@@ -1404,13 +1197,6 @@ function ValueCell({ value, label, children }) {
1404
1197
  ]
1405
1198
  });
1406
1199
  }
1407
- /**
1408
- * Pick the value + gamut flag to display in the single Value column based
1409
- * on the active color-format context. We pre-compute hex/hsl/oklch for the
1410
- * expanded sub-table regardless; the extras (`rgb`, `raw`) take a fresh
1411
- * `formatColor` pass. Keeps the hot path fast while staying honest about
1412
- * out-of-gamut warnings per-format.
1413
- */
1414
1200
  function pickActiveFormat(raw, colorFormat, hex, hsl, oklch) {
1415
1201
  switch (colorFormat) {
1416
1202
  case "hex": return {
@@ -1454,30 +1240,11 @@ function buildVariantDefs(variants) {
1454
1240
  displayOrder
1455
1241
  };
1456
1242
  }
1457
- /**
1458
- * Position of a label within a group — the `base` entry always sorts first,
1459
- * then declared labels in the order the caller wrote them in the `variants`
1460
- * prop. Unknown labels (shouldn't happen in practice) fall to the end.
1461
- */
1462
1243
  function orderIndex(label, defs) {
1463
1244
  if (label === BASE_LABEL) return -1;
1464
1245
  const idx = defs.displayOrder.indexOf(label);
1465
1246
  return idx >= 0 ? idx : Number.POSITIVE_INFINITY;
1466
1247
  }
1467
- /**
1468
- * Resolve the variant label + base path for a token, if any. The leaf
1469
- * (last dot-segment) must either equal the suffix outright (dot-segment
1470
- * form: `hi.disabled` matches suffix `disabled`) or end in `-<suffix>`
1471
- * (hyphen-tail form: `hi-d` matches `d`). The leading hyphen is required
1472
- * for the tail form so suffix `0` can't hit `neutral-900` by character.
1473
- *
1474
- * Returned `basePath` is what gets used as the grouping key:
1475
- * - Dot-segment match → parent path (drop the last dot-segment)
1476
- * - Hyphen-tail match → same dot-depth, leaf with `-<suffix>` stripped
1477
- *
1478
- * Entries in `matchOrder` are pre-sorted longest-first, so `h-dark` wins
1479
- * over `dark` for a path ending in `-h-dark`.
1480
- */
1481
1248
  function matchVariant(path, matchOrder) {
1482
1249
  if (matchOrder.length === 0) return void 0;
1483
1250
  const segments = path.split(".");
@@ -1552,7 +1319,7 @@ function Diagnostics({ caption } = {}) {
1552
1319
  const summary = useMemo(() => summarize(diagnostics), [diagnostics]);
1553
1320
  const headingText = caption ?? `Diagnostics · ${summary.text}`;
1554
1321
  return /* @__PURE__ */ jsx("div", {
1555
- ...themeAttrs(cssVarPrefix, activeAxes),
1322
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1556
1323
  "data-testid": "diagnostics",
1557
1324
  children: /* @__PURE__ */ jsxs("details", {
1558
1325
  open: summary.hasErrorsOrWarnings,
@@ -1583,8 +1350,26 @@ function Diagnostics({ caption } = {}) {
1583
1350
  });
1584
1351
  }
1585
1352
  //#endregion
1353
+ //#region src/dimension-scale/dimension-px.ts
1354
+ /**
1355
+ * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
1356
+ * purpose of deciding whether to cap the rendered size. Returns `NaN` for
1357
+ * units we can't reasonably approximate (ex / ch / %), which the caller
1358
+ * treats as "render at cssVar but don't cap".
1359
+ */
1360
+ function toPixels(raw) {
1361
+ if (raw == null || typeof raw !== "object") return NaN;
1362
+ const v = raw;
1363
+ if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1364
+ switch (v.unit) {
1365
+ case "px": return v.value;
1366
+ case "rem":
1367
+ case "em": return v.value * 16;
1368
+ default: return NaN;
1369
+ }
1370
+ }
1371
+ //#endregion
1586
1372
  //#region src/dimension-scale/DimensionBar.tsx
1587
- const MAX_RENDER_PX$1 = 480;
1588
1373
  const styles$1 = {
1589
1374
  bar: {
1590
1375
  height: 14,
@@ -1605,31 +1390,14 @@ const styles$1 = {
1605
1390
  minHeight: 1
1606
1391
  }
1607
1392
  };
1608
- /**
1609
- * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
1610
- * purpose of deciding whether to cap the rendered bar. Returns `NaN` for
1611
- * units we can't reasonably approximate (ex / ch / %), which the caller
1612
- * treats as "render at cssVar but don't cap".
1613
- */
1614
- function toPixels$1(raw) {
1615
- if (raw == null || typeof raw !== "object") return NaN;
1616
- const v = raw;
1617
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1618
- switch (v.unit) {
1619
- case "px": return v.value;
1620
- case "rem":
1621
- case "em": return v.value * 16;
1622
- default: return NaN;
1623
- }
1624
- }
1625
- function DimensionBar({ path, kind = "length" }) {
1393
+ function DimensionBar({ path, visual = "length" }) {
1626
1394
  const project = useProject();
1627
1395
  const { resolved } = project;
1628
1396
  const cssVar = resolveCssVar(path, project);
1629
1397
  const token = resolved[path];
1630
- const pxValue = toPixels$1(token?.$value);
1631
- const cappedValue = Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX$1 ? `${MAX_RENDER_PX$1}px` : cssVar;
1632
- switch (kind) {
1398
+ const pxValue = toPixels(token?.$value);
1399
+ const cappedValue = Number.isFinite(pxValue) && pxValue > 480 ? `480px` : cssVar;
1400
+ switch (visual) {
1633
1401
  case "radius": return /* @__PURE__ */ jsx("div", {
1634
1402
  style: {
1635
1403
  ...styles$1.radiusSample,
@@ -1674,7 +1442,7 @@ function formatTokenValue(value, $type, colorFormat, listingEntry) {
1674
1442
  switch ($type) {
1675
1443
  case "color": return formatColor(value, colorFormat).value;
1676
1444
  case "dimension":
1677
- case "duration": return formatDimension$1(value);
1445
+ case "duration": return formatDimension(value);
1678
1446
  case "fontFamily": return formatFontFamily$1(value);
1679
1447
  case "fontWeight":
1680
1448
  case "lineHeight":
@@ -1688,7 +1456,7 @@ function formatTokenValue(value, $type, colorFormat, listingEntry) {
1688
1456
  default: return formatUnknown(value);
1689
1457
  }
1690
1458
  }
1691
- function formatDimension$1(v) {
1459
+ function formatDimension(v) {
1692
1460
  if (typeof v === "string" || typeof v === "number") return String(v);
1693
1461
  if (v && typeof v === "object") {
1694
1462
  const d = v;
@@ -1717,7 +1485,7 @@ function formatStrokeStyle(v) {
1717
1485
  if (v && typeof v === "object") {
1718
1486
  const s = v;
1719
1487
  const parts = ["dashed"];
1720
- if (Array.isArray(s.dashArray)) parts.push(s.dashArray.map((n) => formatDimension$1(n)).join(" "));
1488
+ if (Array.isArray(s.dashArray)) parts.push(s.dashArray.map((n) => formatDimension(n)).join(" "));
1721
1489
  if (typeof s.lineCap === "string") parts.push(s.lineCap);
1722
1490
  return parts.join(" · ");
1723
1491
  }
@@ -1728,10 +1496,10 @@ function formatShadow(v, colorFormat) {
1728
1496
  if (!layer || typeof layer !== "object") return formatUnknown(layer);
1729
1497
  const s = layer;
1730
1498
  const pieces = [
1731
- formatDimension$1(s.offsetX),
1732
- formatDimension$1(s.offsetY),
1733
- formatDimension$1(s.blur),
1734
- formatDimension$1(s.spread),
1499
+ formatDimension(s.offsetX),
1500
+ formatDimension(s.offsetY),
1501
+ formatDimension(s.blur),
1502
+ formatDimension(s.spread),
1735
1503
  formatColor(s.color, colorFormat).value
1736
1504
  ].filter((p) => p !== "");
1737
1505
  if (s.inset) pieces.push("inset");
@@ -1742,7 +1510,7 @@ function formatBorder(v, colorFormat) {
1742
1510
  if (!v || typeof v !== "object") return formatUnknown(v);
1743
1511
  const b = v;
1744
1512
  return [
1745
- formatDimension$1(b.width),
1513
+ formatDimension(b.width),
1746
1514
  formatPrimitive$1(b.style),
1747
1515
  formatColor(b.color, colorFormat).value
1748
1516
  ].filter((p) => p !== "").join(" ");
@@ -1758,19 +1526,7 @@ function formatUnknown(v) {
1758
1526
  }
1759
1527
  //#endregion
1760
1528
  //#region src/DimensionScale.tsx
1761
- const MAX_RENDER_PX = 480;
1762
- function toPixels(raw) {
1763
- if (raw == null || typeof raw !== "object") return NaN;
1764
- const v = raw;
1765
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1766
- switch (v.unit) {
1767
- case "px": return v.value;
1768
- case "rem":
1769
- case "em": return v.value * 16;
1770
- default: return NaN;
1771
- }
1772
- }
1773
- function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
1529
+ function DimensionScale({ filter, visual = "length", caption, sortBy = "value", sortDir = "asc" }) {
1774
1530
  const project = useProject();
1775
1531
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1776
1532
  const rows = useMemo(() => {
@@ -1787,7 +1543,7 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1787
1543
  cssVar: resolveCssVar(path, project),
1788
1544
  displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
1789
1545
  pxValue,
1790
- capped: Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX
1546
+ capped: Number.isFinite(pxValue) && pxValue > 480
1791
1547
  };
1792
1548
  });
1793
1549
  }, [
@@ -1799,14 +1555,14 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1799
1555
  ]);
1800
1556
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1801
1557
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1802
- ...themeAttrs(cssVarPrefix, activeAxes),
1558
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1803
1559
  children: /* @__PURE__ */ jsx("div", {
1804
1560
  className: "sb-block__empty",
1805
1561
  children: "No dimension tokens match this filter."
1806
1562
  })
1807
1563
  });
1808
1564
  return /* @__PURE__ */ jsxs("div", {
1809
- ...themeAttrs(cssVarPrefix, activeAxes),
1565
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1810
1566
  children: [/* @__PURE__ */ jsx("div", {
1811
1567
  className: "sb-block__caption",
1812
1568
  children: captionText
@@ -1827,12 +1583,12 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1827
1583
  className: "sb-dimension-scale__visual-cell",
1828
1584
  children: [/* @__PURE__ */ jsx(DimensionBar, {
1829
1585
  path: row.path,
1830
- kind
1586
+ visual
1831
1587
  }), row.capped && /* @__PURE__ */ jsxs("span", {
1832
1588
  className: "sb-dimension-scale__cap",
1833
1589
  children: [
1834
1590
  "capped at ",
1835
- MAX_RENDER_PX,
1591
+ 480,
1836
1592
  "px"
1837
1593
  ]
1838
1594
  })]
@@ -1846,13 +1602,13 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1846
1602
  });
1847
1603
  }
1848
1604
  //#endregion
1849
- //#region src/FontFamilySample.tsx
1605
+ //#region src/FontFamilyPreview.tsx
1850
1606
  function stackString(raw) {
1851
1607
  if (typeof raw === "string") return raw;
1852
1608
  if (Array.isArray(raw)) return raw.map(String).join(", ");
1853
1609
  return "";
1854
1610
  }
1855
- function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1611
+ function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1856
1612
  const project = useProject();
1857
1613
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1858
1614
  const rows = useMemo(() => {
@@ -1876,14 +1632,14 @@ function FontFamilySample({ filter, sample = "The quick brown fox jumps over the
1876
1632
  ]);
1877
1633
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1878
1634
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1879
- ...themeAttrs(cssVarPrefix, activeAxes),
1635
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1880
1636
  children: /* @__PURE__ */ jsx("div", {
1881
1637
  className: "sb-block__empty",
1882
1638
  children: "No fontFamily tokens match this filter."
1883
1639
  })
1884
1640
  });
1885
1641
  return /* @__PURE__ */ jsxs("div", {
1886
- ...themeAttrs(cssVarPrefix, activeAxes),
1642
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1887
1643
  children: [/* @__PURE__ */ jsx("div", {
1888
1644
  className: "sb-block__caption",
1889
1645
  children: captionText
@@ -1963,14 +1719,14 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
1963
1719
  ]);
1964
1720
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1965
1721
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1966
- ...themeAttrs(cssVarPrefix, activeAxes),
1722
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1967
1723
  children: /* @__PURE__ */ jsx("div", {
1968
1724
  className: "sb-block__empty",
1969
1725
  children: "No fontWeight tokens match this filter."
1970
1726
  })
1971
1727
  });
1972
1728
  return /* @__PURE__ */ jsxs("div", {
1973
- ...themeAttrs(cssVarPrefix, activeAxes),
1729
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
1974
1730
  children: [/* @__PURE__ */ jsx("div", {
1975
1731
  className: "sb-block__caption",
1976
1732
  children: captionText
@@ -2006,14 +1762,10 @@ function asStops(raw) {
2006
1762
  if (!Array.isArray(raw)) return [];
2007
1763
  return raw;
2008
1764
  }
2009
- const pct = (n) => `${(n * 100).toFixed(3)}%`;
2010
1765
  function stopCssColor(stop) {
2011
- const color = stop.color;
2012
- if (!color || !Array.isArray(color.components) || color.components.length < 3) return "transparent";
2013
- const [r, g, b] = color.components;
2014
- if (r === void 0 || g === void 0 || b === void 0) return "transparent";
2015
- const alpha = color.alpha ?? 1;
2016
- return alpha === 1 ? `rgb(${pct(r)} ${pct(g)} ${pct(b)})` : `rgb(${pct(r)} ${pct(g)} ${pct(b)} / ${alpha})`;
1766
+ const color = parseColor(stop.color);
1767
+ if (!color) return "transparent";
1768
+ return color.toString();
2017
1769
  }
2018
1770
  function stopKey(path, stop, fallback) {
2019
1771
  return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
@@ -2047,14 +1799,14 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2047
1799
  ]);
2048
1800
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2049
1801
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2050
- ...themeAttrs(cssVarPrefix, activeAxes),
1802
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2051
1803
  children: /* @__PURE__ */ jsx("div", {
2052
1804
  className: "sb-block__empty",
2053
1805
  children: "No gradient tokens match this filter."
2054
1806
  })
2055
1807
  });
2056
1808
  return /* @__PURE__ */ jsxs("div", {
2057
- ...themeAttrs(cssVarPrefix, activeAxes),
1809
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2058
1810
  children: [/* @__PURE__ */ jsx("div", {
2059
1811
  className: "sb-block__caption",
2060
1812
  children: captionText
@@ -2104,14 +1856,6 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2104
1856
  }
2105
1857
  //#endregion
2106
1858
  //#region src/internal/prefers-reduced-motion.ts
2107
- /**
2108
- * True when rendering inside Chromatic's snapshot runner. Chromatic's
2109
- * browser ships a recognisable user-agent string; checked here so
2110
- * motion-looping components can fall back to their static state for
2111
- * deterministic snapshots. Per-component detection rather than the
2112
- * global `chromatic.prefersReducedMotion: true` parameter — that
2113
- * parameter is incompatible with Chromatic's verification parser.
2114
- */
2115
1859
  function isChromatic() {
2116
1860
  if (typeof navigator === "undefined") return false;
2117
1861
  return navigator.userAgent.includes("Chromatic");
@@ -2332,14 +2076,14 @@ function MotionPreview({ filter, caption }) {
2332
2076
  ]);
2333
2077
  const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2334
2078
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2335
- ...themeAttrs(cssVarPrefix, activeAxes),
2079
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2336
2080
  children: /* @__PURE__ */ jsx("div", {
2337
2081
  className: "sb-block__empty",
2338
2082
  children: "No motion tokens match this filter."
2339
2083
  })
2340
2084
  });
2341
2085
  return /* @__PURE__ */ jsxs("div", {
2342
- ...themeAttrs(cssVarPrefix, activeAxes),
2086
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2343
2087
  children: [
2344
2088
  /* @__PURE__ */ jsx("div", {
2345
2089
  className: "sb-block__caption",
@@ -2442,7 +2186,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2442
2186
  ]);
2443
2187
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2444
2188
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2445
- ...themeAttrs(cssVarPrefix, activeAxes),
2189
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2446
2190
  children: /* @__PURE__ */ jsx("div", {
2447
2191
  className: "sb-block__empty",
2448
2192
  children: "No opacity tokens match this filter."
@@ -2450,7 +2194,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2450
2194
  });
2451
2195
  const sampleColorVar = resolveCssVar(sampleColor, project);
2452
2196
  return /* @__PURE__ */ jsxs("div", {
2453
- ...themeAttrs(cssVarPrefix, activeAxes),
2197
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2454
2198
  children: [/* @__PURE__ */ jsx("div", {
2455
2199
  className: "sb-block__caption",
2456
2200
  children: captionText
@@ -2534,27 +2278,13 @@ function ShadowSample({ path }) {
2534
2278
  }
2535
2279
  //#endregion
2536
2280
  //#region src/ShadowPreview.tsx
2537
- function formatDimension(raw) {
2538
- if (raw == null) return "—";
2539
- if (typeof raw === "number") return String(raw);
2540
- if (typeof raw === "string") return raw;
2541
- if (typeof raw === "object") {
2542
- const v = raw;
2543
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
2544
- }
2545
- return JSON.stringify(raw);
2546
- }
2547
- function formatSubColor(raw, format) {
2548
- if (raw == null) return "—";
2549
- return formatColor(raw, format).value;
2550
- }
2551
2281
  function asLayers(raw) {
2552
2282
  if (Array.isArray(raw)) return raw;
2553
2283
  if (raw && typeof raw === "object") return [raw];
2554
2284
  return [];
2555
2285
  }
2556
2286
  function layerKey(path, layer, fallback) {
2557
- return `${path}|${`${formatDimension(layer.offsetX)},${formatDimension(layer.offsetY)}`}|${formatDimension(layer.blur)}|${formatDimension(layer.spread)}|${fallback}`;
2287
+ return `${path}|${`${formatDimension$1(layer.offsetX)},${formatDimension$1(layer.offsetY)}`}|${formatDimension$1(layer.blur)}|${formatDimension$1(layer.spread)}|${fallback}`;
2558
2288
  }
2559
2289
  function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2560
2290
  const project = useProject();
@@ -2581,14 +2311,14 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2581
2311
  ]);
2582
2312
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2583
2313
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2584
- ...themeAttrs(cssVarPrefix, activeAxes),
2314
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2585
2315
  children: /* @__PURE__ */ jsx("div", {
2586
2316
  className: "sb-block__empty",
2587
2317
  children: "No shadow tokens match this filter."
2588
2318
  })
2589
2319
  });
2590
2320
  return /* @__PURE__ */ jsxs("div", {
2591
- ...themeAttrs(cssVarPrefix, activeAxes),
2321
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2592
2322
  children: [/* @__PURE__ */ jsx("div", {
2593
2323
  className: "sb-block__caption",
2594
2324
  children: captionText
@@ -2625,9 +2355,9 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2625
2355
  function renderLayer(layer, format) {
2626
2356
  if (!layer) return [];
2627
2357
  const entries = [
2628
- ["offset", `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`],
2629
- ["blur", formatDimension(layer.blur)],
2630
- ["spread", formatDimension(layer.spread)],
2358
+ ["offset", `${formatDimension$1(layer.offsetX)} ${formatDimension$1(layer.offsetY)}`],
2359
+ ["blur", formatDimension$1(layer.blur)],
2360
+ ["spread", formatDimension$1(layer.spread)],
2631
2361
  ["color", formatSubColor(layer.color, format)]
2632
2362
  ];
2633
2363
  if (layer.inset) entries.push(["inset", String(layer.inset)]);
@@ -2654,7 +2384,7 @@ function Layer({ layer, index, total, colorFormat }) {
2654
2384
  });
2655
2385
  }
2656
2386
  //#endregion
2657
- //#region src/StrokeStyleSample.tsx
2387
+ //#region src/StrokeStylePreview.tsx
2658
2388
  const STRING_STYLES = new Set([
2659
2389
  "solid",
2660
2390
  "dashed",
@@ -2669,7 +2399,7 @@ function extractCssStyle(value) {
2669
2399
  if (typeof value === "string" && STRING_STYLES.has(value)) return value;
2670
2400
  return null;
2671
2401
  }
2672
- function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2402
+ function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2673
2403
  const project = useProject();
2674
2404
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2675
2405
  const rows = useMemo(() => {
@@ -2694,14 +2424,14 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2694
2424
  ]);
2695
2425
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2696
2426
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2697
- ...themeAttrs(cssVarPrefix, activeAxes),
2427
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2698
2428
  children: /* @__PURE__ */ jsx("div", {
2699
2429
  className: "sb-block__empty",
2700
2430
  children: "No strokeStyle tokens match this filter."
2701
2431
  })
2702
2432
  });
2703
2433
  return /* @__PURE__ */ jsxs("div", {
2704
- ...themeAttrs(cssVarPrefix, activeAxes),
2434
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2705
2435
  children: [/* @__PURE__ */ jsx("div", {
2706
2436
  className: "sb-block__caption",
2707
2437
  children: captionText
@@ -2833,15 +2563,13 @@ function AliasedByRow({ node, depth }) {
2833
2563
  function buildAliasedByTree(rootPath, resolved) {
2834
2564
  const direct = resolved[rootPath]?.aliasedBy;
2835
2565
  if (!direct || direct.length === 0) return [];
2836
- const visited = new Set([rootPath]);
2837
- return sortPaths(direct).map((p) => walk(p, resolved, visited, 1));
2566
+ return sortPaths(direct).map((p) => walk(p, resolved, new Set([rootPath]), 1));
2838
2567
  }
2839
- function walk(path, resolved, visited, depth) {
2840
- if (visited.has(path)) return {
2568
+ function walk(path, resolved, ancestors, depth) {
2569
+ if (ancestors.has(path)) return {
2841
2570
  path,
2842
2571
  children: []
2843
2572
  };
2844
- visited.add(path);
2845
2573
  const parents = resolved[path]?.aliasedBy;
2846
2574
  if (!parents || parents.length === 0) return {
2847
2575
  path,
@@ -2852,9 +2580,10 @@ function walk(path, resolved, visited, depth) {
2852
2580
  children: [],
2853
2581
  truncated: true
2854
2582
  };
2583
+ const childAncestors = new Set(ancestors).add(path);
2855
2584
  return {
2856
2585
  path,
2857
- children: sortPaths(parents).map((p) => walk(p, resolved, visited, depth + 1))
2586
+ children: sortPaths(parents).map((p) => walk(p, resolved, childAncestors, depth + 1))
2858
2587
  };
2859
2588
  }
2860
2589
  function sortPaths(paths) {
@@ -3252,12 +2981,6 @@ function formatDimensionValue(v) {
3252
2981
  }
3253
2982
  return JSON.stringify(v);
3254
2983
  }
3255
- /**
3256
- * Route sub-value colors through `formatColor` so they honor the active
3257
- * color-format dropdown, just like the standalone `<ColorPalette />` and
3258
- * `<TokenDetail />` top-line do. Returns `null` for a missing field so
3259
- * the key/value row drops out entirely.
3260
- */
3261
2984
  function formatColorSubValue(v, format) {
3262
2985
  if (v == null) return null;
3263
2986
  return formatColor(v, format).value;
@@ -3270,11 +2993,6 @@ function pickArrayAliases(v) {
3270
2993
  if (!Array.isArray(v)) return void 0;
3271
2994
  return v;
3272
2995
  }
3273
- /**
3274
- * Walk the alias chain starting from an immediate sub-value alias target.
3275
- * `aliasTarget` is the path the sub-value directly references; the target
3276
- * token's own `aliasChain` continues the walk to the primitive.
3277
- */
3278
2996
  function subValueChain(aliasTarget, resolved) {
3279
2997
  if (!aliasTarget) return void 0;
3280
2998
  const tail = (resolved?.[aliasTarget])?.aliasChain;
@@ -3294,6 +3012,35 @@ function gradientStopKey(stop, fallback) {
3294
3012
  return `stop|${stop.position ?? fallback}|${JSON.stringify(stop.color)}`;
3295
3013
  }
3296
3014
  //#endregion
3015
+ //#region src/token-detail/transition-duration.ts
3016
+ /**
3017
+ * Numeric duration (ms) the motion preview should animate over for a given
3018
+ * token type, read from its resolved `$value`. Lets the sample's toggle loop
3019
+ * match the token's real duration instead of a fixed cadence — a long token
3020
+ * (say 2s) otherwise reverses mid-move under the old hardcoded interval.
3021
+ * Returns `undefined` for types that carry no duration, so the caller can
3022
+ * fall back to its default.
3023
+ */
3024
+ function transitionDurationMs(type, rawValue) {
3025
+ if (type === "cubicBezier") return 800;
3026
+ if (type === "duration") return parseDurationMs(rawValue);
3027
+ if (type === "transition" && rawValue !== null && typeof rawValue === "object") return parseDurationMs(rawValue.duration);
3028
+ }
3029
+ function parseDurationMs(v) {
3030
+ if (typeof v === "number") return v;
3031
+ if (typeof v === "string") {
3032
+ const m = /^([\d.]+)(ms|s)?$/.exec(v.trim());
3033
+ if (!m?.[1]) return void 0;
3034
+ const n = Number(m[1]);
3035
+ if (Number.isNaN(n)) return void 0;
3036
+ return m[2] === "s" ? n * 1e3 : n;
3037
+ }
3038
+ if (v !== null && typeof v === "object") {
3039
+ const d = v;
3040
+ if (typeof d.value === "number") return d.unit === "s" ? d.value * 1e3 : d.value;
3041
+ }
3042
+ }
3043
+ //#endregion
3297
3044
  //#region src/token-detail/CompositePreview.tsx
3298
3045
  const PANGRAM = "Sphinx of black quartz, judge my vow.";
3299
3046
  const STROKE_STYLE_STRINGS = new Set([
@@ -3340,7 +3087,10 @@ function CompositePreviewContent({ type, cssVar, rawValue }) {
3340
3087
  style: { border: cssVar },
3341
3088
  "aria-hidden": true
3342
3089
  });
3343
- if (type === "transition") return /* @__PURE__ */ jsx(TransitionSample, { transition: cssVar });
3090
+ if (type === "transition") return /* @__PURE__ */ jsx(TransitionSample, {
3091
+ transition: cssVar,
3092
+ durationMs: transitionDurationMs(type, rawValue)
3093
+ });
3344
3094
  if (type === "dimension") return /* @__PURE__ */ jsx("div", {
3345
3095
  className: "sb-token-detail__dimension-track",
3346
3096
  children: /* @__PURE__ */ jsx("div", {
@@ -3349,7 +3099,10 @@ function CompositePreviewContent({ type, cssVar, rawValue }) {
3349
3099
  "aria-hidden": true
3350
3100
  })
3351
3101
  });
3352
- if (type === "duration") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left ${cssVar} ease` });
3102
+ if (type === "duration") return /* @__PURE__ */ jsx(TransitionSample, {
3103
+ transition: `left ${cssVar} ease`,
3104
+ durationMs: transitionDurationMs(type, rawValue)
3105
+ });
3353
3106
  if (type === "fontFamily") return /* @__PURE__ */ jsx("div", {
3354
3107
  className: "sb-token-detail__font-family-sample",
3355
3108
  style: { fontFamily: cssVar },
@@ -3360,13 +3113,16 @@ function CompositePreviewContent({ type, cssVar, rawValue }) {
3360
3113
  style: { fontWeight: cssVarAsNumber(cssVar) },
3361
3114
  children: "Aa"
3362
3115
  });
3363
- if (type === "cubicBezier") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left 800ms ${cssVar}` });
3116
+ if (type === "cubicBezier") return /* @__PURE__ */ jsx(TransitionSample, {
3117
+ transition: `left 800ms ${cssVar}`,
3118
+ durationMs: transitionDurationMs(type, rawValue)
3119
+ });
3364
3120
  if (type === "gradient") return /* @__PURE__ */ jsx("div", {
3365
3121
  className: "sb-token-detail__gradient-sample",
3366
3122
  style: { background: `linear-gradient(to right, ${cssVar})` },
3367
3123
  "aria-hidden": true
3368
3124
  });
3369
- if (type === "strokeStyle") return /* @__PURE__ */ jsx(StrokeStylePreview, { value: rawValue });
3125
+ if (type === "strokeStyle") return /* @__PURE__ */ jsx(StrokeStylePreview$1, { value: rawValue });
3370
3126
  if (type === "color") return /* @__PURE__ */ jsxs("div", {
3371
3127
  className: "sb-token-detail__color-swatch-row",
3372
3128
  "aria-hidden": true,
@@ -3380,7 +3136,7 @@ function CompositePreviewContent({ type, cssVar, rawValue }) {
3380
3136
  });
3381
3137
  return null;
3382
3138
  }
3383
- function StrokeStylePreview({ value }) {
3139
+ function StrokeStylePreview$1({ value }) {
3384
3140
  if (typeof value === "string" && STROKE_STYLE_STRINGS.has(value)) return /* @__PURE__ */ jsx("div", {
3385
3141
  className: "sb-token-detail__stroke-style-line",
3386
3142
  style: { borderTopStyle: value },
@@ -3431,20 +3187,23 @@ function asDashLengths(raw) {
3431
3187
  }
3432
3188
  return out;
3433
3189
  }
3434
- function TransitionSample({ transition }) {
3190
+ const DEFAULT_LOOP_MS = 1200;
3191
+ const MOTION_HOLD_MS = 400;
3192
+ function TransitionSample({ transition, durationMs }) {
3435
3193
  const reduced = usePrefersReducedMotion();
3436
3194
  const [phase, setPhase] = useState(0);
3437
3195
  useEffect(() => {
3438
3196
  if (reduced) return;
3197
+ const loopMs = durationMs === void 0 ? DEFAULT_LOOP_MS : durationMs + MOTION_HOLD_MS;
3439
3198
  const id = requestAnimationFrame(() => setPhase(1));
3440
3199
  const loop = window.setInterval(() => {
3441
3200
  setPhase((p) => p === 0 ? 1 : 0);
3442
- }, 1200);
3201
+ }, loopMs);
3443
3202
  return () => {
3444
3203
  cancelAnimationFrame(id);
3445
3204
  window.clearInterval(loop);
3446
3205
  };
3447
- }, [reduced]);
3206
+ }, [reduced, durationMs]);
3448
3207
  if (reduced) return /* @__PURE__ */ jsx("div", {
3449
3208
  className: "sb-token-detail__reduced-motion",
3450
3209
  children: "Animation suppressed by `prefers-reduced-motion: reduce`."
@@ -3579,11 +3338,6 @@ function TokenHeader({ path, heading }) {
3579
3338
  }
3580
3339
  //#endregion
3581
3340
  //#region src/token-detail/TokenUsageSnippet.tsx
3582
- /**
3583
- * DTCG `$type`s with a single canonical CSS property. Types whose value is
3584
- * applied across many properties (`dimension`, `number`, `strokeStyle`,
3585
- * `typography`) are intentionally absent — they fall back to a commented hint.
3586
- */
3587
3341
  const CSS_PROPERTY_BY_TYPE = {
3588
3342
  color: "color",
3589
3343
  shadow: "box-shadow",
@@ -3624,10 +3378,11 @@ function TokenUsageSnippet({ path }) {
3624
3378
  function TokenDetail({ path, heading }) {
3625
3379
  const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
3626
3380
  const colorFormat = useColorFormat();
3627
- const theme = themeAttrs(cssVarPrefix, activeAxes);
3381
+ const { listing } = useProject();
3382
+ const wrapperAttrs = blockWrapperAttrs(cssVarPrefix, activeAxes);
3628
3383
  if (!token) return /* @__PURE__ */ jsx("div", {
3629
- ...theme,
3630
- className: cx(theme["className"], "sb-token-detail"),
3384
+ ...wrapperAttrs,
3385
+ className: cx(wrapperAttrs["className"], "sb-token-detail"),
3631
3386
  children: /* @__PURE__ */ jsxs("div", {
3632
3387
  className: "sb-token-detail__missing",
3633
3388
  children: [
@@ -3639,14 +3394,13 @@ function TokenDetail({ path, heading }) {
3639
3394
  ]
3640
3395
  })
3641
3396
  });
3642
- const { listing } = useProject();
3643
3397
  const isColor = token.$type === "color";
3644
3398
  const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
3645
3399
  const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
3646
3400
  const outOfGamut = gamut?.outOfGamut ?? false;
3647
3401
  return /* @__PURE__ */ jsxs("div", {
3648
- ...theme,
3649
- className: cx(theme["className"], "sb-token-detail"),
3402
+ ...wrapperAttrs,
3403
+ className: cx(wrapperAttrs["className"], "sb-token-detail"),
3650
3404
  children: [
3651
3405
  /* @__PURE__ */ jsx(TokenHeader, {
3652
3406
  path,
@@ -3689,12 +3443,6 @@ function TokenDetail({ path, heading }) {
3689
3443
  }
3690
3444
  //#endregion
3691
3445
  //#region src/internal/DetailOverlay.tsx
3692
- /**
3693
- * Selector for elements the trap considers focus stops. Mirrors the
3694
- * "tabbable" set most focus-trap libraries use; the `:not(...)` clauses
3695
- * skip the panel wrapper itself (we focus it manually on mount via its
3696
- * own ref) and any explicitly-detabbed descendants.
3697
- */
3698
3446
  const FOCUSABLE_SELECTOR = [
3699
3447
  "a[href]",
3700
3448
  "button:not([disabled])",
@@ -3734,11 +3482,6 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3734
3482
  window.addEventListener("keydown", onKey);
3735
3483
  return () => window.removeEventListener("keydown", onKey);
3736
3484
  }, [onClose]);
3737
- /**
3738
- * Wrap Tab inside the panel: from the last focusable, jump to the first;
3739
- * from the first (or from the panel itself), Shift+Tab jumps to the last.
3740
- * Defers to the browser otherwise.
3741
- */
3742
3485
  const onPanelKeyDown = (e) => {
3743
3486
  if (e.key !== "Tab") return;
3744
3487
  const panel = panelRef.current;
@@ -3787,12 +3530,6 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3787
3530
  })
3788
3531
  });
3789
3532
  }
3790
- /**
3791
- * Walk up from `node` to the direct child of `<body>` that contains it.
3792
- * Returns `null` when the node isn't attached to the document (mid-mount,
3793
- * post-unmount). Used to identify which top-level branch to *not* mark
3794
- * inert when the overlay opens.
3795
- */
3796
3533
  function findBodyChildContaining(node) {
3797
3534
  let cursor = node;
3798
3535
  while (cursor && cursor.parentElement !== document.body) cursor = cursor.parentElement;
@@ -3886,11 +3623,6 @@ function flattenVisible(nodes, expanded, parentPath, out) {
3886
3623
  if (node.kind === "group" && expanded.has(node.path)) flattenVisible(node.children, expanded, node.path, out);
3887
3624
  }
3888
3625
  }
3889
- /**
3890
- * Return a pruned copy of the tree keeping only leaves whose path is in
3891
- * `matches`, plus the groups on the way to them. Every surviving group's
3892
- * path is added to `expandOut` so callers can force those groups open.
3893
- */
3894
3626
  function pruneTreeForMatches(nodes, matches, expandOut) {
3895
3627
  const out = [];
3896
3628
  for (const node of nodes) if (node.kind === "leaf") {
@@ -4092,11 +3824,11 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4092
3824
  return n;
4093
3825
  }, [visibleTree, searchExpanded]);
4094
3826
  if (tree.length === 0) return /* @__PURE__ */ jsx("div", {
4095
- ...themeAttrs(cssVarPrefix, activeAxes),
3827
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4096
3828
  children: /* @__PURE__ */ jsx(EmptyState, { children: root ? `No tokens under "${root}"${typeFilter ? ` matching ${typeLabel.slice(3)}` : ""}.` : typeFilter ? `No tokens matching ${typeLabel.slice(3)} in the active theme.` : "No tokens in the active theme." })
4097
3829
  });
4098
3830
  return /* @__PURE__ */ jsxs("div", {
4099
- ...themeAttrs(cssVarPrefix, activeAxes),
3831
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4100
3832
  children: [
4101
3833
  searchable && /* @__PURE__ */ jsx("div", {
4102
3834
  className: "sb-token-navigator__search",
@@ -4288,7 +4020,7 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4288
4020
  className: "sb-token-navigator__preview-dimension",
4289
4021
  children: /* @__PURE__ */ jsx(DimensionBar, {
4290
4022
  path,
4291
- kind: "length"
4023
+ visual: "length"
4292
4024
  })
4293
4025
  })]
4294
4026
  });
@@ -4378,14 +4110,14 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4378
4110
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
4379
4111
  const captionText = caption ?? `${rows.length} token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${type ? ` · $type=${type}` : ""}${matchSuffix} · ${activeTheme}`;
4380
4112
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4381
- ...themeAttrs(cssVarPrefix, activeAxes),
4113
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4382
4114
  children: /* @__PURE__ */ jsx("div", {
4383
4115
  className: "sb-block__empty",
4384
4116
  children: "No tokens match this filter."
4385
4117
  })
4386
4118
  });
4387
4119
  return /* @__PURE__ */ jsxs("div", {
4388
- ...themeAttrs(cssVarPrefix, activeAxes),
4120
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4389
4121
  children: [
4390
4122
  searchable && /* @__PURE__ */ jsx("div", {
4391
4123
  className: "sb-token-table__search",
@@ -4557,14 +4289,14 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
4557
4289
  ]);
4558
4290
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
4559
4291
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4560
- ...themeAttrs(cssVarPrefix, activeAxes),
4292
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4561
4293
  children: /* @__PURE__ */ jsx("div", {
4562
4294
  className: "sb-block__empty",
4563
4295
  children: "No typography tokens match this filter."
4564
4296
  })
4565
4297
  });
4566
4298
  return /* @__PURE__ */ jsxs("div", {
4567
- ...themeAttrs(cssVarPrefix, activeAxes),
4299
+ ...blockWrapperAttrs(cssVarPrefix, activeAxes),
4568
4300
  children: [/* @__PURE__ */ jsx("div", {
4569
4301
  className: "sb-block__caption",
4570
4302
  children: captionText
@@ -4587,6 +4319,6 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
4587
4319
  });
4588
4320
  }
4589
4321
  //#endregion
4590
- export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilySample, FontWeightScale, GradientPalette, MotionPreview, MotionSample, OpacityScale, ShadowPreview, ShadowSample, StrokeStyleSample, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
4322
+ export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilyPreview, FontWeightScale, GradientPalette, MotionPreview, MotionSample, OpacityScale, ShadowPreview, ShadowSample, StrokeStylePreview, SwatchbookContext, SwatchbookProvider, TOKENS_UPDATED_EVENT, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, registerTokenSource, useActiveAxes, useActiveTheme, useChannelGlobals, useColorFormat, useOptionalSwatchbookData, useSwatchbookData, useTokenSnapshot };
4591
4323
 
4592
4324
  //# sourceMappingURL=index.mjs.map