@unpunnyfuns/swatchbook-blocks 1.1.1 → 2.0.0

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,100 +1,18 @@
1
1
  import './style.css';
2
- import { COLOR_FORMATS } from "@unpunnyfuns/swatchbook-core/color-formats";
3
- import { formatColor, parseColor } from "@unpunnyfuns/swatchbook-core/format-color";
4
- import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
+ import { C as COLOR_FORMATS, S as BorderSample, _ as cssVarAsNumber, a as mergePresenters, b as useRootFontSize, c as ShadowSample, d as OpacitySwatch, f as MotionSample, g as FontWeightSpecimen, h as GradientSwatch, i as PresenterContext, l as TypeSpecimen, m as usePrefersReducedMotion, n as useProjectSource, p as resolveMotionSpec, r as DEFAULT_PRESENTERS, s as usePresenter, u as StrokeSample, v as FontFamilySpecimen, w as formatColor, x as ColorSwatch, y as DimensionSample } from "./host-e2nSv-sA.mjs";
3
+ import { parseColor } from "@unpunnyfuns/swatchbook-core/format-color";
4
+ import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { formatDimension, formatSubColor, formatTokenValue, formatTokenValue as formatTokenValue$2 } from "@unpunnyfuns/swatchbook-core/token-value-css";
7
+ import cx from "clsx";
8
+ import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
5
9
  import { getVariance, listPaths, resolveAllWithProvenanceAt } from "@unpunnyfuns/swatchbook-core/graph";
6
10
  import { cssVarRef } from "@unpunnyfuns/swatchbook-core/css-var";
7
11
  import { SWATCHBOOK_STYLE_ELEMENT_ID, ensureStyleElement } from "@unpunnyfuns/swatchbook-core/style-element";
8
12
  import { tupleToName } from "@unpunnyfuns/swatchbook-core/themes";
9
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
- import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
11
13
  import { matchPath } from "@unpunnyfuns/swatchbook-core/match-path";
12
14
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
13
- import cx from "clsx";
14
15
  import { useWindowVirtualizer } from "@tanstack/react-virtual";
15
- //#region src/internal/channel.ts
16
- let channel = null;
17
- const pending = /* @__PURE__ */ new Set();
18
- /**
19
- * Host adapter API. The single most load-bearing function in the host
20
- * contract — the addon calls it once at preview init.
21
- *
22
- * Inject the host channel. Runs any subscribers that queued before the
23
- * channel existed, then clears the queue. The host contract does NOT dedupe:
24
- * on HMR a subscriber's module re-runs {@link onChannel}, which re-attaches
25
- * immediately once the channel is set, so each subscriber MUST guard its own
26
- * attach to stay single-wired.
27
- */
28
- function registerChannel(next) {
29
- channel = next;
30
- for (const attach of pending) attach(next);
31
- pending.clear();
32
- }
33
- /**
34
- * Host adapter API — see {@link registerChannel}.
35
- *
36
- * Attach a subscriber to the host channel now if one is registered, else queue
37
- * it to run the moment {@link registerChannel} fires. Blocks call this at
38
- * module load; the queue absorbs the gap until the addon injects the channel.
39
- */
40
- function onChannel(attach) {
41
- if (channel) attach(channel);
42
- else pending.add(attach);
43
- }
44
- //#endregion
45
- //#region src/internal/channel-globals.ts
46
- const AXES_GLOBAL_KEY = "swatchbookAxes";
47
- const COLOR_FORMAT_GLOBAL_KEY = "swatchbookColorFormat";
48
- let snapshot$1 = {
49
- axes: null,
50
- format: null
51
- };
52
- const listeners$1 = /* @__PURE__ */ new Set();
53
- let subscribed$1 = false;
54
- function isColorFormat(value) {
55
- return typeof value === "string" && COLOR_FORMATS.includes(value);
56
- }
57
- function attach$1(channel) {
58
- if (subscribed$1) return;
59
- subscribed$1 = true;
60
- let lastFingerprint = "";
61
- const onGlobals = (payload) => {
62
- const globals = payload.globals;
63
- if (!globals) return;
64
- const incomingAxes = globals[AXES_GLOBAL_KEY];
65
- const incomingFormat = globals[COLOR_FORMAT_GLOBAL_KEY];
66
- const nextAxes = incomingAxes && typeof incomingAxes === "object" ? incomingAxes : snapshot$1.axes;
67
- const nextFormat = isColorFormat(incomingFormat) ? incomingFormat : snapshot$1.format;
68
- const fingerprint = `${nextFormat ?? ""}|${nextAxes ? JSON.stringify(nextAxes) : ""}`;
69
- if (fingerprint === lastFingerprint) return;
70
- lastFingerprint = fingerprint;
71
- snapshot$1 = {
72
- axes: nextAxes,
73
- format: nextFormat
74
- };
75
- for (const cb of listeners$1) cb();
76
- };
77
- channel.on("globalsUpdated", onGlobals);
78
- channel.on("updateGlobals", onGlobals);
79
- channel.on("setGlobals", onGlobals);
80
- }
81
- onChannel(attach$1);
82
- function subscribe$2(cb) {
83
- listeners$1.add(cb);
84
- return () => {
85
- listeners$1.delete(cb);
86
- };
87
- }
88
- function getSnapshot$1() {
89
- return snapshot$1;
90
- }
91
- function getServerSnapshot$1() {
92
- return snapshot$1;
93
- }
94
- function useChannelGlobals() {
95
- return useSyncExternalStore(subscribe$2, getSnapshot$1, getServerSnapshot$1);
96
- }
97
- //#endregion
98
16
  //#region src/contexts.ts
99
17
  /**
100
18
  * Context carrying the full {@link ProjectSnapshot}. `null` sentinel lets
@@ -129,10 +47,18 @@ function useActiveAxes() {
129
47
  return useContext(AxesContext);
130
48
  }
131
49
  /**
132
- * Active color-display format for the current story/docs render. Populated
133
- * by the addon's preview decorator from the `swatchbookColorFormat` global
134
- * (per-story `globals` or toolbar dropdown) and consumed by blocks that
135
- * render color-token values. Emitted CSS is unaffected.
50
+ * Active color-display format for the current story/docs render, consumed
51
+ * by blocks that render color-token values. Emitted CSS is unaffected.
52
+ *
53
+ * Sits in the middle of the precedence chain a block resolves via
54
+ * `colorFormat ?? useColorFormat()`: a block's own `colorFormat` prop wins
55
+ * over this context, which wins over the active source's
56
+ * `defaultColorFormat` (from `Config.defaultColorFormat`). "Active
57
+ * source" is whichever one is actually feeding the render:
58
+ * `SwatchbookProvider`'s snapshot when a story decorator mounted one,
59
+ * otherwise the ambient `ProjectSource` (`#/host.ts`) that MDX-embedded
60
+ * blocks (no `<Story/>`, no provider) read through `useProject()`'s
61
+ * fallback path.
136
62
  *
137
63
  * Runs through plain React context rather than Storybook's `useGlobals` so
138
64
  * per-story seeded globals flow through on first render and the same hook
@@ -140,314 +66,18 @@ function useActiveAxes() {
140
66
  * isn't available).
141
67
  */
142
68
  const ColorFormatContext = createContext(null);
143
- function useColorFormat() {
144
- const contextValue = useContext(ColorFormatContext);
145
- const channelGlobals = useChannelGlobals();
146
- return contextValue ?? channelGlobals.format ?? "hex";
147
- }
148
- //#endregion
149
- //#region src/internal/channel-tokens.ts
150
- /**
151
- * Host adapter API — same integration tier as {@link BlockChannel} /
152
- * {@link registerChannel} / {@link onChannel}, distinct from the
153
- * components/hooks/`SwatchbookProvider` surface MDX/story authors use.
154
- * Most consumers never touch this; it exists for whoever builds the next
155
- * host integration.
156
- *
157
- * Live token snapshot backed by the addon's preview dev-time HMR event.
158
- *
159
- * The initial snapshot is *injected* by the addon preview via
160
- * {@link registerTokenSource} rather than imported from the addon's
161
- * `virtual:swatchbook/tokens` build artifact — so blocks carries no
162
- * dependency on that module and imports cleanly standalone (outside
163
- * Storybook, in unit tests, in the docs site). Until something registers
164
- * a source, blocks render from empty defaults.
165
- *
166
- * For dev-time updates this module subscribes to `TOKENS_UPDATED_EVENT`
167
- * on Storybook's channel (which the addon preview re-broadcasts from its
168
- * own HMR listener) and exposes the latest snapshot via
169
- * `useSyncExternalStore`, so hooks re-render in place on each token save.
170
- */
171
- const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
172
- let snapshot = {
173
- axes: [],
174
- presets: [],
175
- diagnostics: [],
176
- css: "",
177
- cssVarPrefix: "",
178
- indicators: {},
179
- listing: {},
180
- tokenGraph: {
181
- nodes: {},
182
- axes: [],
183
- axisDefaults: {},
184
- axisContexts: {}
185
- },
186
- defaultTuple: {},
187
- version: 0
188
- };
189
- const listeners = /* @__PURE__ */ new Set();
190
- let subscribed = false;
191
- function applyPatch(patch) {
192
- snapshot = {
193
- axes: patch.axes ?? snapshot.axes,
194
- presets: patch.presets ?? snapshot.presets,
195
- diagnostics: patch.diagnostics ?? snapshot.diagnostics,
196
- css: patch.css ?? snapshot.css,
197
- cssVarPrefix: patch.cssVarPrefix ?? snapshot.cssVarPrefix,
198
- indicators: patch.indicators ?? snapshot.indicators,
199
- listing: patch.listing ?? snapshot.listing,
200
- tokenGraph: patch.tokenGraph ?? snapshot.tokenGraph,
201
- defaultTuple: patch.defaultTuple ?? snapshot.defaultTuple,
202
- version: snapshot.version + 1
203
- };
204
- for (const cb of listeners) cb();
205
- }
206
- /**
207
- * Host adapter API — same integration tier as {@link registerChannel}.
208
- *
209
- * Seed the initial token snapshot. The addon preview calls this once at
210
- * init with the build-time `virtual:swatchbook/tokens` data. Keeping the
211
- * virtual-module read on the addon side (the package that owns it) lets
212
- * blocks import cleanly without it. No-op fields fall back to the current
213
- * snapshot, so a partial source is safe.
214
- */
215
- function registerTokenSource(source) {
216
- applyPatch(source);
217
- }
218
- function attach(channel) {
219
- if (subscribed) return;
220
- subscribed = true;
221
- channel.on(TOKENS_UPDATED_EVENT, (payload) => {
222
- applyPatch(payload);
223
- });
224
- }
225
- onChannel(attach);
226
- function subscribe$1(cb) {
227
- listeners.add(cb);
228
- return () => {
229
- listeners.delete(cb);
230
- };
231
- }
232
- function getSnapshot() {
233
- return snapshot;
234
- }
235
- function getServerSnapshot() {
236
- return snapshot;
237
- }
238
- /**
239
- * Host adapter API — same integration tier as {@link registerChannel}. Most
240
- * consumers read project data through `useSwatchbookData()` /
241
- * `SwatchbookProvider` instead; this is the internal subscription
242
- * `useProject()`'s fallback path uses to read the live channel-fed snapshot.
243
- */
244
- function useTokenSnapshot() {
245
- return useSyncExternalStore(subscribe$1, getSnapshot, getServerSnapshot);
246
- }
247
- //#endregion
248
- //#region src/internal/use-project.ts
249
- function computeVarianceByPath(graph) {
250
- if (!graph) return {};
251
- const out = {};
252
- for (const path of listPaths(graph)) out[path] = getVariance(graph, path);
253
- return out;
254
- }
255
- function ensureStylesheet(css) {
256
- ensureStyleElement(SWATCHBOOK_STYLE_ELEMENT_ID, css);
257
- }
258
- function defaultTuple(axes) {
259
- const out = {};
260
- for (const axis of axes) out[axis.name] = axis.default;
261
- return out;
262
- }
263
- function makeResolveAt(graph) {
264
- if (!graph) return () => ({});
265
- return (tuple) => resolveAllWithProvenanceAt(graph, tuple);
266
- }
267
- function snapshotResolveAt(snapshot) {
268
- if (snapshot.resolveAt) return snapshot.resolveAt;
269
- return makeResolveAt(snapshot.tokenGraph);
270
- }
271
- /**
272
- * Reads project data either from a mounted {@link SwatchbookProvider}
273
- * (preferred — the addon's preview decorator installs one around every
274
- * story) or, when no provider is present, from the virtual module plus
275
- * Storybook globals directly.
276
- *
277
- * The provider-less path is what makes the hook safe to call from MDX
278
- * doc blocks and autodocs renders where no story is active. It
279
- * self-mounts the injected snapshot's per-theme CSS and tracks the active
280
- * tuple via the injected channel's `globalsUpdated` event, since a
281
- * story-scoped globals hook only works while a story is rendering.
282
- */
283
- function useProject() {
284
- const snapshot = useOptionalSwatchbookData();
285
- const axes = snapshot?.axes;
286
- const activeAxes = snapshot?.activeAxes;
287
- const activeTheme = snapshot?.activeTheme;
288
- const diagnostics = snapshot?.diagnostics;
289
- const cssVarPrefix = snapshot?.cssVarPrefix;
290
- const indicators = snapshot?.indicators;
291
- const listing = snapshot?.listing;
292
- const tokenGraph = snapshot?.tokenGraph;
293
- const resolveAt = useMemo(() => {
294
- if (!snapshot) return null;
295
- return snapshotResolveAt(snapshot);
296
- }, [tokenGraph, activeTheme]);
297
- const derivedVarianceByPath = useMemo(() => computeVarianceByPath(tokenGraph), [tokenGraph]);
298
- const providerData = useMemo(() => {
299
- if (!snapshot || !resolveAt || !axes || !activeAxes) return null;
300
- return {
301
- activeTheme: activeTheme ?? "",
302
- activeAxes,
303
- axes,
304
- resolved: resolveAt(activeAxes),
305
- diagnostics: diagnostics ?? [],
306
- cssVarPrefix: cssVarPrefix ?? "",
307
- indicators: indicators ?? {},
308
- listing: listing ?? {},
309
- varianceByPath: derivedVarianceByPath,
310
- resolveAt
311
- };
312
- }, [
313
- snapshot,
314
- resolveAt,
315
- axes,
316
- activeTheme,
317
- activeAxes,
318
- diagnostics,
319
- cssVarPrefix,
320
- indicators,
321
- listing,
322
- derivedVarianceByPath,
323
- tokenGraph
324
- ]);
325
- const fallback = useVirtualModuleFallback(snapshot === null);
326
- return providerData ?? fallback;
327
- }
328
- function useVirtualModuleFallback(enabled) {
329
- const contextThemeName = useActiveTheme();
330
- const contextAxes = useActiveAxes();
331
- const channelGlobals = useChannelGlobals();
332
- const tokens = useTokenSnapshot();
333
- useEffect(() => {
334
- if (!enabled) return;
335
- ensureStylesheet(tokens.css);
336
- }, [enabled, tokens.css]);
337
- const activeAxes = useMemo(() => {
338
- return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple(tokens.axes);
339
- }, [
340
- contextAxes,
341
- channelGlobals.axes,
342
- tokens.axes
343
- ]);
344
- const activeTheme = contextThemeName || tupleToName(tokens.axes, activeAxes);
345
- const resolveAt = useMemo(() => makeResolveAt(tokens.tokenGraph), [tokens.tokenGraph]);
346
- const fallbackVarianceByPath = useMemo(() => computeVarianceByPath(tokens.tokenGraph), [tokens.tokenGraph]);
347
- return useMemo(() => ({
348
- activeTheme,
349
- activeAxes,
350
- axes: tokens.axes,
351
- resolved: resolveAt(activeAxes),
352
- diagnostics: tokens.diagnostics,
353
- cssVarPrefix: tokens.cssVarPrefix,
354
- indicators: tokens.indicators,
355
- listing: tokens.listing,
356
- varianceByPath: fallbackVarianceByPath,
357
- resolveAt
358
- }), [
359
- activeTheme,
360
- activeAxes,
361
- tokens.axes,
362
- tokens.diagnostics,
363
- tokens.cssVarPrefix,
364
- tokens.indicators,
365
- tokens.listing,
366
- fallbackVarianceByPath,
367
- resolveAt
368
- ]);
369
- }
370
69
  /**
371
- * Resolve a token's CSS var reference, preferring the authoritative name
372
- * emitted by `@terrazzo/plugin-css` (as recorded by
373
- * `@terrazzo/plugin-token-listing` in the snapshot's `listing` field).
374
- * Falls back to `cssVarRef` when the listing lacks an entry for this
375
- * path covers non-resolver projects, hand-built snapshots, and any
376
- * listing-plugin miss.
70
+ * Resolves the color-display format from the context/snapshot/source
71
+ * chain: `ColorFormatContext` provider snapshot's `defaultColorFormat`
72
+ * ambient source's `defaultColorFormat` → `'hex'`. Composing blocks
73
+ * read `colorFormat ?? useColorFormat()` to give their own `colorFormat`
74
+ * prop top precedence over this chain.
377
75
  */
378
- function resolveCssVar(path, project) {
379
- const listed = project.listing[path]?.names?.["css"];
380
- if (listed) return `var(${listed})`;
381
- return cssVarRef(path, project.cssVarPrefix);
382
- }
383
- /**
384
- * Resolve a color value's display string + gamut flag, preferring the
385
- * listing's `previewValue` when the user's active color-format matches
386
- * plugin-css's output (hex). For any other format we fall back to
387
- * `formatColor` so the toolbar's inspection modes (rgb / hsl / oklch /
388
- * raw) keep working — the listing has only one canonical format.
389
- *
390
- * Pass `path === undefined` when resolving a sub-color inside a composite
391
- * (shadow / border / gradient stop): composites' `previewValue` covers
392
- * the whole token's rendering, not the individual channel, so there's no
393
- * listing entry to key against.
394
- */
395
- function resolveColorValue(path, raw, colorFormat, project) {
396
- if (path !== void 0 && colorFormat === "hex") {
397
- const listed = project.listing[path]?.previewValue;
398
- if (typeof listed === "string") return {
399
- value: listed,
400
- outOfGamut: false
401
- };
402
- }
403
- return formatColor(raw, colorFormat);
404
- }
405
- //#endregion
406
- //#region src/border-preview/BorderSample.tsx
407
- /**
408
- * Pure derivation of a single border token's sample data from resolved
409
- * project data. Extracted so it is unit-testable without React or a store.
410
- */
411
- function deriveBorderSample(path, project) {
412
- return { cssVar: resolveCssVar(path, project) };
413
- }
414
- /** Pure presentation for a single border token's sample. Renders from plain props. */
415
- function BorderSampleView({ cssVar }) {
416
- return /* @__PURE__ */ jsx("div", {
417
- className: "sb-border-sample",
418
- style: { border: cssVar },
419
- "aria-hidden": true
420
- });
421
- }
422
- function BorderSample({ path }) {
423
- const { cssVar } = deriveBorderSample(path, useProject());
424
- return /* @__PURE__ */ jsx(BorderSampleView, { cssVar });
425
- }
426
- //#endregion
427
- //#region src/internal/composite-sample-format.ts
428
- /**
429
- * Display a composite sub-field dimension (shadow offset / blur / spread,
430
- * border width, …) in the preview tables. Renders `—` for a missing
431
- * sub-field and falls back to JSON for shapes it doesn't recognize.
432
- *
433
- * Distinct from `format-token-value`'s internal `formatDimension`, which
434
- * formats a token's top-level value and has no `—` placeholder — these are
435
- * the per-layer sample formatters shared by `ShadowPreview` + `BorderPreview`.
436
- */
437
- function formatDimension$1(raw) {
438
- if (raw == null) return "—";
439
- if (typeof raw === "number") return String(raw);
440
- if (typeof raw === "string") return raw;
441
- if (typeof raw === "object") {
442
- const v = raw;
443
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
444
- }
445
- return JSON.stringify(raw);
446
- }
447
- /** Display a composite sub-field color via the active format; `—` when absent. */
448
- function formatSubColor(raw, format) {
449
- if (raw == null) return "—";
450
- return formatColor(raw, format).value;
76
+ function useColorFormat() {
77
+ const contextValue = useContext(ColorFormatContext);
78
+ const snapshot = useContext(SwatchbookContext);
79
+ const source = useProjectSource();
80
+ return contextValue ?? snapshot?.defaultColorFormat ?? source.defaultColorFormat ?? "hex";
451
81
  }
452
82
  //#endregion
453
83
  //#region src/internal/data-attr.ts
@@ -604,6 +234,163 @@ function toDisplayable(v) {
604
234
  return String(v ?? "");
605
235
  }
606
236
  //#endregion
237
+ //#region src/internal/use-project.ts
238
+ function computeVarianceByPath(graph) {
239
+ if (!graph) return {};
240
+ const out = {};
241
+ for (const path of listPaths(graph)) out[path] = getVariance(graph, path);
242
+ return out;
243
+ }
244
+ function ensureStylesheet(css) {
245
+ ensureStyleElement(SWATCHBOOK_STYLE_ELEMENT_ID, css);
246
+ }
247
+ function defaultTuple(axes) {
248
+ const out = {};
249
+ for (const axis of axes) out[axis.name] = axis.default;
250
+ return out;
251
+ }
252
+ function makeResolveAt(graph) {
253
+ if (!graph) return () => ({});
254
+ return (tuple) => resolveAllWithProvenanceAt(graph, tuple);
255
+ }
256
+ function snapshotResolveAt(snapshot) {
257
+ if (snapshot.resolveAt) return snapshot.resolveAt;
258
+ return makeResolveAt(snapshot.tokenGraph);
259
+ }
260
+ /**
261
+ * Reads project data either from a mounted {@link SwatchbookProvider}
262
+ * (preferred: the addon's preview decorator installs one around every
263
+ * story) or, when no provider is present, from the ambient project source
264
+ * a host pushes into via `registerProjectSource` (`#/host.ts`).
265
+ *
266
+ * The provider-less path is what makes the hook safe to call from MDX
267
+ * doc blocks and autodocs renders where no story is active. It
268
+ * self-mounts the source's per-theme CSS and tracks the active tuple via
269
+ * the source's `activeAxes`, since a story-scoped globals hook only works
270
+ * while a story is rendering.
271
+ */
272
+ function useProject() {
273
+ const snapshot = useOptionalSwatchbookData();
274
+ const axes = snapshot?.axes;
275
+ const activeAxes = snapshot?.activeAxes;
276
+ const activeTheme = snapshot?.activeTheme;
277
+ const diagnostics = snapshot?.diagnostics;
278
+ const cssVarPrefix = snapshot?.cssVarPrefix;
279
+ const indicators = snapshot?.indicators;
280
+ const listing = snapshot?.listing;
281
+ const tokenGraph = snapshot?.tokenGraph;
282
+ const resolveAt = useMemo(() => {
283
+ if (!snapshot) return null;
284
+ return snapshotResolveAt(snapshot);
285
+ }, [tokenGraph, activeTheme]);
286
+ const derivedVarianceByPath = useMemo(() => computeVarianceByPath(tokenGraph), [tokenGraph]);
287
+ const providerData = useMemo(() => {
288
+ if (!snapshot || !resolveAt || !axes || !activeAxes) return null;
289
+ return {
290
+ activeTheme: activeTheme ?? "",
291
+ activeAxes,
292
+ axes,
293
+ resolved: resolveAt(activeAxes),
294
+ diagnostics: diagnostics ?? [],
295
+ cssVarPrefix: cssVarPrefix ?? "",
296
+ indicators: indicators ?? {},
297
+ listing: listing ?? {},
298
+ varianceByPath: derivedVarianceByPath,
299
+ resolveAt
300
+ };
301
+ }, [
302
+ snapshot,
303
+ resolveAt,
304
+ axes,
305
+ activeTheme,
306
+ activeAxes,
307
+ diagnostics,
308
+ cssVarPrefix,
309
+ indicators,
310
+ listing,
311
+ derivedVarianceByPath,
312
+ tokenGraph
313
+ ]);
314
+ const fallback = useVirtualModuleFallback(snapshot === null);
315
+ return providerData ?? fallback;
316
+ }
317
+ function useVirtualModuleFallback(enabled) {
318
+ const contextThemeName = useActiveTheme();
319
+ const contextAxes = useActiveAxes();
320
+ const source = useProjectSource();
321
+ useEffect(() => {
322
+ if (!enabled) return;
323
+ ensureStylesheet(source.css);
324
+ }, [enabled, source.css]);
325
+ const activeAxes = useMemo(() => {
326
+ return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : source.activeAxes ?? defaultTuple(source.axes);
327
+ }, [
328
+ contextAxes,
329
+ source.activeAxes,
330
+ source.axes
331
+ ]);
332
+ const activeTheme = contextThemeName || tupleToName(source.axes, activeAxes);
333
+ const resolveAt = useMemo(() => makeResolveAt(source.tokenGraph), [source.tokenGraph]);
334
+ const fallbackVarianceByPath = useMemo(() => computeVarianceByPath(source.tokenGraph), [source.tokenGraph]);
335
+ return useMemo(() => ({
336
+ activeTheme,
337
+ activeAxes,
338
+ axes: source.axes,
339
+ resolved: resolveAt(activeAxes),
340
+ diagnostics: source.diagnostics,
341
+ cssVarPrefix: source.cssVarPrefix,
342
+ indicators: source.indicators,
343
+ listing: source.listing,
344
+ varianceByPath: fallbackVarianceByPath,
345
+ resolveAt
346
+ }), [
347
+ activeTheme,
348
+ activeAxes,
349
+ source.axes,
350
+ source.diagnostics,
351
+ source.cssVarPrefix,
352
+ source.indicators,
353
+ source.listing,
354
+ fallbackVarianceByPath,
355
+ resolveAt
356
+ ]);
357
+ }
358
+ /**
359
+ * Resolve a token's CSS var reference, preferring the authoritative name
360
+ * emitted by `@terrazzo/plugin-css` (as recorded by
361
+ * `@terrazzo/plugin-token-listing` in the snapshot's `listing` field).
362
+ * Falls back to `cssVarRef` when the listing lacks an entry for this
363
+ * path — covers non-resolver projects, hand-built snapshots, and any
364
+ * listing-plugin miss.
365
+ */
366
+ function resolveCssVar(path, project) {
367
+ const listed = project.listing[path]?.names?.["css"];
368
+ if (listed) return `var(${listed})`;
369
+ return cssVarRef(path, project.cssVarPrefix);
370
+ }
371
+ /**
372
+ * Resolve a color value's display string + gamut flag, preferring the
373
+ * listing's `previewValue` when the user's active color-format matches
374
+ * plugin-css's output (hex). For any other format we fall back to
375
+ * `formatColor` so the toolbar's inspection modes (rgb / hsl / oklch /
376
+ * raw) keep working — the listing has only one canonical format.
377
+ *
378
+ * Pass `path === undefined` when resolving a sub-color inside a composite
379
+ * (shadow / border / gradient stop): composites' `previewValue` covers
380
+ * the whole token's rendering, not the individual channel, so there's no
381
+ * listing entry to key against.
382
+ */
383
+ function resolveColorValue(path, raw, colorFormat, project) {
384
+ if (path !== void 0 && colorFormat === "hex") {
385
+ const listed = project.listing[path]?.previewValue;
386
+ if (typeof listed === "string") return {
387
+ value: listed,
388
+ outOfGamut: false
389
+ };
390
+ }
391
+ return formatColor(raw, colorFormat);
392
+ }
393
+ //#endregion
607
394
  //#region src/BorderPreview.tsx
608
395
  /**
609
396
  * Pure derivation of the preview's display rows from resolved project data.
@@ -621,7 +408,8 @@ function deriveBorderRows(resolved, project, { filter, sortBy, sortDir, colorFor
621
408
  return {
622
409
  path,
623
410
  cssVar: resolveCssVar(path, project),
624
- width: formatDimension$1(value.width),
411
+ token,
412
+ width: formatDimension(value.width),
625
413
  style: value.style != null ? String(value.style) : "—",
626
414
  color: formatSubColor(value.color, colorFormat)
627
415
  };
@@ -629,10 +417,11 @@ function deriveBorderRows(resolved, project, { filter, sortBy, sortDir, colorFor
629
417
  }
630
418
  /**
631
419
  * Pure presentation for the border preview. Renders from plain props;
632
- * composes the connected `BorderSample` as a child (that child reads the
633
- * project itself).
420
+ * composes the connected `BorderSample` as a child, feeding it this row's
421
+ * already-resolved `token`/`cssVar` per the presenter contract.
634
422
  */
635
- function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
423
+ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
424
+ const Sample = usePresenter("border");
636
425
  const captionText = caption ?? `${rows.length} border${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
637
426
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
638
427
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -661,7 +450,12 @@ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
661
450
  }),
662
451
  /* @__PURE__ */ jsx("div", {
663
452
  className: "sb-border-preview__sample-cell",
664
- children: /* @__PURE__ */ jsx(BorderSample, { path: row.path })
453
+ children: Sample && /* @__PURE__ */ jsx(Sample, {
454
+ path: row.path,
455
+ token: row.token,
456
+ cssVar: row.cssVar,
457
+ colorFormat
458
+ })
665
459
  }),
666
460
  /* @__PURE__ */ jsxs("div", {
667
461
  className: "sb-border-preview__breakdown",
@@ -687,27 +481,29 @@ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
687
481
  }, row.path))]
688
482
  });
689
483
  }
690
- function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
484
+ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc", colorFormat }) {
691
485
  const project = useProject();
692
486
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
693
- const colorFormat = useColorFormat();
487
+ const contextColorFormat = useColorFormat();
488
+ const format = colorFormat ?? contextColorFormat;
694
489
  return /* @__PURE__ */ jsx(BorderPreviewView, {
695
490
  rows: useMemo(() => deriveBorderRows(resolved, project, {
696
491
  filter,
697
492
  sortBy,
698
493
  sortDir,
699
- colorFormat
494
+ colorFormat: format
700
495
  }), [
701
496
  resolved,
702
497
  project,
703
498
  filter,
704
499
  sortBy,
705
500
  sortDir,
706
- colorFormat
501
+ format
707
502
  ]),
708
503
  activeTheme,
709
504
  cssVarPrefix,
710
505
  activeAxes,
506
+ colorFormat: format,
711
507
  filter,
712
508
  caption
713
509
  });
@@ -728,7 +524,7 @@ function fixedPrefixLength(filter) {
728
524
  * Pure derivation of the palette's grouped swatch rows from resolved project
729
525
  * data. Extracted so it is unit-testable without React or a store.
730
526
  */
731
- function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, groupBy, sortBy, sortDir, colorFormat }) {
527
+ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, groupBy, sortBy, sortDir }) {
732
528
  const projectFields = {
733
529
  listing,
734
530
  cssVarPrefix
@@ -748,13 +544,11 @@ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, gro
748
544
  const groupKey = segments.slice(0, effectiveGroupBy).join(".");
749
545
  const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
750
546
  const list = bucket.get(groupKey) ?? [];
751
- const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
752
547
  list.push({
753
548
  path,
754
- leaf,
755
549
  cssVar: resolveCssVar(path, projectFields),
756
- value: formatted.value,
757
- outOfGamut: formatted.outOfGamut
550
+ leaf,
551
+ token
758
552
  });
759
553
  bucket.set(groupKey, list);
760
554
  }
@@ -763,8 +557,13 @@ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, gro
763
557
  swatches
764
558
  }));
765
559
  }
766
- /** Pure presentation for the color palette. Renders from plain props. */
767
- function ColorPaletteView({ groups, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
560
+ /**
561
+ * Pure presentation for the color palette. Renders from plain props;
562
+ * composes the registry's `color` presenter per swatch, feeding it this
563
+ * row's already-resolved `token`/`cssVar` per the presenter contract.
564
+ */
565
+ function ColorPaletteView({ groups, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
566
+ const Swatch = usePresenter("color");
768
567
  const totalCount = groups.reduce((acc, { swatches }) => acc + swatches.length, 0);
769
568
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
770
569
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
@@ -786,42 +585,27 @@ function ColorPaletteView({ groups, activeTheme, cssVarPrefix, activeAxes, filte
786
585
  children: group
787
586
  }), /* @__PURE__ */ jsx("div", {
788
587
  className: "sb-color-palette__grid",
789
- children: swatches.map((swatch) => /* @__PURE__ */ jsxs("div", {
790
- className: "sb-color-palette__card",
791
- children: [/* @__PURE__ */ jsx("div", {
792
- className: "sb-color-palette__swatch",
793
- style: { background: swatch.cssVar },
794
- "aria-hidden": true
795
- }), /* @__PURE__ */ jsxs("div", {
796
- className: "sb-color-palette__meta",
797
- children: [/* @__PURE__ */ jsx("span", {
798
- className: "sb-color-palette__leaf",
799
- children: swatch.leaf
800
- }), /* @__PURE__ */ jsxs("span", {
801
- className: "sb-color-palette__value",
802
- children: [swatch.value, swatch.outOfGamut && /* @__PURE__ */ jsxs("span", {
803
- title: "Out of sRGB gamut for this format",
804
- "aria-label": "out of gamut",
805
- className: "sb-color-palette__gamut-warn",
806
- children: [" ", "⚠"]
807
- })]
808
- })]
809
- })]
588
+ children: swatches.map((swatch) => Swatch && /* @__PURE__ */ jsx(Swatch, {
589
+ path: swatch.path,
590
+ token: swatch.token,
591
+ cssVar: swatch.cssVar,
592
+ colorFormat,
593
+ options: { label: swatch.leaf }
810
594
  }, swatch.path))
811
595
  })]
812
596
  }, group))]
813
597
  });
814
598
  }
815
- function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
599
+ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc", colorFormat }) {
816
600
  const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
817
- const colorFormat = useColorFormat();
601
+ const contextColorFormat = useColorFormat();
602
+ const format = colorFormat ?? contextColorFormat;
818
603
  return /* @__PURE__ */ jsx(ColorPaletteView, {
819
604
  groups: useMemo(() => deriveColorPaletteGroups(resolved, listing, cssVarPrefix, {
820
605
  filter,
821
606
  groupBy,
822
607
  sortBy,
823
- sortDir,
824
- colorFormat
608
+ sortDir
825
609
  }), [
826
610
  resolved,
827
611
  listing,
@@ -829,12 +613,12 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
829
613
  filter,
830
614
  groupBy,
831
615
  sortBy,
832
- sortDir,
833
- colorFormat
616
+ sortDir
834
617
  ]),
835
618
  activeTheme,
836
619
  cssVarPrefix,
837
620
  activeAxes,
621
+ colorFormat: format,
838
622
  filter,
839
623
  caption
840
624
  });
@@ -1157,11 +941,11 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
1157
941
  /**
1158
942
  * Block UI state that survives a docs-mode remount.
1159
943
  *
1160
- * In MDX docs mode Storybook re-renders the docs container on every
1161
- * `updateGlobals` (axis flip), which unmounts and remounts the embedded
1162
- * blocks — destroying any plain `useState` (expand/collapse, selection,
1163
- * search). This is the same problem `channel-globals` solves for the
1164
- * globals: lift the value out of React into module state so it persists
944
+ * In MDX docs mode Storybook re-renders the docs container on every axis
945
+ * flip, which unmounts and remounts the embedded blocks: destroying any
946
+ * plain `useState` (expand/collapse, selection, search). This is the same
947
+ * problem the ambient project source (`#/host.ts`) solves for token/axis
948
+ * data: lift the value out of React into module state so it persists
1165
949
  * across the remount, and re-seed component state from it on mount.
1166
950
  *
1167
951
  * `usePersistedState` is a drop-in `useState` whose value is mirrored to a
@@ -1402,9 +1186,10 @@ function ColorTableView({ groups, activeTheme, cssVarPrefix, activeAxes, colorFo
1402
1186
  * selector; clicking a row expands it in place to show the description,
1403
1187
  * alias chain, and (for grouped rows) every variant's hex/HSL/OKLCH values.
1404
1188
  */
1405
- function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants, id, indicators }) {
1189
+ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants, id, indicators, colorFormat }) {
1406
1190
  const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath } = useProject();
1407
- const colorFormat = useColorFormat();
1191
+ const contextColorFormat = useColorFormat();
1192
+ const format = colorFormat ?? contextColorFormat;
1408
1193
  const enabledIndicators = useMemo(() => ({
1409
1194
  ...resolveIndicators(indicators),
1410
1195
  gamut: false
@@ -1420,7 +1205,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1420
1205
  variants,
1421
1206
  sortBy,
1422
1207
  sortDir,
1423
- colorFormat
1208
+ colorFormat: format
1424
1209
  }), [
1425
1210
  resolved,
1426
1211
  listing,
@@ -1430,12 +1215,12 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1430
1215
  variants,
1431
1216
  sortBy,
1432
1217
  sortDir,
1433
- colorFormat
1218
+ format
1434
1219
  ]),
1435
1220
  activeTheme,
1436
1221
  cssVarPrefix,
1437
1222
  activeAxes,
1438
- colorFormat,
1223
+ colorFormat: format,
1439
1224
  enabledIndicators,
1440
1225
  blockKey,
1441
1226
  filter,
@@ -1796,244 +1581,27 @@ function DiagnosticsView({ caption, diagnostics, cssVarPrefix, activeAxes }) {
1796
1581
  })] })]
1797
1582
  }, diagnosticKey(d, i)))
1798
1583
  })]
1799
- })
1800
- });
1801
- }
1802
- /**
1803
- * Render the project's load diagnostics — parser errors, resolver warnings,
1804
- * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
1805
- * when the project carries errors or warnings; stays collapsed for clean
1806
- * loads and info-only loads.
1807
- *
1808
- * Replaces the diagnostics section from the addon's (now-retired) Design
1809
- * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1810
- * on their own MDX pages.
1811
- */
1812
- function Diagnostics({ caption } = {}) {
1813
- const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1814
- return /* @__PURE__ */ jsx(DiagnosticsView, {
1815
- caption,
1816
- diagnostics,
1817
- cssVarPrefix,
1818
- activeAxes
1819
- });
1820
- }
1821
- //#endregion
1822
- //#region src/dimension-scale/dimension-px.ts
1823
- /**
1824
- * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
1825
- * purpose of deciding whether to cap the rendered size. `rootFontSizePx`
1826
- * scales `rem` against the rendering context's actual root font-size
1827
- * (default 16 for the no-DOM / SSR path); the bar paints at the same
1828
- * context's `var()`, so passing the measured root keeps the cap decision
1829
- * aligned with what's drawn. Returns `NaN` for anything other than `px` /
1830
- * `rem` — `ex` / `ch` / `%`, and the non-DTCG `em` (the `dimension` type
1831
- * permits only `px | rem`) — which the caller treats as "render at cssVar
1832
- * but don't cap".
1833
- */
1834
- function toPixels(raw, rootFontSizePx = 16) {
1835
- if (raw == null || typeof raw !== "object") return NaN;
1836
- const v = raw;
1837
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1838
- switch (v.unit) {
1839
- case "px": return v.value;
1840
- case "rem": return v.value * rootFontSizePx;
1841
- default: return NaN;
1842
- }
1843
- }
1844
- //#endregion
1845
- //#region src/internal/use-root-font-size.ts
1846
- function readRootFontSize() {
1847
- if (typeof document === "undefined") return 16;
1848
- const fontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
1849
- return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 16;
1850
- }
1851
- function subscribe(onChange) {
1852
- if (typeof window === "undefined") return () => {};
1853
- window.addEventListener("resize", onChange);
1854
- return () => window.removeEventListener("resize", onChange);
1855
- }
1856
- /**
1857
- * Root font-size (px) of the context a block renders in, tracked across
1858
- * viewport changes. `rem` dimension values resolve their `var()` against this
1859
- * root, so the cap math (`toPixels`) and value sort (`sortTokens`) must scale
1860
- * `rem` by it rather than a literal 16: a Storybook Docs page, or a responsive
1861
- * desktop/tablet/mobile breakpoint, can apply a different root to the same
1862
- * token. Falls back to 16 with no DOM.
1863
- */
1864
- function useRootFontSize() {
1865
- return useSyncExternalStore(subscribe, readRootFontSize, () => 16);
1866
- }
1867
- //#endregion
1868
- //#region src/dimension-scale/DimensionSample.tsx
1869
- /**
1870
- * Pure derivation of a single dimension token's sample geometry from
1871
- * resolved project data. Extracted so it is unit-testable without React or
1872
- * a store.
1873
- */
1874
- function deriveDimensionSample(path, project, rootFontSizePx) {
1875
- const cssVar = resolveCssVar(path, project);
1876
- const token = project.resolved[path];
1877
- const pxValue = toPixels(token?.$value, rootFontSizePx);
1878
- const capped = Number.isFinite(pxValue) && pxValue > 480;
1879
- return {
1880
- cssVar,
1881
- pxValue,
1882
- capped,
1883
- cappedValue: capped ? `480px` : cssVar
1884
- };
1885
- }
1886
- function withCap(visual) {
1887
- return /* @__PURE__ */ jsxs("span", {
1888
- className: "sb-dimension-sample sb-dimension-sample--capped",
1889
- title: `capped at 480px`,
1890
- children: [visual, /* @__PURE__ */ jsx("span", {
1891
- className: "sb-dimension-sample__cap",
1892
- "aria-hidden": true,
1893
- children: "…"
1894
- })]
1895
- });
1896
- }
1897
- /** Pure presentation for a single dimension token's bar/sample. Renders from plain props. */
1898
- function DimensionSampleView({ cssVar, capped, cappedValue, visual }) {
1899
- switch (visual) {
1900
- case "radius": return /* @__PURE__ */ jsx("div", {
1901
- className: "sb-dimension-sample__radius-sample",
1902
- style: { borderRadius: cssVar },
1903
- "aria-hidden": true
1904
- });
1905
- case "size": {
1906
- const sample = /* @__PURE__ */ jsx("div", {
1907
- className: "sb-dimension-sample__size-sample",
1908
- style: {
1909
- width: cappedValue,
1910
- height: cappedValue
1911
- },
1912
- "aria-hidden": true
1913
- });
1914
- return capped ? withCap(sample) : sample;
1915
- }
1916
- default: {
1917
- const bar = /* @__PURE__ */ jsx("div", {
1918
- className: "sb-dimension-sample__bar",
1919
- style: { width: cappedValue },
1920
- "aria-hidden": true
1921
- });
1922
- return capped ? withCap(bar) : bar;
1923
- }
1924
- }
1925
- }
1926
- /** Connected block: resolves `path` against the active project and renders its dimension sample. */
1927
- function DimensionSample({ path, visual = "length" }) {
1928
- const { cssVar, capped, cappedValue } = deriveDimensionSample(path, useProject(), useRootFontSize());
1929
- return /* @__PURE__ */ jsx(DimensionSampleView, {
1930
- cssVar,
1931
- capped,
1932
- cappedValue,
1933
- visual
1584
+ })
1934
1585
  });
1935
1586
  }
1936
- //#endregion
1937
- //#region src/internal/format-token-value.ts
1938
1587
  /**
1939
- * Single-line display string for any DTCG token `$value`. Prefers
1940
- * plugin-css's `previewValue` from the Token Listing; for color
1941
- * tokens only when the toolbar format is hex (other formats route
1942
- * through local colorjs.io).
1588
+ * Render the project's load diagnostics parser errors, resolver warnings,
1589
+ * disabled-axes validation issues, etc. as a collapsible list. Auto-opens
1590
+ * when the project carries errors or warnings; stays collapsed for clean
1591
+ * loads and info-only loads.
1592
+ *
1593
+ * Replaces the diagnostics section from the addon's (now-retired) Design
1594
+ * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1595
+ * on their own MDX pages.
1943
1596
  */
1944
- function formatTokenValue(value, $type, colorFormat, listingEntry) {
1945
- if (value == null) return "";
1946
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
1947
- const preview = listingEntry?.previewValue;
1948
- if (preview !== void 0) {
1949
- const previewStr = typeof preview === "string" ? cleanFloatNoise(preview) : String(preview);
1950
- if ($type !== "color") return previewStr;
1951
- if (colorFormat === "hex") return previewStr;
1952
- }
1953
- switch ($type) {
1954
- case "color": return formatColor(value, colorFormat).value;
1955
- case "dimension":
1956
- case "duration": return formatDimension(value);
1957
- case "fontFamily": return formatFontFamily$1(value);
1958
- case "fontWeight":
1959
- case "lineHeight":
1960
- case "letterSpacing":
1961
- case "opacity":
1962
- case "number": return formatPrimitive$1(value);
1963
- case "cubicBezier": return formatCubicBezier(value);
1964
- case "strokeStyle": return formatStrokeStyle(value);
1965
- case "shadow": return formatShadow(value, colorFormat);
1966
- case "border": return formatBorder(value, colorFormat);
1967
- default: return formatUnknown(value);
1968
- }
1969
- }
1970
- function formatDimension(v) {
1971
- if (typeof v === "string" || typeof v === "number") return String(v);
1972
- if (v && typeof v === "object") {
1973
- const d = v;
1974
- if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
1975
- }
1976
- return formatUnknown(v);
1977
- }
1978
- function cleanFloatNoise(s) {
1979
- return s.replace(/-?\d+\.\d{8,}/g, (m) => `${+Number(m).toFixed(3)}`);
1980
- }
1981
- function formatFontFamily$1(v) {
1982
- if (typeof v === "string") return v;
1983
- if (Array.isArray(v)) return v.map(String).join(", ");
1984
- return formatUnknown(v);
1985
- }
1986
- function formatPrimitive$1(v) {
1987
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
1988
- return formatUnknown(v);
1989
- }
1990
- function formatCubicBezier(v) {
1991
- if (Array.isArray(v) && v.length === 4) return `cubic-bezier(${v.map((n) => typeof n === "number" ? n : 0).join(", ")})`;
1992
- return formatUnknown(v);
1993
- }
1994
- function formatStrokeStyle(v) {
1995
- if (typeof v === "string") return v;
1996
- if (v && typeof v === "object") {
1997
- const s = v;
1998
- const parts = ["dashed"];
1999
- if (Array.isArray(s.dashArray)) parts.push(s.dashArray.map((n) => formatDimension(n)).join(" "));
2000
- if (typeof s.lineCap === "string") parts.push(s.lineCap);
2001
- return parts.join(" · ");
2002
- }
2003
- return formatUnknown(v);
2004
- }
2005
- function formatShadow(v, colorFormat) {
2006
- return (Array.isArray(v) ? v : [v]).map((layer) => {
2007
- if (!layer || typeof layer !== "object") return formatUnknown(layer);
2008
- const s = layer;
2009
- const pieces = [
2010
- formatDimension(s.offsetX),
2011
- formatDimension(s.offsetY),
2012
- formatDimension(s.blur),
2013
- formatDimension(s.spread),
2014
- formatColor(s.color, colorFormat).value
2015
- ].filter((p) => p !== "");
2016
- if (s.inset) pieces.push("inset");
2017
- return pieces.join(" ");
2018
- }).join(", ");
2019
- }
2020
- function formatBorder(v, colorFormat) {
2021
- if (!v || typeof v !== "object") return formatUnknown(v);
2022
- const b = v;
2023
- return [
2024
- formatDimension(b.width),
2025
- formatPrimitive$1(b.style),
2026
- formatColor(b.color, colorFormat).value
2027
- ].filter((p) => p !== "").join(" ");
2028
- }
2029
- function formatUnknown(v) {
2030
- if (v == null) return "";
2031
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
2032
- try {
2033
- return JSON.stringify(v).slice(0, 120);
2034
- } catch {
2035
- return String(v);
2036
- }
1597
+ function Diagnostics({ caption } = {}) {
1598
+ const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1599
+ return /* @__PURE__ */ jsx(DiagnosticsView, {
1600
+ caption,
1601
+ diagnostics,
1602
+ cssVarPrefix,
1603
+ activeAxes
1604
+ });
2037
1605
  }
2038
1606
  //#endregion
2039
1607
  //#region src/DimensionScale.tsx
@@ -2052,15 +1620,17 @@ function deriveDimensionRows(resolved, project, { filter, sortBy, sortDir, rootF
2052
1620
  }).map(([path, token]) => ({
2053
1621
  path,
2054
1622
  cssVar: resolveCssVar(path, project),
2055
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path])
1623
+ token,
1624
+ displayValue: formatTokenValue$2(token.$value, token.$type, "raw", project.listing[path])
2056
1625
  }));
2057
1626
  }
2058
1627
  /**
2059
1628
  * Pure presentation for the dimension scale. Renders from plain props;
2060
- * composes the connected `DimensionSample` as a child (that child reads the
2061
- * project itself).
1629
+ * composes the connected `DimensionSample` as a child, feeding it this row's
1630
+ * already-resolved `token`/`cssVar` per the presenter contract.
2062
1631
  */
2063
- function DimensionScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, visual, filter, caption }) {
1632
+ function DimensionScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, visual, colorFormat, filter, caption }) {
1633
+ const Sample = usePresenter("dimension");
2064
1634
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2065
1635
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2066
1636
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2089,9 +1659,12 @@ function DimensionScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, visua
2089
1659
  }),
2090
1660
  /* @__PURE__ */ jsx("div", {
2091
1661
  className: "sb-dimension-scale__visual-cell",
2092
- children: /* @__PURE__ */ jsx(DimensionSample, {
1662
+ children: Sample && /* @__PURE__ */ jsx(Sample, {
2093
1663
  path: row.path,
2094
- visual
1664
+ token: row.token,
1665
+ cssVar: row.cssVar,
1666
+ colorFormat,
1667
+ options: { visual }
2095
1668
  })
2096
1669
  }),
2097
1670
  /* @__PURE__ */ jsx("span", {
@@ -2106,6 +1679,7 @@ function DimensionScale({ filter, visual = "length", caption, sortBy = "value",
2106
1679
  const project = useProject();
2107
1680
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2108
1681
  const rootFontSize = useRootFontSize();
1682
+ const colorFormat = useColorFormat();
2109
1683
  return /* @__PURE__ */ jsx(DimensionScaleView, {
2110
1684
  rows: useMemo(() => deriveDimensionRows(resolved, project, {
2111
1685
  filter,
@@ -2124,17 +1698,13 @@ function DimensionScale({ filter, visual = "length", caption, sortBy = "value",
2124
1698
  cssVarPrefix,
2125
1699
  activeAxes,
2126
1700
  visual,
1701
+ colorFormat,
2127
1702
  filter,
2128
1703
  caption
2129
1704
  });
2130
1705
  }
2131
1706
  //#endregion
2132
1707
  //#region src/FontFamilyPreview.tsx
2133
- function stackString(raw) {
2134
- if (typeof raw === "string") return raw;
2135
- if (Array.isArray(raw)) return raw.map(String).join(", ");
2136
- return "";
2137
- }
2138
1708
  /**
2139
1709
  * Pure derivation of the preview's display rows from resolved project data.
2140
1710
  * Extracted so it is unit-testable without React or a store.
@@ -2149,11 +1719,16 @@ function deriveFontFamilyRows(resolved, project, { filter, sortBy, sortDir }) {
2149
1719
  }).map(([path, token]) => ({
2150
1720
  path,
2151
1721
  cssVar: resolveCssVar(path, project),
2152
- stack: stackString(token.$value)
1722
+ token
2153
1723
  }));
2154
1724
  }
2155
- /** Pure presentation for the font-family preview. Renders from plain props. */
2156
- function FontFamilyPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
1725
+ /**
1726
+ * Pure presentation for the font-family preview. Renders from plain props;
1727
+ * composes the connected `FontFamilySpecimen` as a child, feeding it this
1728
+ * row's already-resolved `token`/`cssVar` per the presenter contract.
1729
+ */
1730
+ function FontFamilyPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, colorFormat, filter, caption }) {
1731
+ const Specimen = usePresenter("fontFamily");
2157
1732
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2158
1733
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2159
1734
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2167,35 +1742,19 @@ function FontFamilyPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, sa
2167
1742
  children: [/* @__PURE__ */ jsx("div", {
2168
1743
  className: "sb-block__caption",
2169
1744
  children: captionText
2170
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2171
- className: "sb-font-family-sample__row",
2172
- children: [
2173
- /* @__PURE__ */ jsxs("div", {
2174
- className: "sb-font-family-sample__meta",
2175
- children: [/* @__PURE__ */ jsx("span", {
2176
- className: "sb-font-family-sample__path",
2177
- children: row.path
2178
- }), /* @__PURE__ */ jsx("span", {
2179
- className: "sb-font-family-sample__stack",
2180
- children: row.stack
2181
- })]
2182
- }),
2183
- /* @__PURE__ */ jsx("div", {
2184
- className: "sb-font-family-sample__sample",
2185
- style: { fontFamily: row.cssVar },
2186
- children: sample
2187
- }),
2188
- /* @__PURE__ */ jsx("span", {
2189
- className: "sb-font-family-sample__css-var",
2190
- children: row.cssVar
2191
- })
2192
- ]
1745
+ }), rows.map((row) => Specimen && /* @__PURE__ */ jsx(Specimen, {
1746
+ path: row.path,
1747
+ token: row.token,
1748
+ cssVar: row.cssVar,
1749
+ colorFormat,
1750
+ options: { sample }
2193
1751
  }, row.path))]
2194
1752
  });
2195
1753
  }
2196
1754
  function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
2197
1755
  const project = useProject();
2198
1756
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1757
+ const colorFormat = useColorFormat();
2199
1758
  return /* @__PURE__ */ jsx(FontFamilyPreviewView, {
2200
1759
  rows: useMemo(() => deriveFontFamilyRows(resolved, project, {
2201
1760
  filter,
@@ -2212,35 +1771,13 @@ function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over th
2212
1771
  cssVarPrefix,
2213
1772
  activeAxes,
2214
1773
  sample,
1774
+ colorFormat,
2215
1775
  filter,
2216
1776
  caption
2217
1777
  });
2218
1778
  }
2219
1779
  //#endregion
2220
- //#region src/internal/css-var-style.ts
2221
- /**
2222
- * Coerce a CSS `var(--…)` reference into the numeric slot of a React
2223
- * inline-style property.
2224
- *
2225
- * React's `CSSProperties` types unitless properties (`fontWeight`,
2226
- * `lineHeight`, `zIndex`, …) as `number`. The DOM accepts a string at
2227
- * runtime — the rendered stylesheet just receives whatever React passes —
2228
- * so a `var(--font-weight)` reference works functionally. TypeScript still
2229
- * complains. Centralising the type assertion in one named helper keeps
2230
- * the gap visible (and greppable) instead of scattering casts across
2231
- * block components that want CSS-var-driven typography or layout values.
2232
- */
2233
- const cssVarAsNumber = (varRef) => varRef;
2234
- //#endregion
2235
1780
  //#region src/FontWeightScale.tsx
2236
- function toWeight(raw) {
2237
- if (typeof raw === "number") return raw;
2238
- if (typeof raw === "string") {
2239
- const n = Number.parseInt(raw, 10);
2240
- return Number.isFinite(n) ? n : NaN;
2241
- }
2242
- return NaN;
2243
- }
2244
1781
  /**
2245
1782
  * Pure derivation of the scale's display rows from resolved project data.
2246
1783
  * Extracted so it is unit-testable without React or a store.
@@ -2255,12 +1792,16 @@ function deriveFontWeightRows(resolved, project, { filter, sortBy, sortDir }) {
2255
1792
  }).map(([path, token]) => ({
2256
1793
  path,
2257
1794
  cssVar: resolveCssVar(path, project),
2258
- display: token.$value == null ? "" : String(token.$value),
2259
- weight: toWeight(token.$value)
1795
+ token
2260
1796
  }));
2261
1797
  }
2262
- /** Pure presentation for the font-weight scale. Renders from plain props. */
2263
- function FontWeightScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
1798
+ /**
1799
+ * Pure presentation for the font-weight scale. Renders from plain props;
1800
+ * composes the connected `FontWeightSpecimen` as a child, feeding it this
1801
+ * row's already-resolved `token`/`cssVar` per the presenter contract.
1802
+ */
1803
+ function FontWeightScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, colorFormat, filter, caption }) {
1804
+ const Specimen = usePresenter("fontWeight");
2264
1805
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2265
1806
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2266
1807
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2274,35 +1815,19 @@ function FontWeightScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, samp
2274
1815
  children: [/* @__PURE__ */ jsx("div", {
2275
1816
  className: "sb-block__caption",
2276
1817
  children: captionText
2277
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2278
- className: "sb-font-weight-scale__row",
2279
- children: [
2280
- /* @__PURE__ */ jsxs("div", {
2281
- className: "sb-font-weight-scale__meta",
2282
- children: [/* @__PURE__ */ jsx("span", {
2283
- className: "sb-font-weight-scale__path",
2284
- children: row.path
2285
- }), /* @__PURE__ */ jsx("span", {
2286
- className: "sb-font-weight-scale__value",
2287
- children: row.display
2288
- })]
2289
- }),
2290
- /* @__PURE__ */ jsx("div", {
2291
- className: "sb-font-weight-scale__sample",
2292
- style: { fontWeight: cssVarAsNumber(row.cssVar) },
2293
- children: sample
2294
- }),
2295
- /* @__PURE__ */ jsx("span", {
2296
- className: "sb-font-weight-scale__css-var",
2297
- children: row.cssVar
2298
- })
2299
- ]
1818
+ }), rows.map((row) => Specimen && /* @__PURE__ */ jsx(Specimen, {
1819
+ path: row.path,
1820
+ token: row.token,
1821
+ cssVar: row.cssVar,
1822
+ colorFormat,
1823
+ options: { sample }
2300
1824
  }, row.path))]
2301
1825
  });
2302
1826
  }
2303
1827
  function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
2304
1828
  const project = useProject();
2305
1829
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1830
+ const colorFormat = useColorFormat();
2306
1831
  return /* @__PURE__ */ jsx(FontWeightScaleView, {
2307
1832
  rows: useMemo(() => deriveFontWeightRows(resolved, project, {
2308
1833
  filter,
@@ -2319,6 +1844,7 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
2319
1844
  cssVarPrefix,
2320
1845
  activeAxes,
2321
1846
  sample,
1847
+ colorFormat,
2322
1848
  filter,
2323
1849
  caption
2324
1850
  });
@@ -2357,6 +1883,7 @@ function deriveGradientRows(resolved, listing, cssVarPrefix, { filter, sortBy, s
2357
1883
  return {
2358
1884
  path,
2359
1885
  cssVar: resolveCssVar(path, projectFields),
1886
+ token,
2360
1887
  stops: stops.map((stop, i) => ({
2361
1888
  key: stopKey(path, stop, i),
2362
1889
  cssColor: stopCssColor(stop),
@@ -2366,8 +1893,13 @@ function deriveGradientRows(resolved, listing, cssVarPrefix, { filter, sortBy, s
2366
1893
  };
2367
1894
  });
2368
1895
  }
2369
- /** Pure presentation for the gradient palette. Renders from plain props. */
2370
- function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
1896
+ /**
1897
+ * Pure presentation for the gradient palette. Renders from plain props;
1898
+ * composes the registry's `gradient` presenter per row, feeding it this
1899
+ * row's already-resolved `token`/`cssVar` per the presenter contract.
1900
+ */
1901
+ function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
1902
+ const Swatch = usePresenter("gradient");
2371
1903
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2372
1904
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2373
1905
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2394,10 +1926,11 @@ function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, filt
2394
1926
  children: row.cssVar
2395
1927
  })]
2396
1928
  }),
2397
- /* @__PURE__ */ jsx("div", {
2398
- className: "sb-gradient-palette__sample",
2399
- style: { background: `linear-gradient(to right, ${row.cssVar})` },
2400
- "aria-hidden": true
1929
+ Swatch && /* @__PURE__ */ jsx(Swatch, {
1930
+ path: row.path,
1931
+ token: row.token,
1932
+ cssVar: row.cssVar,
1933
+ colorFormat
2401
1934
  }),
2402
1935
  /* @__PURE__ */ jsx("div", {
2403
1936
  className: "sb-gradient-palette__stops",
@@ -2425,15 +1958,16 @@ function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, filt
2425
1958
  }, row.path))]
2426
1959
  });
2427
1960
  }
2428
- function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1961
+ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc", colorFormat }) {
2429
1962
  const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
2430
- const colorFormat = useColorFormat();
1963
+ const contextColorFormat = useColorFormat();
1964
+ const format = colorFormat ?? contextColorFormat;
2431
1965
  return /* @__PURE__ */ jsx(GradientPaletteView, {
2432
1966
  rows: useMemo(() => deriveGradientRows(resolved, listing, cssVarPrefix, {
2433
1967
  filter,
2434
1968
  sortBy,
2435
1969
  sortDir,
2436
- colorFormat
1970
+ colorFormat: format
2437
1971
  }), [
2438
1972
  resolved,
2439
1973
  listing,
@@ -2441,172 +1975,17 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2441
1975
  filter,
2442
1976
  sortBy,
2443
1977
  sortDir,
2444
- colorFormat
1978
+ format
2445
1979
  ]),
2446
1980
  activeTheme,
2447
1981
  cssVarPrefix,
2448
1982
  activeAxes,
1983
+ colorFormat: format,
2449
1984
  filter,
2450
1985
  caption
2451
1986
  });
2452
1987
  }
2453
1988
  //#endregion
2454
- //#region src/internal/prefers-reduced-motion.ts
2455
- function isChromatic() {
2456
- if (typeof navigator === "undefined") return false;
2457
- return navigator.userAgent.includes("Chromatic");
2458
- }
2459
- /**
2460
- * Reactive `prefers-reduced-motion: reduce` detector. Returns the current
2461
- * match and updates if the user toggles the OS-level preference. Also
2462
- * returns `true` under Chromatic to keep animated samples deterministic
2463
- * during visual regression capture.
2464
- */
2465
- function usePrefersReducedMotion() {
2466
- const [reduced, setReduced] = useState(false);
2467
- useEffect(() => {
2468
- if (typeof window === "undefined") return;
2469
- if (isChromatic()) {
2470
- setReduced(true);
2471
- return;
2472
- }
2473
- const query = window.matchMedia("(prefers-reduced-motion: reduce)");
2474
- setReduced(query.matches);
2475
- const onChange = (e) => setReduced(e.matches);
2476
- query.addEventListener("change", onChange);
2477
- return () => query.removeEventListener("change", onChange);
2478
- }, []);
2479
- return reduced;
2480
- }
2481
- //#endregion
2482
- //#region src/motion-preview/MotionSample.tsx
2483
- const DEFAULT_DURATION_MS = 300;
2484
- const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2485
- function extractDurationMs(raw) {
2486
- if (raw == null) return NaN;
2487
- if (typeof raw === "object") {
2488
- const v = raw;
2489
- if (typeof v.value === "number" && typeof v.unit === "string") {
2490
- if (v.unit === "ms") return v.value;
2491
- if (v.unit === "s") return v.value * 1e3;
2492
- }
2493
- }
2494
- return NaN;
2495
- }
2496
- function extractCubicBezier(raw) {
2497
- if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
2498
- return null;
2499
- }
2500
- function asDuration(raw, themeTokens, fallback) {
2501
- const direct = extractDurationMs(raw);
2502
- if (Number.isFinite(direct)) return direct;
2503
- if (typeof raw === "string") {
2504
- const match = raw.match(/^\{([^}]+)\}$/);
2505
- if (match && match[1]) {
2506
- const referenced = themeTokens[match[1]];
2507
- const resolved = extractDurationMs(referenced?.$value);
2508
- if (Number.isFinite(resolved)) return resolved;
2509
- }
2510
- }
2511
- return fallback;
2512
- }
2513
- function asEasing(raw, themeTokens, fallback) {
2514
- const direct = extractCubicBezier(raw);
2515
- if (direct) return direct;
2516
- if (typeof raw === "string") {
2517
- const match = raw.match(/^\{([^}]+)\}$/);
2518
- if (match && match[1]) {
2519
- const referenced = themeTokens[match[1]];
2520
- const resolved = extractCubicBezier(referenced?.$value);
2521
- if (resolved) return resolved;
2522
- }
2523
- }
2524
- return fallback;
2525
- }
2526
- function resolveMotionSpec(token, themeTokens) {
2527
- if (!token) return null;
2528
- const type = token.$type;
2529
- if (type === "transition") {
2530
- const v = token.$value ?? {};
2531
- return {
2532
- durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
2533
- easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
2534
- };
2535
- }
2536
- if (type === "duration") {
2537
- const durationMs = extractDurationMs(token.$value);
2538
- if (!Number.isFinite(durationMs)) return null;
2539
- return {
2540
- durationMs,
2541
- easing: DEFAULT_EASING
2542
- };
2543
- }
2544
- if (type === "cubicBezier") {
2545
- const easing = extractCubicBezier(token.$value);
2546
- if (!easing) return null;
2547
- return {
2548
- durationMs: DEFAULT_DURATION_MS,
2549
- easing
2550
- };
2551
- }
2552
- return null;
2553
- }
2554
- /**
2555
- * Pure derivation of a single motion token's animation spec from resolved
2556
- * project data. Extracted so it is unit-testable without React or a store.
2557
- */
2558
- function deriveMotionSample(path, project) {
2559
- return { spec: resolveMotionSpec(project.resolved[path], project.resolved) };
2560
- }
2561
- /** Pure presentation + animation for a single motion token's sample. Renders from plain props. */
2562
- function MotionSampleView({ spec, speed, runKey }) {
2563
- const reducedMotion = usePrefersReducedMotion();
2564
- const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
2565
- const easing = spec?.easing ?? DEFAULT_EASING;
2566
- const scaledDuration = Math.max(1, durationMs / speed);
2567
- const [phase, setPhase] = useState(0);
2568
- useEffect(() => {
2569
- if (reducedMotion) return;
2570
- setPhase(0);
2571
- const id = requestAnimationFrame(() => setPhase(1));
2572
- const loop = window.setInterval(() => {
2573
- setPhase((p) => p === 0 ? 1 : 0);
2574
- }, scaledDuration * 2);
2575
- return () => {
2576
- cancelAnimationFrame(id);
2577
- window.clearInterval(loop);
2578
- };
2579
- }, [
2580
- scaledDuration,
2581
- runKey,
2582
- reducedMotion
2583
- ]);
2584
- if (reducedMotion) return /* @__PURE__ */ jsxs("div", {
2585
- className: "sb-motion-sample__reduced-motion",
2586
- children: [
2587
- "Animation suppressed by ",
2588
- /* @__PURE__ */ jsx("code", { children: "prefers-reduced-motion: reduce" }),
2589
- "."
2590
- ]
2591
- });
2592
- return /* @__PURE__ */ jsx("div", {
2593
- className: "sb-motion-sample__track",
2594
- children: /* @__PURE__ */ jsx("div", {
2595
- className: cx("sb-motion-sample__ball", phase === 1 ? "sb-motion-sample__ball--end" : "sb-motion-sample__ball--start"),
2596
- style: { transition: `left ${scaledDuration}ms ${easing}` },
2597
- "aria-hidden": true
2598
- })
2599
- });
2600
- }
2601
- function MotionSample({ path, speed = 1, runKey = 0 }) {
2602
- const { spec } = deriveMotionSample(path, useProject());
2603
- return /* @__PURE__ */ jsx(MotionSampleView, {
2604
- spec,
2605
- speed,
2606
- runKey
2607
- });
2608
- }
2609
- //#endregion
2610
1989
  //#region src/MotionPreview.tsx
2611
1990
  const SPEEDS = [
2612
1991
  .25,
@@ -2641,6 +2020,7 @@ function deriveMotionRows(resolved, project, { filter }) {
2641
2020
  collected.push({
2642
2021
  path,
2643
2022
  cssVar: resolveCssVar(path, project),
2023
+ token,
2644
2024
  durationMs: spec.durationMs,
2645
2025
  easing: spec.easing,
2646
2026
  kind
@@ -2656,10 +2036,11 @@ function deriveMotionRows(resolved, project, { filter }) {
2656
2036
  * Pure presentation for the motion preview. Owns the speed/replay controls'
2657
2037
  * local UI state and the `prefers-reduced-motion` read (a browser-environment
2658
2038
  * concern, not project data); renders from the derived `rows` view-model.
2659
- * Composes the connected `MotionSample` as a child (that child reads the
2660
- * project itself).
2039
+ * Composes the connected `MotionSample` as a child, feeding it this row's
2040
+ * already-resolved `token`/`cssVar` per the presenter contract.
2661
2041
  */
2662
- function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2042
+ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
2043
+ const Sample = usePresenter("transition");
2663
2044
  const [speed, setSpeed] = useState(1);
2664
2045
  const [run, setRun] = useState(0);
2665
2046
  const reducedMotion = usePrefersReducedMotion();
@@ -2714,10 +2095,15 @@ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2714
2095
  children: formatSpec(row)
2715
2096
  })]
2716
2097
  }),
2717
- /* @__PURE__ */ jsx(MotionSample, {
2098
+ Sample && /* @__PURE__ */ jsx(Sample, {
2718
2099
  path: row.path,
2719
- speed,
2720
- runKey: run
2100
+ token: row.token,
2101
+ cssVar: row.cssVar,
2102
+ colorFormat,
2103
+ options: {
2104
+ speed,
2105
+ runKey: run
2106
+ }
2721
2107
  }),
2722
2108
  /* @__PURE__ */ jsx("span", {
2723
2109
  className: "sb-motion-preview__css-var",
@@ -2731,6 +2117,7 @@ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2731
2117
  function MotionPreview({ filter, caption }) {
2732
2118
  const project = useProject();
2733
2119
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2120
+ const colorFormat = useColorFormat();
2734
2121
  return /* @__PURE__ */ jsx(MotionPreviewView, {
2735
2122
  rows: useMemo(() => deriveMotionRows(resolved, project, { filter }), [
2736
2123
  resolved,
@@ -2740,6 +2127,7 @@ function MotionPreview({ filter, caption }) {
2740
2127
  activeTheme,
2741
2128
  cssVarPrefix,
2742
2129
  activeAxes,
2130
+ colorFormat,
2743
2131
  filter,
2744
2132
  caption
2745
2133
  });
@@ -2772,12 +2160,18 @@ function deriveOpacityRows(resolved, project, { filter, type, sortBy, sortDir })
2772
2160
  path,
2773
2161
  cssVar: resolveCssVar(path, project),
2774
2162
  opacity,
2775
- displayValue: String(opacity)
2163
+ displayValue: String(opacity),
2164
+ token
2776
2165
  };
2777
2166
  });
2778
2167
  }
2779
- /** Pure presentation for the opacity scale. Renders from plain props. */
2780
- function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleColorVar, filter, caption }) {
2168
+ /**
2169
+ * Pure presentation for the opacity scale. Renders from plain props;
2170
+ * composes the connected `OpacitySwatch` as a child, feeding it this row's
2171
+ * already-resolved `token`/`cssVar` per the presenter contract.
2172
+ */
2173
+ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleColorVar, colorFormat, filter, caption }) {
2174
+ const Swatch = usePresenter("number");
2781
2175
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2782
2176
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2783
2177
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2795,13 +2189,12 @@ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleC
2795
2189
  className: "sb-opacity-scale__grid",
2796
2190
  children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
2797
2191
  className: "sb-opacity-scale__card",
2798
- children: [/* @__PURE__ */ jsx("div", {
2799
- className: "sb-opacity-scale__swatch",
2800
- style: {
2801
- "--swatchbook-opacity-scale-color": sampleColorVar,
2802
- "--swatchbook-opacity-scale-alpha": String(row.opacity)
2803
- },
2804
- "aria-hidden": true
2192
+ children: [Swatch && /* @__PURE__ */ jsx(Swatch, {
2193
+ path: row.path,
2194
+ token: row.token,
2195
+ cssVar: row.cssVar,
2196
+ colorFormat,
2197
+ options: { sampleColorVar }
2805
2198
  }), /* @__PURE__ */ jsxs("div", {
2806
2199
  className: "sb-opacity-scale__meta",
2807
2200
  children: [
@@ -2832,6 +2225,7 @@ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleC
2832
2225
  function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2833
2226
  const project = useProject();
2834
2227
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2228
+ const colorFormat = useColorFormat();
2835
2229
  return /* @__PURE__ */ jsx(OpacityScaleView, {
2836
2230
  rows: useMemo(() => deriveOpacityRows(resolved, project, {
2837
2231
  filter,
@@ -2850,59 +2244,87 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2850
2244
  cssVarPrefix,
2851
2245
  activeAxes,
2852
2246
  sampleColorVar: resolveCssVar(sampleColor, project),
2247
+ colorFormat,
2853
2248
  filter,
2854
2249
  caption
2855
2250
  });
2856
2251
  }
2857
2252
  //#endregion
2858
2253
  //#region src/provider.tsx
2254
+ const SetAxesContext = createContext(null);
2255
+ function assemble(snapshot, activeAxes) {
2256
+ return {
2257
+ axes: snapshot.axes,
2258
+ activeAxes,
2259
+ activeTheme: tupleToName(snapshot.axes, activeAxes),
2260
+ cssVarPrefix: snapshot.cssVarPrefix,
2261
+ indicators: snapshot.indicators,
2262
+ diagnostics: snapshot.diagnostics,
2263
+ css: snapshot.css,
2264
+ listing: snapshot.listing,
2265
+ tokenGraph: snapshot.tokenGraph,
2266
+ defaultTuple: snapshot.defaultTuple,
2267
+ defaultColorFormat: snapshot.defaultColorFormat
2268
+ };
2269
+ }
2859
2270
  /**
2860
- * Wraps a tree of blocks with the token data they need to render.
2271
+ * Wraps a tree of blocks with the token data they need to render, assembled
2272
+ * from core's wire snapshot plus an active axis tuple.
2861
2273
  *
2862
- * The Storybook addon's preview decorator mounts this automatically, so
2863
- * story/MDX authors typically never see it. Outside Storybook — unit
2864
- * tests, custom React apps, non-Storybook doc sites — consumers construct
2865
- * a {@link ProjectSnapshot} (often imported from a JSON file) and wrap
2866
- * their blocks in this provider.
2274
+ * The Storybook addon's preview decorator mounts this automatically
2275
+ * (`axes` controlled by the toolbar), so story/MDX authors typically never
2276
+ * see it. Outside Storybook: unit tests, custom React apps, non-Storybook
2277
+ * doc sites. Consumers pass a {@link SnapshotForWire} (often imported from
2278
+ * a JSON file) and either drive `axes` themselves or let the provider own
2279
+ * the tuple via `defaultAxes` + {@link useSetAxes}.
2867
2280
  */
2868
- function SwatchbookProvider({ value, children }) {
2869
- return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
2870
- value,
2871
- children
2281
+ function SwatchbookProvider({ snapshot, axes, defaultAxes, presenters, mountCss = true, children }) {
2282
+ const [internalAxes, setInternalAxes] = useState(() => defaultAxes ?? snapshot.defaultTuple);
2283
+ useEffect(() => {
2284
+ if (!mountCss) return;
2285
+ ensureStyleElement(SWATCHBOOK_STYLE_ELEMENT_ID, snapshot.css);
2286
+ }, [mountCss, snapshot.css]);
2287
+ const controlled = axes !== void 0;
2288
+ const activeAxes = controlled ? axes : internalAxes;
2289
+ const value = useMemo(() => assemble(snapshot, activeAxes), [snapshot, activeAxes]);
2290
+ const merged = useMemo(() => mergePresenters(presenters), [presenters]);
2291
+ const setAxes = controlled ? null : setInternalAxes;
2292
+ return /* @__PURE__ */ jsx("div", {
2293
+ ...perAxisAttrs(snapshot.cssVarPrefix, activeAxes),
2294
+ children: /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
2295
+ value,
2296
+ children: /* @__PURE__ */ jsx(PresenterContext.Provider, {
2297
+ value: merged,
2298
+ children: /* @__PURE__ */ jsx(SetAxesContext.Provider, {
2299
+ value: setAxes,
2300
+ children
2301
+ })
2302
+ })
2303
+ })
2872
2304
  });
2873
2305
  }
2874
2306
  /**
2307
+ * Flip the active tuple on an uncontrolled {@link SwatchbookProvider}
2308
+ * (mounted with `defaultAxes`, not `axes`). Throws when the nearest
2309
+ * provider is controlled (the host owns `axes`) or absent, since there's
2310
+ * no internal state to flip in either case.
2311
+ */
2312
+ function useSetAxes() {
2313
+ const setAxes = useContext(SetAxesContext);
2314
+ if (!setAxes) throw new Error("[swatchbook-blocks] useSetAxes() requires an uncontrolled <SwatchbookProvider defaultAxes={…}> (not one given an `axes` prop, and not no provider at all).");
2315
+ return setAxes;
2316
+ }
2317
+ /**
2875
2318
  * Read the current {@link ProjectSnapshot}. Throws if called outside a
2876
2319
  * {@link SwatchbookProvider}; blocks that need to fall back to the
2877
2320
  * virtual module go through the internal `useProject()` hook instead.
2878
2321
  */
2879
2322
  function useSwatchbookData() {
2880
2323
  const value = useOptionalSwatchbookData();
2881
- if (!value) throw new Error("[swatchbook-blocks] useSwatchbookData() called outside <SwatchbookProvider>. Wrap your tree in <SwatchbookProvider value={snapshot}> or render inside a Storybook story.");
2324
+ if (!value) throw new Error("[swatchbook-blocks] useSwatchbookData() called outside <SwatchbookProvider>. Wrap your tree in <SwatchbookProvider snapshot={wire}> or render inside a Storybook story.");
2882
2325
  return value;
2883
2326
  }
2884
2327
  //#endregion
2885
- //#region src/shadow-preview/ShadowSample.tsx
2886
- /**
2887
- * Pure derivation of a single shadow token's sample data from resolved
2888
- * project data. Extracted so it is unit-testable without React or a store.
2889
- */
2890
- function deriveShadowSample(path, project) {
2891
- return { cssVar: resolveCssVar(path, project) };
2892
- }
2893
- /** Pure presentation for a single shadow token's sample. Renders from plain props. */
2894
- function ShadowSampleView({ cssVar }) {
2895
- return /* @__PURE__ */ jsx("div", {
2896
- className: "sb-shadow-sample",
2897
- style: { boxShadow: cssVar },
2898
- "aria-hidden": true
2899
- });
2900
- }
2901
- function ShadowSample({ path }) {
2902
- const { cssVar } = deriveShadowSample(path, useProject());
2903
- return /* @__PURE__ */ jsx(ShadowSampleView, { cssVar });
2904
- }
2905
- //#endregion
2906
2328
  //#region src/ShadowPreview.tsx
2907
2329
  function layerKey(path, layer, fallback) {
2908
2330
  return `${path}|${layer.offset}|${layer.blur}|${layer.spread}|${fallback}`;
@@ -2914,9 +2336,9 @@ function asLayers(raw) {
2914
2336
  }
2915
2337
  function formatLayer(layer, colorFormat) {
2916
2338
  return {
2917
- offset: `${formatDimension$1(layer.offsetX)} ${formatDimension$1(layer.offsetY)}`,
2918
- blur: formatDimension$1(layer.blur),
2919
- spread: formatDimension$1(layer.spread),
2339
+ offset: `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`,
2340
+ blur: formatDimension(layer.blur),
2341
+ spread: formatDimension(layer.spread),
2920
2342
  color: formatSubColor(layer.color, colorFormat),
2921
2343
  inset: layer.inset ? String(layer.inset) : void 0
2922
2344
  };
@@ -2935,15 +2357,17 @@ function deriveShadowRows(resolved, project, { filter, sortBy, sortDir, colorFor
2935
2357
  }).map(([path, token]) => ({
2936
2358
  path,
2937
2359
  cssVar: resolveCssVar(path, project),
2360
+ token,
2938
2361
  layers: asLayers(token.$value).map((layer) => formatLayer(layer, colorFormat))
2939
2362
  }));
2940
2363
  }
2941
2364
  /**
2942
2365
  * Pure presentation for the shadow preview. Renders from plain props;
2943
- * composes the connected `ShadowSample` as a child (that child reads the
2944
- * project itself).
2366
+ * composes the connected `ShadowSample` as a child, feeding it this row's
2367
+ * already-resolved `token`/`cssVar` per the presenter contract.
2945
2368
  */
2946
- function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2369
+ function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
2370
+ const Sample = usePresenter("shadow");
2947
2371
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2948
2372
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2949
2373
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2972,7 +2396,12 @@ function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2972
2396
  }),
2973
2397
  /* @__PURE__ */ jsx("div", {
2974
2398
  className: "sb-shadow-preview__sample-cell",
2975
- children: /* @__PURE__ */ jsx(ShadowSample, { path: row.path })
2399
+ children: Sample && /* @__PURE__ */ jsx(Sample, {
2400
+ path: row.path,
2401
+ token: row.token,
2402
+ cssVar: row.cssVar,
2403
+ colorFormat
2404
+ })
2976
2405
  }),
2977
2406
  /* @__PURE__ */ jsx("div", {
2978
2407
  className: "sb-shadow-preview__breakdown",
@@ -3017,47 +2446,35 @@ function Layer({ layer, index, total }) {
3017
2446
  })]
3018
2447
  });
3019
2448
  }
3020
- function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2449
+ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc", colorFormat }) {
3021
2450
  const project = useProject();
3022
2451
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
3023
- const colorFormat = useColorFormat();
2452
+ const contextColorFormat = useColorFormat();
2453
+ const format = colorFormat ?? contextColorFormat;
3024
2454
  return /* @__PURE__ */ jsx(ShadowPreviewView, {
3025
2455
  rows: useMemo(() => deriveShadowRows(resolved, project, {
3026
2456
  filter,
3027
2457
  sortBy,
3028
2458
  sortDir,
3029
- colorFormat
2459
+ colorFormat: format
3030
2460
  }), [
3031
2461
  resolved,
3032
2462
  project,
3033
2463
  filter,
3034
2464
  sortBy,
3035
2465
  sortDir,
3036
- colorFormat
2466
+ format
3037
2467
  ]),
3038
2468
  activeTheme,
3039
2469
  cssVarPrefix,
3040
2470
  activeAxes,
2471
+ colorFormat: format,
3041
2472
  filter,
3042
2473
  caption
3043
2474
  });
3044
2475
  }
3045
2476
  //#endregion
3046
2477
  //#region src/StrokeStylePreview.tsx
3047
- const STRING_STYLES = new Set([
3048
- "solid",
3049
- "dashed",
3050
- "dotted",
3051
- "double",
3052
- "groove",
3053
- "ridge",
3054
- "outset",
3055
- "inset"
3056
- ]);
3057
- function extractCssStyle(value) {
3058
- if (typeof value === "string" && STRING_STYLES.has(value)) return value;
3059
- return null;
3060
- }
3061
2478
  /**
3062
2479
  * Pure derivation of the preview's display rows from resolved project data.
3063
2480
  * Extracted so it is unit-testable without React or a store.
@@ -3072,12 +2489,17 @@ function deriveStrokeStyleRows(resolved, project, { filter, sortBy, sortDir }) {
3072
2489
  }).map(([path, token]) => ({
3073
2490
  path,
3074
2491
  cssVar: resolveCssVar(path, project),
3075
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
3076
- cssStyle: extractCssStyle(token.$value)
2492
+ displayValue: formatTokenValue$2(token.$value, token.$type, "raw", project.listing[path]),
2493
+ token
3077
2494
  }));
3078
2495
  }
3079
- /** Pure presentation for the stroke-style preview. Renders from plain props. */
3080
- function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2496
+ /**
2497
+ * Pure presentation for the stroke-style preview. Renders from plain props;
2498
+ * composes the connected `StrokeSample` as a child, feeding it this row's
2499
+ * already-resolved `token`/`cssVar` per the presenter contract.
2500
+ */
2501
+ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
2502
+ const Sample = usePresenter("strokeStyle");
3081
2503
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
3082
2504
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
3083
2505
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -3104,13 +2526,11 @@ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, f
3104
2526
  children: row.displayValue
3105
2527
  })]
3106
2528
  }),
3107
- row.cssStyle ? /* @__PURE__ */ jsx("div", {
3108
- className: "sb-stroke-style-sample__line",
3109
- style: { borderTopStyle: row.cssStyle },
3110
- "aria-hidden": true
3111
- }) : /* @__PURE__ */ jsx("span", {
3112
- className: "sb-stroke-style-sample__object-fallback",
3113
- children: "Object-form (dashArray + lineCap) — no pure CSS `border-style` equivalent."
2529
+ Sample && /* @__PURE__ */ jsx(Sample, {
2530
+ path: row.path,
2531
+ token: row.token,
2532
+ cssVar: row.cssVar,
2533
+ colorFormat
3114
2534
  }),
3115
2535
  /* @__PURE__ */ jsx("span", {
3116
2536
  className: "sb-stroke-style-sample__css-var",
@@ -3123,6 +2543,7 @@ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, f
3123
2543
  function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3124
2544
  const project = useProject();
3125
2545
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2546
+ const colorFormat = useColorFormat();
3126
2547
  return /* @__PURE__ */ jsx(StrokeStylePreviewView, {
3127
2548
  rows: useMemo(() => deriveStrokeStyleRows(resolved, project, {
3128
2549
  filter,
@@ -3138,6 +2559,7 @@ function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc"
3138
2559
  activeTheme,
3139
2560
  cssVarPrefix,
3140
2561
  activeAxes,
2562
+ colorFormat,
3141
2563
  filter,
3142
2564
  caption
3143
2565
  });
@@ -3280,12 +2702,13 @@ function treeHasTruncation(nodes) {
3280
2702
  }
3281
2703
  //#endregion
3282
2704
  //#region src/token-detail/AxisVariance.tsx
3283
- function AxisVariance({ path }) {
2705
+ function AxisVariance({ path, colorFormat }) {
3284
2706
  const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt } = useTokenDetailData(path);
3285
- const colorFormat = useColorFormat();
2707
+ const contextColorFormat = useColorFormat();
2708
+ const format = colorFormat ?? contextColorFormat;
3286
2709
  const tokenType = token?.$type;
3287
2710
  const isColor = tokenType === "color";
3288
- const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2711
+ const formatFn = (t) => valueFor(t, tokenType, format);
3289
2712
  const variance = useMemo(() => {
3290
2713
  const result = varianceByPath[path];
3291
2714
  if (!result) return { kind: "constant" };
@@ -3434,20 +2857,21 @@ function AxisVariance({ path }) {
3434
2857
  }
3435
2858
  function valueFor(token, $type, format) {
3436
2859
  if (!token) return "—";
3437
- return formatTokenValue(token.$value, $type, format);
2860
+ return formatTokenValue$2(token.$value, $type, format);
3438
2861
  }
3439
2862
  //#endregion
3440
2863
  //#region src/token-detail/CompositeBreakdown.tsx
3441
- function CompositeBreakdown({ path }) {
2864
+ function CompositeBreakdown({ path, colorFormat }) {
3442
2865
  const { token, resolved } = useTokenDetailData(path);
3443
- const colorFormat = useColorFormat();
2866
+ const contextColorFormat = useColorFormat();
2867
+ const format = colorFormat ?? contextColorFormat;
3444
2868
  if (!token) return null;
3445
2869
  return /* @__PURE__ */ jsx(CompositeBreakdownContent, {
3446
2870
  type: token.$type,
3447
2871
  rawValue: token.$value,
3448
2872
  partialAliasOf: token.partialAliasOf,
3449
2873
  resolved,
3450
- colorFormat
2874
+ colorFormat: format
3451
2875
  });
3452
2876
  }
3453
2877
  function CompositeBreakdownContent({ type, rawValue, partialAliasOf, resolved, colorFormat }) {
@@ -4076,7 +3500,7 @@ function deriveTokenDetail(path, token, listing, colorFormat) {
4076
3500
  if (!token) return EMPTY_DERIVED;
4077
3501
  const isColor = token.$type === "color";
4078
3502
  const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
4079
- const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
3503
+ const value = formatTokenValue$2(token.$value, token.$type, colorFormat, listing[path]);
4080
3504
  const outOfGamut = gamut?.outOfGamut ?? false;
4081
3505
  const dep = token.$deprecated;
4082
3506
  return {
@@ -4166,12 +3590,13 @@ function TokenDetailView({ path, heading, token, cssVar, activeTheme, activeAxes
4166
3590
  ]
4167
3591
  });
4168
3592
  }
4169
- function TokenDetail({ path, heading }) {
3593
+ function TokenDetail({ path, heading, colorFormat }) {
4170
3594
  const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
4171
- const colorFormat = useColorFormat();
3595
+ const contextColorFormat = useColorFormat();
3596
+ const format = colorFormat ?? contextColorFormat;
4172
3597
  const { listing } = useProject();
4173
- const derived = deriveTokenDetail(path, token, listing, colorFormat);
4174
- return /* @__PURE__ */ jsx(TokenDetailView, {
3598
+ const derived = deriveTokenDetail(path, token, listing, format);
3599
+ const view = /* @__PURE__ */ jsx(TokenDetailView, {
4175
3600
  path,
4176
3601
  ...heading !== void 0 && { heading },
4177
3602
  token,
@@ -4181,6 +3606,10 @@ function TokenDetail({ path, heading }) {
4181
3606
  cssVarPrefix,
4182
3607
  ...derived
4183
3608
  });
3609
+ return colorFormat !== void 0 ? /* @__PURE__ */ jsx(ColorFormatContext.Provider, {
3610
+ value: colorFormat,
3611
+ children: view
3612
+ }) : view;
4184
3613
  }
4185
3614
  //#endregion
4186
3615
  //#region src/internal/DetailOverlay.tsx
@@ -4730,14 +4159,14 @@ function TokenNavigatorView({ resolved, root, enabledIndicators, cssVarPrefix, a
4730
4159
  * navigation. Click a leaf to inspect it in a slide-over (unless `onSelect`
4731
4160
  * is provided, which hands the follow-up to the consumer).
4732
4161
  */
4733
- function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect, id, indicators }) {
4162
+ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect, id, indicators, colorFormat }) {
4734
4163
  const { resolved, activeTheme, activeAxes, cssVarPrefix, indicators: indicatorBaseline } = useProject();
4735
4164
  const blockKey = useBlockKey("TokenNavigator", [
4736
4165
  root,
4737
4166
  type === void 0 ? "" : typeof type === "string" ? type : type.join(","),
4738
4167
  id
4739
4168
  ]);
4740
- return /* @__PURE__ */ jsx(TokenNavigatorView, {
4169
+ const view = /* @__PURE__ */ jsx(TokenNavigatorView, {
4741
4170
  resolved,
4742
4171
  root,
4743
4172
  enabledIndicators: useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]),
@@ -4750,6 +4179,10 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4750
4179
  searchable,
4751
4180
  onSelect
4752
4181
  });
4182
+ return colorFormat !== void 0 ? /* @__PURE__ */ jsx(ColorFormatContext.Provider, {
4183
+ value: colorFormat,
4184
+ children: view
4185
+ }) : view;
4753
4186
  }
4754
4187
  function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, root, resolveInView, onNavigate, enabled, level, setsize, posinset }) {
4755
4188
  if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
@@ -4887,7 +4320,7 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4887
4320
  className: "sb-token-navigator__preview-box",
4888
4321
  children: [/* @__PURE__ */ jsx("span", {
4889
4322
  className: "sb-token-navigator__value",
4890
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4323
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4891
4324
  }), /* @__PURE__ */ jsx("span", {
4892
4325
  className: "sb-token-navigator__color-swatch",
4893
4326
  style: { background: cssVar },
@@ -4899,12 +4332,15 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4899
4332
  className: "sb-token-navigator__preview-box",
4900
4333
  children: [/* @__PURE__ */ jsx("span", {
4901
4334
  className: "sb-token-navigator__value",
4902
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4335
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4903
4336
  }), /* @__PURE__ */ jsx("span", {
4904
4337
  className: "sb-token-navigator__preview-dimension",
4905
4338
  children: /* @__PURE__ */ jsx(DimensionSample, {
4906
4339
  path,
4907
- visual: "length"
4340
+ token,
4341
+ cssVar: resolveCssVar(path, project),
4342
+ colorFormat,
4343
+ options: { visual: "length" }
4908
4344
  })
4909
4345
  })]
4910
4346
  });
@@ -4912,28 +4348,43 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4912
4348
  className: "sb-token-navigator__preview-box",
4913
4349
  children: /* @__PURE__ */ jsx("span", {
4914
4350
  className: "sb-token-navigator__preview-scaled",
4915
- children: /* @__PURE__ */ jsx(ShadowSample, { path })
4351
+ children: /* @__PURE__ */ jsx(ShadowSample, {
4352
+ path,
4353
+ token,
4354
+ cssVar: resolveCssVar(path, project),
4355
+ colorFormat
4356
+ })
4916
4357
  })
4917
4358
  });
4918
4359
  if (type === "border") return /* @__PURE__ */ jsx("span", {
4919
4360
  className: "sb-token-navigator__preview-box",
4920
4361
  children: /* @__PURE__ */ jsx("span", {
4921
4362
  className: "sb-token-navigator__preview-scaled",
4922
- children: /* @__PURE__ */ jsx(BorderSample, { path })
4363
+ children: /* @__PURE__ */ jsx(BorderSample, {
4364
+ path,
4365
+ token,
4366
+ cssVar: resolveCssVar(path, project),
4367
+ colorFormat
4368
+ })
4923
4369
  })
4924
4370
  });
4925
4371
  if (type === "transition" || type === "duration" || type === "cubicBezier") return /* @__PURE__ */ jsx("span", {
4926
4372
  className: "sb-token-navigator__preview-box",
4927
4373
  children: /* @__PURE__ */ jsx("span", {
4928
4374
  className: "sb-token-navigator__preview-motion",
4929
- children: /* @__PURE__ */ jsx(MotionSample, { path })
4375
+ children: /* @__PURE__ */ jsx(MotionSample, {
4376
+ path,
4377
+ token,
4378
+ cssVar: resolveCssVar(path, project),
4379
+ colorFormat
4380
+ })
4930
4381
  })
4931
4382
  });
4932
4383
  return /* @__PURE__ */ jsx("span", {
4933
4384
  className: "sb-token-navigator__preview-box",
4934
4385
  children: /* @__PURE__ */ jsx("span", {
4935
4386
  className: "sb-token-navigator__value",
4936
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4387
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4937
4388
  })
4938
4389
  });
4939
4390
  });
@@ -4965,7 +4416,7 @@ function deriveTokenRows(resolved, listing, cssVarPrefix, varianceByPath, { filt
4965
4416
  return {
4966
4417
  path,
4967
4418
  type: token.$type ?? "",
4968
- value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4419
+ value: formatTokenValue$2(token.$value, token.$type, colorFormat, listing[path]),
4969
4420
  outOfGamut: color?.outOfGamut ?? false,
4970
4421
  cssVar: resolveCssVar(path, projectFields),
4971
4422
  isColor,
@@ -5207,9 +4658,10 @@ function TokenTableView({ rows, activeTheme, cssVarPrefix, activeAxes, colorForm
5207
4658
  * slide-over (unless `onSelect` is provided, which hands the follow-up to the
5208
4659
  * consumer).
5209
4660
  */
5210
- function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, id, indicators }) {
4661
+ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, id, indicators, colorFormat }) {
5211
4662
  const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath, indicators: indicatorBaseline } = useProject();
5212
- const colorFormat = useColorFormat();
4663
+ const contextColorFormat = useColorFormat();
4664
+ const format = colorFormat ?? contextColorFormat;
5213
4665
  const rootFontSize = useRootFontSize();
5214
4666
  const blockKey = useBlockKey("TokenTable", [
5215
4667
  filter,
@@ -5218,13 +4670,13 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
5218
4670
  id
5219
4671
  ]);
5220
4672
  const enabledIndicators = useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]);
5221
- return /* @__PURE__ */ jsx(TokenTableView, {
4673
+ const view = /* @__PURE__ */ jsx(TokenTableView, {
5222
4674
  rows: useMemo(() => deriveTokenRows(resolved, listing, cssVarPrefix, varianceByPath, {
5223
4675
  filter,
5224
4676
  type,
5225
4677
  sortBy,
5226
4678
  sortDir,
5227
- colorFormat,
4679
+ colorFormat: format,
5228
4680
  rootFontSizePx: rootFontSize
5229
4681
  }), [
5230
4682
  resolved,
@@ -5235,13 +4687,13 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
5235
4687
  type,
5236
4688
  sortBy,
5237
4689
  sortDir,
5238
- colorFormat,
4690
+ format,
5239
4691
  rootFontSize
5240
4692
  ]),
5241
4693
  activeTheme,
5242
4694
  cssVarPrefix,
5243
4695
  activeAxes,
5244
- colorFormat,
4696
+ colorFormat: format,
5245
4697
  enabledIndicators,
5246
4698
  validPaths: useMemo(() => new Set(Object.keys(resolved)), [resolved]),
5247
4699
  blockKey,
@@ -5251,66 +4703,37 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
5251
4703
  searchable,
5252
4704
  onSelect
5253
4705
  });
4706
+ return colorFormat !== void 0 ? /* @__PURE__ */ jsx(ColorFormatContext.Provider, {
4707
+ value: colorFormat,
4708
+ children: view
4709
+ }) : view;
5254
4710
  }
5255
4711
  //#endregion
5256
4712
  //#region src/TypographyScale.tsx
5257
- function asDimension(raw) {
5258
- if (raw == null) return void 0;
5259
- if (typeof raw === "string" || typeof raw === "number") return String(raw);
5260
- if (typeof raw === "object") {
5261
- const v = raw;
5262
- if (v.value !== void 0 && v.unit !== void 0) return `${String(v.value)}${String(v.unit)}`;
5263
- }
5264
- }
5265
- function asFontFamily(raw) {
5266
- if (typeof raw === "string") return raw;
5267
- if (Array.isArray(raw)) return raw.map(String).join(", ");
5268
- }
5269
- function buildRow(path, composite) {
5270
- const fontFamily = asFontFamily(composite.fontFamily);
5271
- const fontSize = asDimension(composite.fontSize);
5272
- const fontWeight = composite.fontWeight == null ? void 0 : String(composite.fontWeight);
5273
- const lineHeight = composite.lineHeight == null ? void 0 : String(composite.lineHeight);
5274
- const letterSpacing = asDimension(composite.letterSpacing);
5275
- const sampleStyle = {};
5276
- if (fontFamily) sampleStyle.fontFamily = fontFamily;
5277
- if (fontSize) sampleStyle.fontSize = fontSize;
5278
- if (fontWeight) sampleStyle.fontWeight = fontWeight;
5279
- if (lineHeight) sampleStyle.lineHeight = lineHeight;
5280
- if (letterSpacing) sampleStyle.letterSpacing = letterSpacing;
5281
- return {
5282
- path,
5283
- sampleStyle,
5284
- specs: [
5285
- fontSize,
5286
- fontWeight ? `w${fontWeight}` : void 0,
5287
- lineHeight ? `lh ${lineHeight}` : void 0
5288
- ].filter(Boolean).join(" · ")
5289
- };
5290
- }
5291
4713
  /**
5292
4714
  * Pure derivation of the scale's display rows from resolved project data.
5293
4715
  * Extracted so it is unit-testable without React or a store.
5294
4716
  */
5295
- function deriveTypographyRows(resolved, { filter, sortBy, sortDir }) {
4717
+ function deriveTypographyRows(resolved, project, { filter, sortBy, sortDir }) {
5296
4718
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
5297
4719
  if (token.$type !== "typography") return false;
5298
4720
  return matchPath(path, filter);
5299
4721
  }), {
5300
4722
  by: sortBy,
5301
4723
  dir: sortDir
5302
- }).map(([path, token]) => {
5303
- const value = token.$value;
5304
- if (!value || typeof value !== "object") return {
5305
- path,
5306
- sampleStyle: {},
5307
- specs: ""
5308
- };
5309
- return buildRow(path, value);
5310
- });
4724
+ }).map(([path, token]) => ({
4725
+ path,
4726
+ cssVar: resolveCssVar(path, project),
4727
+ token
4728
+ }));
5311
4729
  }
5312
- /** Pure presentation for the typography scale. Renders from plain props. */
5313
- function TypographyScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
4730
+ /**
4731
+ * Pure presentation for the typography scale. Renders from plain props;
4732
+ * composes the connected `TypeSpecimen` as a child, feeding it this row's
4733
+ * already-resolved `token`/`cssVar` per the presenter contract.
4734
+ */
4735
+ function TypographyScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, colorFormat, filter, caption }) {
4736
+ const Specimen = usePresenter("typography");
5314
4737
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
5315
4738
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
5316
4739
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -5326,31 +4749,31 @@ function TypographyScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, samp
5326
4749
  children: captionText
5327
4750
  }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
5328
4751
  className: "sb-typography-scale__row",
5329
- children: [/* @__PURE__ */ jsxs("div", {
5330
- className: "sb-typography-scale__meta",
5331
- children: [/* @__PURE__ */ jsx("span", {
5332
- className: "sb-typography-scale__path",
5333
- children: row.path
5334
- }), row.specs && /* @__PURE__ */ jsx("span", {
5335
- className: "sb-typography-scale__specs",
5336
- children: row.specs
5337
- })]
5338
- }), /* @__PURE__ */ jsx("div", {
5339
- style: row.sampleStyle,
5340
- children: sample
4752
+ children: [/* @__PURE__ */ jsx("span", {
4753
+ className: "sb-typography-scale__path",
4754
+ children: row.path
4755
+ }), Specimen && /* @__PURE__ */ jsx(Specimen, {
4756
+ path: row.path,
4757
+ token: row.token,
4758
+ cssVar: row.cssVar,
4759
+ colorFormat,
4760
+ options: { sample }
5341
4761
  })]
5342
4762
  }, row.path))]
5343
4763
  });
5344
4764
  }
5345
4765
  function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
5346
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
4766
+ const project = useProject();
4767
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
4768
+ const colorFormat = useColorFormat();
5347
4769
  return /* @__PURE__ */ jsx(TypographyScaleView, {
5348
- rows: useMemo(() => deriveTypographyRows(resolved, {
4770
+ rows: useMemo(() => deriveTypographyRows(resolved, project, {
5349
4771
  filter,
5350
4772
  sortBy,
5351
4773
  sortDir
5352
4774
  }), [
5353
4775
  resolved,
4776
+ project,
5354
4777
  filter,
5355
4778
  sortBy,
5356
4779
  sortDir
@@ -5359,11 +4782,12 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
5359
4782
  cssVarPrefix,
5360
4783
  activeAxes,
5361
4784
  sample,
4785
+ colorFormat,
5362
4786
  filter,
5363
4787
  caption
5364
4788
  });
5365
4789
  }
5366
4790
  //#endregion
5367
- export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionSample, DimensionScale, FontFamilyPreview, FontWeightScale, GradientPalette, MotionPreview, MotionSample, OpacityScale, ShadowPreview, ShadowSample, StrokeStylePreview, SwatchbookProvider, TOKENS_UPDATED_EVENT, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, onChannel, registerChannel, registerTokenSource, useActiveAxes, useActiveTheme, useChannelGlobals, useColorFormat, useOptionalSwatchbookData, useSwatchbookData, useTokenSnapshot };
4791
+ export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorSwatch, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, DEFAULT_PRESENTERS, Diagnostics, DimensionSample, DimensionScale, FontFamilyPreview, FontFamilySpecimen, FontWeightScale, FontWeightSpecimen, GradientPalette, GradientSwatch, MotionPreview, MotionSample, OpacityScale, OpacitySwatch, PresenterContext, ShadowPreview, ShadowSample, StrokeSample, StrokeStylePreview, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypeSpecimen, TypographyScale, formatColor, formatTokenValue, mergePresenters, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, usePresenter, useSetAxes, useSwatchbookData };
5368
4792
 
5369
4793
  //# sourceMappingURL=index.mjs.map