@unpunnyfuns/swatchbook-blocks 1.2.0 → 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,201 +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
- //#region src/internal/channel-tokens.ts
99
- /**
100
- * Host adapter API — same integration tier as {@link BlockChannel} /
101
- * {@link registerChannel} / {@link onChannel}, distinct from the
102
- * components/hooks/`SwatchbookProvider` surface MDX/story authors use.
103
- * Most consumers never touch this; it exists for whoever builds the next
104
- * host integration.
105
- *
106
- * Live token snapshot backed by the addon's preview dev-time HMR event.
107
- *
108
- * The initial snapshot is *injected* by the addon preview via
109
- * {@link registerTokenSource} rather than imported from the addon's
110
- * `virtual:swatchbook/tokens` build artifact — so blocks carries no
111
- * dependency on that module and imports cleanly standalone (outside
112
- * Storybook, in unit tests, in the docs site). Until something registers
113
- * a source, blocks render from empty defaults.
114
- *
115
- * For dev-time updates this module subscribes to `TOKENS_UPDATED_EVENT`
116
- * on Storybook's channel (which the addon preview re-broadcasts from its
117
- * own HMR listener) and exposes the latest snapshot via
118
- * `useSyncExternalStore`, so hooks re-render in place on each token save.
119
- */
120
- const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
121
- let snapshot = {
122
- axes: [],
123
- presets: [],
124
- diagnostics: [],
125
- css: "",
126
- cssVarPrefix: "",
127
- indicators: {},
128
- listing: {},
129
- tokenGraph: {
130
- nodes: {},
131
- axes: [],
132
- axisDefaults: {},
133
- axisContexts: {}
134
- },
135
- defaultTuple: {},
136
- defaultColorFormat: "hex",
137
- version: 0
138
- };
139
- const listeners = /* @__PURE__ */ new Set();
140
- let subscribed = false;
141
- function applyPatch(patch) {
142
- snapshot = {
143
- axes: patch.axes ?? snapshot.axes,
144
- presets: patch.presets ?? snapshot.presets,
145
- diagnostics: patch.diagnostics ?? snapshot.diagnostics,
146
- css: patch.css ?? snapshot.css,
147
- cssVarPrefix: patch.cssVarPrefix ?? snapshot.cssVarPrefix,
148
- indicators: patch.indicators ?? snapshot.indicators,
149
- listing: patch.listing ?? snapshot.listing,
150
- tokenGraph: patch.tokenGraph ?? snapshot.tokenGraph,
151
- defaultTuple: patch.defaultTuple ?? snapshot.defaultTuple,
152
- defaultColorFormat: patch.defaultColorFormat ?? snapshot.defaultColorFormat,
153
- version: snapshot.version + 1
154
- };
155
- for (const cb of listeners) cb();
156
- }
157
- /**
158
- * Host adapter API — same integration tier as {@link registerChannel}.
159
- *
160
- * Seed the initial token snapshot. The addon preview calls this once at
161
- * init with the build-time `virtual:swatchbook/tokens` data. Keeping the
162
- * virtual-module read on the addon side (the package that owns it) lets
163
- * blocks import cleanly without it. No-op fields fall back to the current
164
- * snapshot, so a partial source is safe.
165
- */
166
- function registerTokenSource(source) {
167
- applyPatch(source);
168
- }
169
- function attach(channel) {
170
- if (subscribed) return;
171
- subscribed = true;
172
- channel.on(TOKENS_UPDATED_EVENT, (payload) => {
173
- applyPatch(payload);
174
- });
175
- }
176
- onChannel(attach);
177
- function subscribe$1(cb) {
178
- listeners.add(cb);
179
- return () => {
180
- listeners.delete(cb);
181
- };
182
- }
183
- function getSnapshot() {
184
- return snapshot;
185
- }
186
- function getServerSnapshot() {
187
- return snapshot;
188
- }
189
- /**
190
- * Host adapter API — same integration tier as {@link registerChannel}. Most
191
- * consumers read project data through `useSwatchbookData()` /
192
- * `SwatchbookProvider` instead; this is the internal subscription
193
- * `useProject()`'s fallback path uses to read the live channel-fed snapshot.
194
- */
195
- function useTokenSnapshot() {
196
- return useSyncExternalStore(subscribe$1, getSnapshot, getServerSnapshot);
197
- }
198
- //#endregion
199
16
  //#region src/contexts.ts
200
17
  /**
201
18
  * Context carrying the full {@link ProjectSnapshot}. `null` sentinel lets
@@ -235,12 +52,13 @@ function useActiveAxes() {
235
52
  *
236
53
  * Sits in the middle of the precedence chain a block resolves via
237
54
  * `colorFormat ?? useColorFormat()`: a block's own `colorFormat` prop wins
238
- * over this context, which wins over the active snapshot's
55
+ * over this context, which wins over the active source's
239
56
  * `defaultColorFormat` (from `Config.defaultColorFormat`). "Active
240
- * snapshot" is whichever source is actually feeding the render:
241
- * `SwatchbookProvider` when a story decorator mounted one, otherwise the
242
- * channel-fed `TokenSnapshot` that MDX-embedded blocks (no `<Story/>`, no
243
- * provider) read through `useProject()`'s fallback path.
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.
244
62
  *
245
63
  * Runs through plain React context rather than Storybook's `useGlobals` so
246
64
  * per-story seeded globals flow through on first render and the same hook
@@ -249,223 +67,17 @@ function useActiveAxes() {
249
67
  */
250
68
  const ColorFormatContext = createContext(null);
251
69
  /**
252
- * Resolves the color-display format from the context/snapshot/default
253
- * chain: `ColorFormatContext` → active snapshot's `defaultColorFormat`
254
- * `'hex'`. Composing blocks read `colorFormat ?? useColorFormat()` to give
255
- * their own `colorFormat` prop top precedence over this chain.
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.
256
75
  */
257
76
  function useColorFormat() {
258
77
  const contextValue = useContext(ColorFormatContext);
259
- const channelGlobals = useChannelGlobals();
260
78
  const snapshot = useContext(SwatchbookContext);
261
- const tokenSnapshot = useTokenSnapshot();
262
- const activeDefault = snapshot ? snapshot.defaultColorFormat : tokenSnapshot.defaultColorFormat;
263
- return contextValue ?? channelGlobals.format ?? activeDefault ?? "hex";
264
- }
265
- //#endregion
266
- //#region src/internal/use-project.ts
267
- function computeVarianceByPath(graph) {
268
- if (!graph) return {};
269
- const out = {};
270
- for (const path of listPaths(graph)) out[path] = getVariance(graph, path);
271
- return out;
272
- }
273
- function ensureStylesheet(css) {
274
- ensureStyleElement(SWATCHBOOK_STYLE_ELEMENT_ID, css);
275
- }
276
- function defaultTuple(axes) {
277
- const out = {};
278
- for (const axis of axes) out[axis.name] = axis.default;
279
- return out;
280
- }
281
- function makeResolveAt(graph) {
282
- if (!graph) return () => ({});
283
- return (tuple) => resolveAllWithProvenanceAt(graph, tuple);
284
- }
285
- function snapshotResolveAt(snapshot) {
286
- if (snapshot.resolveAt) return snapshot.resolveAt;
287
- return makeResolveAt(snapshot.tokenGraph);
288
- }
289
- /**
290
- * Reads project data either from a mounted {@link SwatchbookProvider}
291
- * (preferred — the addon's preview decorator installs one around every
292
- * story) or, when no provider is present, from the virtual module plus
293
- * Storybook globals directly.
294
- *
295
- * The provider-less path is what makes the hook safe to call from MDX
296
- * doc blocks and autodocs renders where no story is active. It
297
- * self-mounts the injected snapshot's per-theme CSS and tracks the active
298
- * tuple via the injected channel's `globalsUpdated` event, since a
299
- * story-scoped globals hook only works while a story is rendering.
300
- */
301
- function useProject() {
302
- const snapshot = useOptionalSwatchbookData();
303
- const axes = snapshot?.axes;
304
- const activeAxes = snapshot?.activeAxes;
305
- const activeTheme = snapshot?.activeTheme;
306
- const diagnostics = snapshot?.diagnostics;
307
- const cssVarPrefix = snapshot?.cssVarPrefix;
308
- const indicators = snapshot?.indicators;
309
- const listing = snapshot?.listing;
310
- const tokenGraph = snapshot?.tokenGraph;
311
- const resolveAt = useMemo(() => {
312
- if (!snapshot) return null;
313
- return snapshotResolveAt(snapshot);
314
- }, [tokenGraph, activeTheme]);
315
- const derivedVarianceByPath = useMemo(() => computeVarianceByPath(tokenGraph), [tokenGraph]);
316
- const providerData = useMemo(() => {
317
- if (!snapshot || !resolveAt || !axes || !activeAxes) return null;
318
- return {
319
- activeTheme: activeTheme ?? "",
320
- activeAxes,
321
- axes,
322
- resolved: resolveAt(activeAxes),
323
- diagnostics: diagnostics ?? [],
324
- cssVarPrefix: cssVarPrefix ?? "",
325
- indicators: indicators ?? {},
326
- listing: listing ?? {},
327
- varianceByPath: derivedVarianceByPath,
328
- resolveAt
329
- };
330
- }, [
331
- snapshot,
332
- resolveAt,
333
- axes,
334
- activeTheme,
335
- activeAxes,
336
- diagnostics,
337
- cssVarPrefix,
338
- indicators,
339
- listing,
340
- derivedVarianceByPath,
341
- tokenGraph
342
- ]);
343
- const fallback = useVirtualModuleFallback(snapshot === null);
344
- return providerData ?? fallback;
345
- }
346
- function useVirtualModuleFallback(enabled) {
347
- const contextThemeName = useActiveTheme();
348
- const contextAxes = useActiveAxes();
349
- const channelGlobals = useChannelGlobals();
350
- const tokens = useTokenSnapshot();
351
- useEffect(() => {
352
- if (!enabled) return;
353
- ensureStylesheet(tokens.css);
354
- }, [enabled, tokens.css]);
355
- const activeAxes = useMemo(() => {
356
- return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple(tokens.axes);
357
- }, [
358
- contextAxes,
359
- channelGlobals.axes,
360
- tokens.axes
361
- ]);
362
- const activeTheme = contextThemeName || tupleToName(tokens.axes, activeAxes);
363
- const resolveAt = useMemo(() => makeResolveAt(tokens.tokenGraph), [tokens.tokenGraph]);
364
- const fallbackVarianceByPath = useMemo(() => computeVarianceByPath(tokens.tokenGraph), [tokens.tokenGraph]);
365
- return useMemo(() => ({
366
- activeTheme,
367
- activeAxes,
368
- axes: tokens.axes,
369
- resolved: resolveAt(activeAxes),
370
- diagnostics: tokens.diagnostics,
371
- cssVarPrefix: tokens.cssVarPrefix,
372
- indicators: tokens.indicators,
373
- listing: tokens.listing,
374
- varianceByPath: fallbackVarianceByPath,
375
- resolveAt
376
- }), [
377
- activeTheme,
378
- activeAxes,
379
- tokens.axes,
380
- tokens.diagnostics,
381
- tokens.cssVarPrefix,
382
- tokens.indicators,
383
- tokens.listing,
384
- fallbackVarianceByPath,
385
- resolveAt
386
- ]);
387
- }
388
- /**
389
- * Resolve a token's CSS var reference, preferring the authoritative name
390
- * emitted by `@terrazzo/plugin-css` (as recorded by
391
- * `@terrazzo/plugin-token-listing` in the snapshot's `listing` field).
392
- * Falls back to `cssVarRef` when the listing lacks an entry for this
393
- * path — covers non-resolver projects, hand-built snapshots, and any
394
- * listing-plugin miss.
395
- */
396
- function resolveCssVar(path, project) {
397
- const listed = project.listing[path]?.names?.["css"];
398
- if (listed) return `var(${listed})`;
399
- return cssVarRef(path, project.cssVarPrefix);
400
- }
401
- /**
402
- * Resolve a color value's display string + gamut flag, preferring the
403
- * listing's `previewValue` when the user's active color-format matches
404
- * plugin-css's output (hex). For any other format we fall back to
405
- * `formatColor` so the toolbar's inspection modes (rgb / hsl / oklch /
406
- * raw) keep working — the listing has only one canonical format.
407
- *
408
- * Pass `path === undefined` when resolving a sub-color inside a composite
409
- * (shadow / border / gradient stop): composites' `previewValue` covers
410
- * the whole token's rendering, not the individual channel, so there's no
411
- * listing entry to key against.
412
- */
413
- function resolveColorValue(path, raw, colorFormat, project) {
414
- if (path !== void 0 && colorFormat === "hex") {
415
- const listed = project.listing[path]?.previewValue;
416
- if (typeof listed === "string") return {
417
- value: listed,
418
- outOfGamut: false
419
- };
420
- }
421
- return formatColor(raw, colorFormat);
422
- }
423
- //#endregion
424
- //#region src/border-preview/BorderSample.tsx
425
- /**
426
- * Pure derivation of a single border token's sample data from resolved
427
- * project data. Extracted so it is unit-testable without React or a store.
428
- */
429
- function deriveBorderSample(path, project) {
430
- return { cssVar: resolveCssVar(path, project) };
431
- }
432
- /** Pure presentation for a single border token's sample. Renders from plain props. */
433
- function BorderSampleView({ cssVar }) {
434
- return /* @__PURE__ */ jsx("div", {
435
- className: "sb-border-sample",
436
- style: { border: cssVar },
437
- "aria-hidden": true
438
- });
439
- }
440
- function BorderSample({ path }) {
441
- const { cssVar } = deriveBorderSample(path, useProject());
442
- return /* @__PURE__ */ jsx(BorderSampleView, { cssVar });
443
- }
444
- //#endregion
445
- //#region src/internal/composite-sample-format.ts
446
- /**
447
- * Display a composite sub-field dimension (shadow offset / blur / spread,
448
- * border width, …) in the preview tables. Renders `—` for a missing
449
- * sub-field and falls back to JSON for shapes it doesn't recognize.
450
- *
451
- * Distinct from `format-token-value`'s internal `formatDimension`, which
452
- * formats a token's top-level value and has no `—` placeholder — these are
453
- * the per-layer sample formatters shared by `ShadowPreview` + `BorderPreview`.
454
- */
455
- function formatDimension$1(raw) {
456
- if (raw == null) return "—";
457
- if (typeof raw === "number") return String(raw);
458
- if (typeof raw === "string") return raw;
459
- if (typeof raw === "object") {
460
- const v = raw;
461
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
462
- }
463
- return JSON.stringify(raw);
464
- }
465
- /** Display a composite sub-field color via the active format; `—` when absent. */
466
- function formatSubColor(raw, format) {
467
- if (raw == null) return "—";
468
- return formatColor(raw, format).value;
79
+ const source = useProjectSource();
80
+ return contextValue ?? snapshot?.defaultColorFormat ?? source.defaultColorFormat ?? "hex";
469
81
  }
470
82
  //#endregion
471
83
  //#region src/internal/data-attr.ts
@@ -622,6 +234,163 @@ function toDisplayable(v) {
622
234
  return String(v ?? "");
623
235
  }
624
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
625
394
  //#region src/BorderPreview.tsx
626
395
  /**
627
396
  * Pure derivation of the preview's display rows from resolved project data.
@@ -639,7 +408,8 @@ function deriveBorderRows(resolved, project, { filter, sortBy, sortDir, colorFor
639
408
  return {
640
409
  path,
641
410
  cssVar: resolveCssVar(path, project),
642
- width: formatDimension$1(value.width),
411
+ token,
412
+ width: formatDimension(value.width),
643
413
  style: value.style != null ? String(value.style) : "—",
644
414
  color: formatSubColor(value.color, colorFormat)
645
415
  };
@@ -647,10 +417,11 @@ function deriveBorderRows(resolved, project, { filter, sortBy, sortDir, colorFor
647
417
  }
648
418
  /**
649
419
  * Pure presentation for the border preview. Renders from plain props;
650
- * composes the connected `BorderSample` as a child (that child reads the
651
- * project itself).
420
+ * composes the connected `BorderSample` as a child, feeding it this row's
421
+ * already-resolved `token`/`cssVar` per the presenter contract.
652
422
  */
653
- function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
423
+ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
424
+ const Sample = usePresenter("border");
654
425
  const captionText = caption ?? `${rows.length} border${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
655
426
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
656
427
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -679,7 +450,12 @@ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
679
450
  }),
680
451
  /* @__PURE__ */ jsx("div", {
681
452
  className: "sb-border-preview__sample-cell",
682
- 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
+ })
683
459
  }),
684
460
  /* @__PURE__ */ jsxs("div", {
685
461
  className: "sb-border-preview__breakdown",
@@ -727,6 +503,7 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc", colo
727
503
  activeTheme,
728
504
  cssVarPrefix,
729
505
  activeAxes,
506
+ colorFormat: format,
730
507
  filter,
731
508
  caption
732
509
  });
@@ -747,7 +524,7 @@ function fixedPrefixLength(filter) {
747
524
  * Pure derivation of the palette's grouped swatch rows from resolved project
748
525
  * data. Extracted so it is unit-testable without React or a store.
749
526
  */
750
- function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, groupBy, sortBy, sortDir, colorFormat }) {
527
+ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, groupBy, sortBy, sortDir }) {
751
528
  const projectFields = {
752
529
  listing,
753
530
  cssVarPrefix
@@ -767,13 +544,11 @@ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, gro
767
544
  const groupKey = segments.slice(0, effectiveGroupBy).join(".");
768
545
  const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
769
546
  const list = bucket.get(groupKey) ?? [];
770
- const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
771
547
  list.push({
772
548
  path,
773
- leaf,
774
549
  cssVar: resolveCssVar(path, projectFields),
775
- value: formatted.value,
776
- outOfGamut: formatted.outOfGamut
550
+ leaf,
551
+ token
777
552
  });
778
553
  bucket.set(groupKey, list);
779
554
  }
@@ -782,8 +557,13 @@ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, gro
782
557
  swatches
783
558
  }));
784
559
  }
785
- /** Pure presentation for the color palette. Renders from plain props. */
786
- 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");
787
567
  const totalCount = groups.reduce((acc, { swatches }) => acc + swatches.length, 0);
788
568
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
789
569
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
@@ -805,27 +585,12 @@ function ColorPaletteView({ groups, activeTheme, cssVarPrefix, activeAxes, filte
805
585
  children: group
806
586
  }), /* @__PURE__ */ jsx("div", {
807
587
  className: "sb-color-palette__grid",
808
- children: swatches.map((swatch) => /* @__PURE__ */ jsxs("div", {
809
- className: "sb-color-palette__card",
810
- children: [/* @__PURE__ */ jsx("div", {
811
- className: "sb-color-palette__swatch",
812
- style: { background: swatch.cssVar },
813
- "aria-hidden": true
814
- }), /* @__PURE__ */ jsxs("div", {
815
- className: "sb-color-palette__meta",
816
- children: [/* @__PURE__ */ jsx("span", {
817
- className: "sb-color-palette__leaf",
818
- children: swatch.leaf
819
- }), /* @__PURE__ */ jsxs("span", {
820
- className: "sb-color-palette__value",
821
- children: [swatch.value, swatch.outOfGamut && /* @__PURE__ */ jsxs("span", {
822
- title: "Out of sRGB gamut for this format",
823
- "aria-label": "out of gamut",
824
- className: "sb-color-palette__gamut-warn",
825
- children: [" ", "⚠"]
826
- })]
827
- })]
828
- })]
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 }
829
594
  }, swatch.path))
830
595
  })]
831
596
  }, group))]
@@ -840,8 +605,7 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
840
605
  filter,
841
606
  groupBy,
842
607
  sortBy,
843
- sortDir,
844
- colorFormat: format
608
+ sortDir
845
609
  }), [
846
610
  resolved,
847
611
  listing,
@@ -849,12 +613,12 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
849
613
  filter,
850
614
  groupBy,
851
615
  sortBy,
852
- sortDir,
853
- format
616
+ sortDir
854
617
  ]),
855
618
  activeTheme,
856
619
  cssVarPrefix,
857
620
  activeAxes,
621
+ colorFormat: format,
858
622
  filter,
859
623
  caption
860
624
  });
@@ -1177,11 +941,11 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
1177
941
  /**
1178
942
  * Block UI state that survives a docs-mode remount.
1179
943
  *
1180
- * In MDX docs mode Storybook re-renders the docs container on every
1181
- * `updateGlobals` (axis flip), which unmounts and remounts the embedded
1182
- * blocks — destroying any plain `useState` (expand/collapse, selection,
1183
- * search). This is the same problem `channel-globals` solves for the
1184
- * 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
1185
949
  * across the remount, and re-seed component state from it on mount.
1186
950
  *
1187
951
  * `usePersistedState` is a drop-in `useState` whose value is mirrored to a
@@ -1801,260 +1565,43 @@ function DiagnosticsView({ caption, diagnostics, cssVarPrefix, activeAxes }) {
1801
1565
  role: "list",
1802
1566
  className: "sb-diagnostics__list",
1803
1567
  children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
1804
- className: "sb-diagnostics__row",
1805
- "aria-label": `${severityLabel[d.severity]}: ${d.message}`,
1806
- children: [/* @__PURE__ */ jsx("span", {
1807
- className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
1808
- "aria-hidden": true,
1809
- children: severityLabel[d.severity]
1810
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
1811
- className: "sb-diagnostics__meta",
1812
- children: [
1813
- d.group,
1814
- d.filename,
1815
- d.line ? `:${d.line}` : ""
1816
- ].filter(Boolean).join(" · ")
1817
- })] })]
1818
- }, diagnosticKey(d, i)))
1819
- })]
1820
- })
1821
- });
1822
- }
1823
- /**
1824
- * Render the project's load diagnostics — parser errors, resolver warnings,
1825
- * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
1826
- * when the project carries errors or warnings; stays collapsed for clean
1827
- * loads and info-only loads.
1828
- *
1829
- * Replaces the diagnostics section from the addon's (now-retired) Design
1830
- * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1831
- * on their own MDX pages.
1832
- */
1833
- function Diagnostics({ caption } = {}) {
1834
- const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1835
- return /* @__PURE__ */ jsx(DiagnosticsView, {
1836
- caption,
1837
- diagnostics,
1838
- cssVarPrefix,
1839
- activeAxes
1840
- });
1841
- }
1842
- //#endregion
1843
- //#region src/dimension-scale/dimension-px.ts
1844
- /**
1845
- * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
1846
- * purpose of deciding whether to cap the rendered size. `rootFontSizePx`
1847
- * scales `rem` against the rendering context's actual root font-size
1848
- * (default 16 for the no-DOM / SSR path); the bar paints at the same
1849
- * context's `var()`, so passing the measured root keeps the cap decision
1850
- * aligned with what's drawn. Returns `NaN` for anything other than `px` /
1851
- * `rem` — `ex` / `ch` / `%`, and the non-DTCG `em` (the `dimension` type
1852
- * permits only `px | rem`) — which the caller treats as "render at cssVar
1853
- * but don't cap".
1854
- */
1855
- function toPixels(raw, rootFontSizePx = 16) {
1856
- if (raw == null || typeof raw !== "object") return NaN;
1857
- const v = raw;
1858
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1859
- switch (v.unit) {
1860
- case "px": return v.value;
1861
- case "rem": return v.value * rootFontSizePx;
1862
- default: return NaN;
1863
- }
1864
- }
1865
- //#endregion
1866
- //#region src/internal/use-root-font-size.ts
1867
- function readRootFontSize() {
1868
- if (typeof document === "undefined") return 16;
1869
- const fontSize = Number.parseFloat(getComputedStyle(document.documentElement).fontSize);
1870
- return Number.isFinite(fontSize) && fontSize > 0 ? fontSize : 16;
1871
- }
1872
- function subscribe(onChange) {
1873
- if (typeof window === "undefined") return () => {};
1874
- window.addEventListener("resize", onChange);
1875
- return () => window.removeEventListener("resize", onChange);
1876
- }
1877
- /**
1878
- * Root font-size (px) of the context a block renders in, tracked across
1879
- * viewport changes. `rem` dimension values resolve their `var()` against this
1880
- * root, so the cap math (`toPixels`) and value sort (`sortTokens`) must scale
1881
- * `rem` by it rather than a literal 16: a Storybook Docs page, or a responsive
1882
- * desktop/tablet/mobile breakpoint, can apply a different root to the same
1883
- * token. Falls back to 16 with no DOM.
1884
- */
1885
- function useRootFontSize() {
1886
- return useSyncExternalStore(subscribe, readRootFontSize, () => 16);
1887
- }
1888
- //#endregion
1889
- //#region src/dimension-scale/DimensionSample.tsx
1890
- /**
1891
- * Pure derivation of a single dimension token's sample geometry from
1892
- * resolved project data. Extracted so it is unit-testable without React or
1893
- * a store.
1894
- */
1895
- function deriveDimensionSample(path, project, rootFontSizePx) {
1896
- const cssVar = resolveCssVar(path, project);
1897
- const token = project.resolved[path];
1898
- const pxValue = toPixels(token?.$value, rootFontSizePx);
1899
- const capped = Number.isFinite(pxValue) && pxValue > 480;
1900
- return {
1901
- cssVar,
1902
- pxValue,
1903
- capped,
1904
- cappedValue: capped ? `480px` : cssVar
1905
- };
1906
- }
1907
- function withCap(visual) {
1908
- return /* @__PURE__ */ jsxs("span", {
1909
- className: "sb-dimension-sample sb-dimension-sample--capped",
1910
- title: `capped at 480px`,
1911
- children: [visual, /* @__PURE__ */ jsx("span", {
1912
- className: "sb-dimension-sample__cap",
1913
- "aria-hidden": true,
1914
- children: "…"
1915
- })]
1916
- });
1917
- }
1918
- /** Pure presentation for a single dimension token's bar/sample. Renders from plain props. */
1919
- function DimensionSampleView({ cssVar, capped, cappedValue, visual }) {
1920
- switch (visual) {
1921
- case "radius": return /* @__PURE__ */ jsx("div", {
1922
- className: "sb-dimension-sample__radius-sample",
1923
- style: { borderRadius: cssVar },
1924
- "aria-hidden": true
1925
- });
1926
- case "size": {
1927
- const sample = /* @__PURE__ */ jsx("div", {
1928
- className: "sb-dimension-sample__size-sample",
1929
- style: {
1930
- width: cappedValue,
1931
- height: cappedValue
1932
- },
1933
- "aria-hidden": true
1934
- });
1935
- return capped ? withCap(sample) : sample;
1936
- }
1937
- default: {
1938
- const bar = /* @__PURE__ */ jsx("div", {
1939
- className: "sb-dimension-sample__bar",
1940
- style: { width: cappedValue },
1941
- "aria-hidden": true
1942
- });
1943
- return capped ? withCap(bar) : bar;
1944
- }
1945
- }
1946
- }
1947
- /** Connected block: resolves `path` against the active project and renders its dimension sample. */
1948
- function DimensionSample({ path, visual = "length" }) {
1949
- const { cssVar, capped, cappedValue } = deriveDimensionSample(path, useProject(), useRootFontSize());
1950
- return /* @__PURE__ */ jsx(DimensionSampleView, {
1951
- cssVar,
1952
- capped,
1953
- cappedValue,
1954
- visual
1568
+ className: "sb-diagnostics__row",
1569
+ "aria-label": `${severityLabel[d.severity]}: ${d.message}`,
1570
+ children: [/* @__PURE__ */ jsx("span", {
1571
+ className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
1572
+ "aria-hidden": true,
1573
+ children: severityLabel[d.severity]
1574
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
1575
+ className: "sb-diagnostics__meta",
1576
+ children: [
1577
+ d.group,
1578
+ d.filename,
1579
+ d.line ? `:${d.line}` : ""
1580
+ ].filter(Boolean).join(" · ")
1581
+ })] })]
1582
+ }, diagnosticKey(d, i)))
1583
+ })]
1584
+ })
1955
1585
  });
1956
1586
  }
1957
- //#endregion
1958
- //#region src/internal/format-token-value.ts
1959
1587
  /**
1960
- * Single-line display string for any DTCG token `$value`. Prefers
1961
- * plugin-css's `previewValue` from the Token Listing; for color
1962
- * tokens only when the toolbar format is hex (other formats route
1963
- * 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.
1964
1596
  */
1965
- function formatTokenValue(value, $type, colorFormat, listingEntry) {
1966
- if (value == null) return "";
1967
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
1968
- const preview = listingEntry?.previewValue;
1969
- if (preview !== void 0) {
1970
- const previewStr = typeof preview === "string" ? cleanFloatNoise(preview) : String(preview);
1971
- if ($type !== "color") return previewStr;
1972
- if (colorFormat === "hex") return previewStr;
1973
- }
1974
- switch ($type) {
1975
- case "color": return formatColor(value, colorFormat).value;
1976
- case "dimension":
1977
- case "duration": return formatDimension(value);
1978
- case "fontFamily": return formatFontFamily$1(value);
1979
- case "fontWeight":
1980
- case "lineHeight":
1981
- case "letterSpacing":
1982
- case "opacity":
1983
- case "number": return formatPrimitive$1(value);
1984
- case "cubicBezier": return formatCubicBezier(value);
1985
- case "strokeStyle": return formatStrokeStyle(value);
1986
- case "shadow": return formatShadow(value, colorFormat);
1987
- case "border": return formatBorder(value, colorFormat);
1988
- default: return formatUnknown(value);
1989
- }
1990
- }
1991
- function formatDimension(v) {
1992
- if (typeof v === "string" || typeof v === "number") return String(v);
1993
- if (v && typeof v === "object") {
1994
- const d = v;
1995
- if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
1996
- }
1997
- return formatUnknown(v);
1998
- }
1999
- function cleanFloatNoise(s) {
2000
- return s.replace(/-?\d+\.\d{8,}/g, (m) => `${+Number(m).toFixed(3)}`);
2001
- }
2002
- function formatFontFamily$1(v) {
2003
- if (typeof v === "string") return v;
2004
- if (Array.isArray(v)) return v.map(String).join(", ");
2005
- return formatUnknown(v);
2006
- }
2007
- function formatPrimitive$1(v) {
2008
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
2009
- return formatUnknown(v);
2010
- }
2011
- function formatCubicBezier(v) {
2012
- if (Array.isArray(v) && v.length === 4) return `cubic-bezier(${v.map((n) => typeof n === "number" ? n : 0).join(", ")})`;
2013
- return formatUnknown(v);
2014
- }
2015
- function formatStrokeStyle(v) {
2016
- if (typeof v === "string") return v;
2017
- if (v && typeof v === "object") {
2018
- const s = v;
2019
- const parts = ["dashed"];
2020
- if (Array.isArray(s.dashArray)) parts.push(s.dashArray.map((n) => formatDimension(n)).join(" "));
2021
- if (typeof s.lineCap === "string") parts.push(s.lineCap);
2022
- return parts.join(" · ");
2023
- }
2024
- return formatUnknown(v);
2025
- }
2026
- function formatShadow(v, colorFormat) {
2027
- return (Array.isArray(v) ? v : [v]).map((layer) => {
2028
- if (!layer || typeof layer !== "object") return formatUnknown(layer);
2029
- const s = layer;
2030
- const pieces = [
2031
- formatDimension(s.offsetX),
2032
- formatDimension(s.offsetY),
2033
- formatDimension(s.blur),
2034
- formatDimension(s.spread),
2035
- formatColor(s.color, colorFormat).value
2036
- ].filter((p) => p !== "");
2037
- if (s.inset) pieces.push("inset");
2038
- return pieces.join(" ");
2039
- }).join(", ");
2040
- }
2041
- function formatBorder(v, colorFormat) {
2042
- if (!v || typeof v !== "object") return formatUnknown(v);
2043
- const b = v;
2044
- return [
2045
- formatDimension(b.width),
2046
- formatPrimitive$1(b.style),
2047
- formatColor(b.color, colorFormat).value
2048
- ].filter((p) => p !== "").join(" ");
2049
- }
2050
- function formatUnknown(v) {
2051
- if (v == null) return "";
2052
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
2053
- try {
2054
- return JSON.stringify(v).slice(0, 120);
2055
- } catch {
2056
- return String(v);
2057
- }
1597
+ function Diagnostics({ caption } = {}) {
1598
+ const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1599
+ return /* @__PURE__ */ jsx(DiagnosticsView, {
1600
+ caption,
1601
+ diagnostics,
1602
+ cssVarPrefix,
1603
+ activeAxes
1604
+ });
2058
1605
  }
2059
1606
  //#endregion
2060
1607
  //#region src/DimensionScale.tsx
@@ -2073,15 +1620,17 @@ function deriveDimensionRows(resolved, project, { filter, sortBy, sortDir, rootF
2073
1620
  }).map(([path, token]) => ({
2074
1621
  path,
2075
1622
  cssVar: resolveCssVar(path, project),
2076
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path])
1623
+ token,
1624
+ displayValue: formatTokenValue$2(token.$value, token.$type, "raw", project.listing[path])
2077
1625
  }));
2078
1626
  }
2079
1627
  /**
2080
1628
  * Pure presentation for the dimension scale. Renders from plain props;
2081
- * composes the connected `DimensionSample` as a child (that child reads the
2082
- * project itself).
1629
+ * composes the connected `DimensionSample` as a child, feeding it this row's
1630
+ * already-resolved `token`/`cssVar` per the presenter contract.
2083
1631
  */
2084
- 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");
2085
1634
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2086
1635
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2087
1636
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2110,9 +1659,12 @@ function DimensionScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, visua
2110
1659
  }),
2111
1660
  /* @__PURE__ */ jsx("div", {
2112
1661
  className: "sb-dimension-scale__visual-cell",
2113
- children: /* @__PURE__ */ jsx(DimensionSample, {
1662
+ children: Sample && /* @__PURE__ */ jsx(Sample, {
2114
1663
  path: row.path,
2115
- visual
1664
+ token: row.token,
1665
+ cssVar: row.cssVar,
1666
+ colorFormat,
1667
+ options: { visual }
2116
1668
  })
2117
1669
  }),
2118
1670
  /* @__PURE__ */ jsx("span", {
@@ -2127,6 +1679,7 @@ function DimensionScale({ filter, visual = "length", caption, sortBy = "value",
2127
1679
  const project = useProject();
2128
1680
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2129
1681
  const rootFontSize = useRootFontSize();
1682
+ const colorFormat = useColorFormat();
2130
1683
  return /* @__PURE__ */ jsx(DimensionScaleView, {
2131
1684
  rows: useMemo(() => deriveDimensionRows(resolved, project, {
2132
1685
  filter,
@@ -2145,17 +1698,13 @@ function DimensionScale({ filter, visual = "length", caption, sortBy = "value",
2145
1698
  cssVarPrefix,
2146
1699
  activeAxes,
2147
1700
  visual,
1701
+ colorFormat,
2148
1702
  filter,
2149
1703
  caption
2150
1704
  });
2151
1705
  }
2152
1706
  //#endregion
2153
1707
  //#region src/FontFamilyPreview.tsx
2154
- function stackString(raw) {
2155
- if (typeof raw === "string") return raw;
2156
- if (Array.isArray(raw)) return raw.map(String).join(", ");
2157
- return "";
2158
- }
2159
1708
  /**
2160
1709
  * Pure derivation of the preview's display rows from resolved project data.
2161
1710
  * Extracted so it is unit-testable without React or a store.
@@ -2170,11 +1719,16 @@ function deriveFontFamilyRows(resolved, project, { filter, sortBy, sortDir }) {
2170
1719
  }).map(([path, token]) => ({
2171
1720
  path,
2172
1721
  cssVar: resolveCssVar(path, project),
2173
- stack: stackString(token.$value)
1722
+ token
2174
1723
  }));
2175
1724
  }
2176
- /** Pure presentation for the font-family preview. Renders from plain props. */
2177
- 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");
2178
1732
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2179
1733
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2180
1734
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2188,35 +1742,19 @@ function FontFamilyPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, sa
2188
1742
  children: [/* @__PURE__ */ jsx("div", {
2189
1743
  className: "sb-block__caption",
2190
1744
  children: captionText
2191
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2192
- className: "sb-font-family-sample__row",
2193
- children: [
2194
- /* @__PURE__ */ jsxs("div", {
2195
- className: "sb-font-family-sample__meta",
2196
- children: [/* @__PURE__ */ jsx("span", {
2197
- className: "sb-font-family-sample__path",
2198
- children: row.path
2199
- }), /* @__PURE__ */ jsx("span", {
2200
- className: "sb-font-family-sample__stack",
2201
- children: row.stack
2202
- })]
2203
- }),
2204
- /* @__PURE__ */ jsx("div", {
2205
- className: "sb-font-family-sample__sample",
2206
- style: { fontFamily: row.cssVar },
2207
- children: sample
2208
- }),
2209
- /* @__PURE__ */ jsx("span", {
2210
- className: "sb-font-family-sample__css-var",
2211
- children: row.cssVar
2212
- })
2213
- ]
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 }
2214
1751
  }, row.path))]
2215
1752
  });
2216
1753
  }
2217
1754
  function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
2218
1755
  const project = useProject();
2219
1756
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1757
+ const colorFormat = useColorFormat();
2220
1758
  return /* @__PURE__ */ jsx(FontFamilyPreviewView, {
2221
1759
  rows: useMemo(() => deriveFontFamilyRows(resolved, project, {
2222
1760
  filter,
@@ -2233,35 +1771,13 @@ function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over th
2233
1771
  cssVarPrefix,
2234
1772
  activeAxes,
2235
1773
  sample,
1774
+ colorFormat,
2236
1775
  filter,
2237
1776
  caption
2238
1777
  });
2239
1778
  }
2240
1779
  //#endregion
2241
- //#region src/internal/css-var-style.ts
2242
- /**
2243
- * Coerce a CSS `var(--…)` reference into the numeric slot of a React
2244
- * inline-style property.
2245
- *
2246
- * React's `CSSProperties` types unitless properties (`fontWeight`,
2247
- * `lineHeight`, `zIndex`, …) as `number`. The DOM accepts a string at
2248
- * runtime — the rendered stylesheet just receives whatever React passes —
2249
- * so a `var(--font-weight)` reference works functionally. TypeScript still
2250
- * complains. Centralising the type assertion in one named helper keeps
2251
- * the gap visible (and greppable) instead of scattering casts across
2252
- * block components that want CSS-var-driven typography or layout values.
2253
- */
2254
- const cssVarAsNumber = (varRef) => varRef;
2255
- //#endregion
2256
1780
  //#region src/FontWeightScale.tsx
2257
- function toWeight(raw) {
2258
- if (typeof raw === "number") return raw;
2259
- if (typeof raw === "string") {
2260
- const n = Number.parseInt(raw, 10);
2261
- return Number.isFinite(n) ? n : NaN;
2262
- }
2263
- return NaN;
2264
- }
2265
1781
  /**
2266
1782
  * Pure derivation of the scale's display rows from resolved project data.
2267
1783
  * Extracted so it is unit-testable without React or a store.
@@ -2276,12 +1792,16 @@ function deriveFontWeightRows(resolved, project, { filter, sortBy, sortDir }) {
2276
1792
  }).map(([path, token]) => ({
2277
1793
  path,
2278
1794
  cssVar: resolveCssVar(path, project),
2279
- display: token.$value == null ? "" : String(token.$value),
2280
- weight: toWeight(token.$value)
1795
+ token
2281
1796
  }));
2282
1797
  }
2283
- /** Pure presentation for the font-weight scale. Renders from plain props. */
2284
- 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");
2285
1805
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2286
1806
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2287
1807
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2295,35 +1815,19 @@ function FontWeightScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, samp
2295
1815
  children: [/* @__PURE__ */ jsx("div", {
2296
1816
  className: "sb-block__caption",
2297
1817
  children: captionText
2298
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2299
- className: "sb-font-weight-scale__row",
2300
- children: [
2301
- /* @__PURE__ */ jsxs("div", {
2302
- className: "sb-font-weight-scale__meta",
2303
- children: [/* @__PURE__ */ jsx("span", {
2304
- className: "sb-font-weight-scale__path",
2305
- children: row.path
2306
- }), /* @__PURE__ */ jsx("span", {
2307
- className: "sb-font-weight-scale__value",
2308
- children: row.display
2309
- })]
2310
- }),
2311
- /* @__PURE__ */ jsx("div", {
2312
- className: "sb-font-weight-scale__sample",
2313
- style: { fontWeight: cssVarAsNumber(row.cssVar) },
2314
- children: sample
2315
- }),
2316
- /* @__PURE__ */ jsx("span", {
2317
- className: "sb-font-weight-scale__css-var",
2318
- children: row.cssVar
2319
- })
2320
- ]
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 }
2321
1824
  }, row.path))]
2322
1825
  });
2323
1826
  }
2324
1827
  function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
2325
1828
  const project = useProject();
2326
1829
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1830
+ const colorFormat = useColorFormat();
2327
1831
  return /* @__PURE__ */ jsx(FontWeightScaleView, {
2328
1832
  rows: useMemo(() => deriveFontWeightRows(resolved, project, {
2329
1833
  filter,
@@ -2340,6 +1844,7 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
2340
1844
  cssVarPrefix,
2341
1845
  activeAxes,
2342
1846
  sample,
1847
+ colorFormat,
2343
1848
  filter,
2344
1849
  caption
2345
1850
  });
@@ -2378,6 +1883,7 @@ function deriveGradientRows(resolved, listing, cssVarPrefix, { filter, sortBy, s
2378
1883
  return {
2379
1884
  path,
2380
1885
  cssVar: resolveCssVar(path, projectFields),
1886
+ token,
2381
1887
  stops: stops.map((stop, i) => ({
2382
1888
  key: stopKey(path, stop, i),
2383
1889
  cssColor: stopCssColor(stop),
@@ -2387,8 +1893,13 @@ function deriveGradientRows(resolved, listing, cssVarPrefix, { filter, sortBy, s
2387
1893
  };
2388
1894
  });
2389
1895
  }
2390
- /** Pure presentation for the gradient palette. Renders from plain props. */
2391
- 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");
2392
1903
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2393
1904
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2394
1905
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2415,10 +1926,11 @@ function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, filt
2415
1926
  children: row.cssVar
2416
1927
  })]
2417
1928
  }),
2418
- /* @__PURE__ */ jsx("div", {
2419
- className: "sb-gradient-palette__sample",
2420
- style: { background: `linear-gradient(to right, ${row.cssVar})` },
2421
- "aria-hidden": true
1929
+ Swatch && /* @__PURE__ */ jsx(Swatch, {
1930
+ path: row.path,
1931
+ token: row.token,
1932
+ cssVar: row.cssVar,
1933
+ colorFormat
2422
1934
  }),
2423
1935
  /* @__PURE__ */ jsx("div", {
2424
1936
  className: "sb-gradient-palette__stops",
@@ -2468,167 +1980,12 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc", co
2468
1980
  activeTheme,
2469
1981
  cssVarPrefix,
2470
1982
  activeAxes,
1983
+ colorFormat: format,
2471
1984
  filter,
2472
1985
  caption
2473
1986
  });
2474
1987
  }
2475
1988
  //#endregion
2476
- //#region src/internal/prefers-reduced-motion.ts
2477
- function isChromatic() {
2478
- if (typeof navigator === "undefined") return false;
2479
- return navigator.userAgent.includes("Chromatic");
2480
- }
2481
- /**
2482
- * Reactive `prefers-reduced-motion: reduce` detector. Returns the current
2483
- * match and updates if the user toggles the OS-level preference. Also
2484
- * returns `true` under Chromatic to keep animated samples deterministic
2485
- * during visual regression capture.
2486
- */
2487
- function usePrefersReducedMotion() {
2488
- const [reduced, setReduced] = useState(false);
2489
- useEffect(() => {
2490
- if (typeof window === "undefined") return;
2491
- if (isChromatic()) {
2492
- setReduced(true);
2493
- return;
2494
- }
2495
- const query = window.matchMedia("(prefers-reduced-motion: reduce)");
2496
- setReduced(query.matches);
2497
- const onChange = (e) => setReduced(e.matches);
2498
- query.addEventListener("change", onChange);
2499
- return () => query.removeEventListener("change", onChange);
2500
- }, []);
2501
- return reduced;
2502
- }
2503
- //#endregion
2504
- //#region src/motion-preview/MotionSample.tsx
2505
- const DEFAULT_DURATION_MS = 300;
2506
- const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2507
- function extractDurationMs(raw) {
2508
- if (raw == null) return NaN;
2509
- if (typeof raw === "object") {
2510
- const v = raw;
2511
- if (typeof v.value === "number" && typeof v.unit === "string") {
2512
- if (v.unit === "ms") return v.value;
2513
- if (v.unit === "s") return v.value * 1e3;
2514
- }
2515
- }
2516
- return NaN;
2517
- }
2518
- function extractCubicBezier(raw) {
2519
- if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
2520
- return null;
2521
- }
2522
- function asDuration(raw, themeTokens, fallback) {
2523
- const direct = extractDurationMs(raw);
2524
- if (Number.isFinite(direct)) return direct;
2525
- if (typeof raw === "string") {
2526
- const match = raw.match(/^\{([^}]+)\}$/);
2527
- if (match && match[1]) {
2528
- const referenced = themeTokens[match[1]];
2529
- const resolved = extractDurationMs(referenced?.$value);
2530
- if (Number.isFinite(resolved)) return resolved;
2531
- }
2532
- }
2533
- return fallback;
2534
- }
2535
- function asEasing(raw, themeTokens, fallback) {
2536
- const direct = extractCubicBezier(raw);
2537
- if (direct) return direct;
2538
- if (typeof raw === "string") {
2539
- const match = raw.match(/^\{([^}]+)\}$/);
2540
- if (match && match[1]) {
2541
- const referenced = themeTokens[match[1]];
2542
- const resolved = extractCubicBezier(referenced?.$value);
2543
- if (resolved) return resolved;
2544
- }
2545
- }
2546
- return fallback;
2547
- }
2548
- function resolveMotionSpec(token, themeTokens) {
2549
- if (!token) return null;
2550
- const type = token.$type;
2551
- if (type === "transition") {
2552
- const v = token.$value ?? {};
2553
- return {
2554
- durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
2555
- easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
2556
- };
2557
- }
2558
- if (type === "duration") {
2559
- const durationMs = extractDurationMs(token.$value);
2560
- if (!Number.isFinite(durationMs)) return null;
2561
- return {
2562
- durationMs,
2563
- easing: DEFAULT_EASING
2564
- };
2565
- }
2566
- if (type === "cubicBezier") {
2567
- const easing = extractCubicBezier(token.$value);
2568
- if (!easing) return null;
2569
- return {
2570
- durationMs: DEFAULT_DURATION_MS,
2571
- easing
2572
- };
2573
- }
2574
- return null;
2575
- }
2576
- /**
2577
- * Pure derivation of a single motion token's animation spec from resolved
2578
- * project data. Extracted so it is unit-testable without React or a store.
2579
- */
2580
- function deriveMotionSample(path, project) {
2581
- return { spec: resolveMotionSpec(project.resolved[path], project.resolved) };
2582
- }
2583
- /** Pure presentation + animation for a single motion token's sample. Renders from plain props. */
2584
- function MotionSampleView({ spec, speed, runKey }) {
2585
- const reducedMotion = usePrefersReducedMotion();
2586
- const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
2587
- const easing = spec?.easing ?? DEFAULT_EASING;
2588
- const scaledDuration = Math.max(1, durationMs / speed);
2589
- const [phase, setPhase] = useState(0);
2590
- useEffect(() => {
2591
- if (reducedMotion) return;
2592
- setPhase(0);
2593
- const id = requestAnimationFrame(() => setPhase(1));
2594
- const loop = window.setInterval(() => {
2595
- setPhase((p) => p === 0 ? 1 : 0);
2596
- }, scaledDuration * 2);
2597
- return () => {
2598
- cancelAnimationFrame(id);
2599
- window.clearInterval(loop);
2600
- };
2601
- }, [
2602
- scaledDuration,
2603
- runKey,
2604
- reducedMotion
2605
- ]);
2606
- if (reducedMotion) return /* @__PURE__ */ jsxs("div", {
2607
- className: "sb-motion-sample__reduced-motion",
2608
- children: [
2609
- "Animation suppressed by ",
2610
- /* @__PURE__ */ jsx("code", { children: "prefers-reduced-motion: reduce" }),
2611
- "."
2612
- ]
2613
- });
2614
- return /* @__PURE__ */ jsx("div", {
2615
- className: "sb-motion-sample__track",
2616
- children: /* @__PURE__ */ jsx("div", {
2617
- className: cx("sb-motion-sample__ball", phase === 1 ? "sb-motion-sample__ball--end" : "sb-motion-sample__ball--start"),
2618
- style: { transition: `left ${scaledDuration}ms ${easing}` },
2619
- "aria-hidden": true
2620
- })
2621
- });
2622
- }
2623
- function MotionSample({ path, speed = 1, runKey = 0 }) {
2624
- const { spec } = deriveMotionSample(path, useProject());
2625
- return /* @__PURE__ */ jsx(MotionSampleView, {
2626
- spec,
2627
- speed,
2628
- runKey
2629
- });
2630
- }
2631
- //#endregion
2632
1989
  //#region src/MotionPreview.tsx
2633
1990
  const SPEEDS = [
2634
1991
  .25,
@@ -2663,6 +2020,7 @@ function deriveMotionRows(resolved, project, { filter }) {
2663
2020
  collected.push({
2664
2021
  path,
2665
2022
  cssVar: resolveCssVar(path, project),
2023
+ token,
2666
2024
  durationMs: spec.durationMs,
2667
2025
  easing: spec.easing,
2668
2026
  kind
@@ -2678,10 +2036,11 @@ function deriveMotionRows(resolved, project, { filter }) {
2678
2036
  * Pure presentation for the motion preview. Owns the speed/replay controls'
2679
2037
  * local UI state and the `prefers-reduced-motion` read (a browser-environment
2680
2038
  * concern, not project data); renders from the derived `rows` view-model.
2681
- * Composes the connected `MotionSample` as a child (that child reads the
2682
- * project itself).
2039
+ * Composes the connected `MotionSample` as a child, feeding it this row's
2040
+ * already-resolved `token`/`cssVar` per the presenter contract.
2683
2041
  */
2684
- function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2042
+ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
2043
+ const Sample = usePresenter("transition");
2685
2044
  const [speed, setSpeed] = useState(1);
2686
2045
  const [run, setRun] = useState(0);
2687
2046
  const reducedMotion = usePrefersReducedMotion();
@@ -2736,10 +2095,15 @@ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2736
2095
  children: formatSpec(row)
2737
2096
  })]
2738
2097
  }),
2739
- /* @__PURE__ */ jsx(MotionSample, {
2098
+ Sample && /* @__PURE__ */ jsx(Sample, {
2740
2099
  path: row.path,
2741
- speed,
2742
- runKey: run
2100
+ token: row.token,
2101
+ cssVar: row.cssVar,
2102
+ colorFormat,
2103
+ options: {
2104
+ speed,
2105
+ runKey: run
2106
+ }
2743
2107
  }),
2744
2108
  /* @__PURE__ */ jsx("span", {
2745
2109
  className: "sb-motion-preview__css-var",
@@ -2753,6 +2117,7 @@ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2753
2117
  function MotionPreview({ filter, caption }) {
2754
2118
  const project = useProject();
2755
2119
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2120
+ const colorFormat = useColorFormat();
2756
2121
  return /* @__PURE__ */ jsx(MotionPreviewView, {
2757
2122
  rows: useMemo(() => deriveMotionRows(resolved, project, { filter }), [
2758
2123
  resolved,
@@ -2762,6 +2127,7 @@ function MotionPreview({ filter, caption }) {
2762
2127
  activeTheme,
2763
2128
  cssVarPrefix,
2764
2129
  activeAxes,
2130
+ colorFormat,
2765
2131
  filter,
2766
2132
  caption
2767
2133
  });
@@ -2794,12 +2160,18 @@ function deriveOpacityRows(resolved, project, { filter, type, sortBy, sortDir })
2794
2160
  path,
2795
2161
  cssVar: resolveCssVar(path, project),
2796
2162
  opacity,
2797
- displayValue: String(opacity)
2163
+ displayValue: String(opacity),
2164
+ token
2798
2165
  };
2799
2166
  });
2800
2167
  }
2801
- /** Pure presentation for the opacity scale. Renders from plain props. */
2802
- 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");
2803
2175
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2804
2176
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2805
2177
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2817,13 +2189,12 @@ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleC
2817
2189
  className: "sb-opacity-scale__grid",
2818
2190
  children: rows.map((row) => /* @__PURE__ */ jsxs("div", {
2819
2191
  className: "sb-opacity-scale__card",
2820
- children: [/* @__PURE__ */ jsx("div", {
2821
- className: "sb-opacity-scale__swatch",
2822
- style: {
2823
- "--swatchbook-opacity-scale-color": sampleColorVar,
2824
- "--swatchbook-opacity-scale-alpha": String(row.opacity)
2825
- },
2826
- "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 }
2827
2198
  }), /* @__PURE__ */ jsxs("div", {
2828
2199
  className: "sb-opacity-scale__meta",
2829
2200
  children: [
@@ -2854,6 +2225,7 @@ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleC
2854
2225
  function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2855
2226
  const project = useProject();
2856
2227
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2228
+ const colorFormat = useColorFormat();
2857
2229
  return /* @__PURE__ */ jsx(OpacityScaleView, {
2858
2230
  rows: useMemo(() => deriveOpacityRows(resolved, project, {
2859
2231
  filter,
@@ -2872,59 +2244,87 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2872
2244
  cssVarPrefix,
2873
2245
  activeAxes,
2874
2246
  sampleColorVar: resolveCssVar(sampleColor, project),
2247
+ colorFormat,
2875
2248
  filter,
2876
2249
  caption
2877
2250
  });
2878
2251
  }
2879
2252
  //#endregion
2880
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
+ }
2881
2270
  /**
2882
- * 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.
2883
2273
  *
2884
- * The Storybook addon's preview decorator mounts this automatically, so
2885
- * story/MDX authors typically never see it. Outside Storybook — unit
2886
- * tests, custom React apps, non-Storybook doc sites — consumers construct
2887
- * a {@link ProjectSnapshot} (often imported from a JSON file) and wrap
2888
- * 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}.
2889
2280
  */
2890
- function SwatchbookProvider({ value, children }) {
2891
- return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
2892
- value,
2893
- 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
+ })
2894
2304
  });
2895
2305
  }
2896
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
+ /**
2897
2318
  * Read the current {@link ProjectSnapshot}. Throws if called outside a
2898
2319
  * {@link SwatchbookProvider}; blocks that need to fall back to the
2899
2320
  * virtual module go through the internal `useProject()` hook instead.
2900
2321
  */
2901
2322
  function useSwatchbookData() {
2902
2323
  const value = useOptionalSwatchbookData();
2903
- 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.");
2904
2325
  return value;
2905
2326
  }
2906
2327
  //#endregion
2907
- //#region src/shadow-preview/ShadowSample.tsx
2908
- /**
2909
- * Pure derivation of a single shadow token's sample data from resolved
2910
- * project data. Extracted so it is unit-testable without React or a store.
2911
- */
2912
- function deriveShadowSample(path, project) {
2913
- return { cssVar: resolveCssVar(path, project) };
2914
- }
2915
- /** Pure presentation for a single shadow token's sample. Renders from plain props. */
2916
- function ShadowSampleView({ cssVar }) {
2917
- return /* @__PURE__ */ jsx("div", {
2918
- className: "sb-shadow-sample",
2919
- style: { boxShadow: cssVar },
2920
- "aria-hidden": true
2921
- });
2922
- }
2923
- function ShadowSample({ path }) {
2924
- const { cssVar } = deriveShadowSample(path, useProject());
2925
- return /* @__PURE__ */ jsx(ShadowSampleView, { cssVar });
2926
- }
2927
- //#endregion
2928
2328
  //#region src/ShadowPreview.tsx
2929
2329
  function layerKey(path, layer, fallback) {
2930
2330
  return `${path}|${layer.offset}|${layer.blur}|${layer.spread}|${fallback}`;
@@ -2936,9 +2336,9 @@ function asLayers(raw) {
2936
2336
  }
2937
2337
  function formatLayer(layer, colorFormat) {
2938
2338
  return {
2939
- offset: `${formatDimension$1(layer.offsetX)} ${formatDimension$1(layer.offsetY)}`,
2940
- blur: formatDimension$1(layer.blur),
2941
- spread: formatDimension$1(layer.spread),
2339
+ offset: `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`,
2340
+ blur: formatDimension(layer.blur),
2341
+ spread: formatDimension(layer.spread),
2942
2342
  color: formatSubColor(layer.color, colorFormat),
2943
2343
  inset: layer.inset ? String(layer.inset) : void 0
2944
2344
  };
@@ -2957,15 +2357,17 @@ function deriveShadowRows(resolved, project, { filter, sortBy, sortDir, colorFor
2957
2357
  }).map(([path, token]) => ({
2958
2358
  path,
2959
2359
  cssVar: resolveCssVar(path, project),
2360
+ token,
2960
2361
  layers: asLayers(token.$value).map((layer) => formatLayer(layer, colorFormat))
2961
2362
  }));
2962
2363
  }
2963
2364
  /**
2964
2365
  * Pure presentation for the shadow preview. Renders from plain props;
2965
- * composes the connected `ShadowSample` as a child (that child reads the
2966
- * project itself).
2366
+ * composes the connected `ShadowSample` as a child, feeding it this row's
2367
+ * already-resolved `token`/`cssVar` per the presenter contract.
2967
2368
  */
2968
- function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2369
+ function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, filter, caption }) {
2370
+ const Sample = usePresenter("shadow");
2969
2371
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2970
2372
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2971
2373
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2994,7 +2396,12 @@ function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter
2994
2396
  }),
2995
2397
  /* @__PURE__ */ jsx("div", {
2996
2398
  className: "sb-shadow-preview__sample-cell",
2997
- 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
+ })
2998
2405
  }),
2999
2406
  /* @__PURE__ */ jsx("div", {
3000
2407
  className: "sb-shadow-preview__breakdown",
@@ -3061,26 +2468,13 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc", colo
3061
2468
  activeTheme,
3062
2469
  cssVarPrefix,
3063
2470
  activeAxes,
2471
+ colorFormat: format,
3064
2472
  filter,
3065
2473
  caption
3066
2474
  });
3067
2475
  }
3068
2476
  //#endregion
3069
2477
  //#region src/StrokeStylePreview.tsx
3070
- const STRING_STYLES = new Set([
3071
- "solid",
3072
- "dashed",
3073
- "dotted",
3074
- "double",
3075
- "groove",
3076
- "ridge",
3077
- "outset",
3078
- "inset"
3079
- ]);
3080
- function extractCssStyle(value) {
3081
- if (typeof value === "string" && STRING_STYLES.has(value)) return value;
3082
- return null;
3083
- }
3084
2478
  /**
3085
2479
  * Pure derivation of the preview's display rows from resolved project data.
3086
2480
  * Extracted so it is unit-testable without React or a store.
@@ -3095,12 +2489,17 @@ function deriveStrokeStyleRows(resolved, project, { filter, sortBy, sortDir }) {
3095
2489
  }).map(([path, token]) => ({
3096
2490
  path,
3097
2491
  cssVar: resolveCssVar(path, project),
3098
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
3099
- cssStyle: extractCssStyle(token.$value)
2492
+ displayValue: formatTokenValue$2(token.$value, token.$type, "raw", project.listing[path]),
2493
+ token
3100
2494
  }));
3101
2495
  }
3102
- /** Pure presentation for the stroke-style preview. Renders from plain props. */
3103
- 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");
3104
2503
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
3105
2504
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
3106
2505
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -3127,13 +2526,11 @@ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, f
3127
2526
  children: row.displayValue
3128
2527
  })]
3129
2528
  }),
3130
- row.cssStyle ? /* @__PURE__ */ jsx("div", {
3131
- className: "sb-stroke-style-sample__line",
3132
- style: { borderTopStyle: row.cssStyle },
3133
- "aria-hidden": true
3134
- }) : /* @__PURE__ */ jsx("span", {
3135
- className: "sb-stroke-style-sample__object-fallback",
3136
- 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
3137
2534
  }),
3138
2535
  /* @__PURE__ */ jsx("span", {
3139
2536
  className: "sb-stroke-style-sample__css-var",
@@ -3146,6 +2543,7 @@ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, f
3146
2543
  function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3147
2544
  const project = useProject();
3148
2545
  const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2546
+ const colorFormat = useColorFormat();
3149
2547
  return /* @__PURE__ */ jsx(StrokeStylePreviewView, {
3150
2548
  rows: useMemo(() => deriveStrokeStyleRows(resolved, project, {
3151
2549
  filter,
@@ -3161,6 +2559,7 @@ function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc"
3161
2559
  activeTheme,
3162
2560
  cssVarPrefix,
3163
2561
  activeAxes,
2562
+ colorFormat,
3164
2563
  filter,
3165
2564
  caption
3166
2565
  });
@@ -3458,7 +2857,7 @@ function AxisVariance({ path, colorFormat }) {
3458
2857
  }
3459
2858
  function valueFor(token, $type, format) {
3460
2859
  if (!token) return "—";
3461
- return formatTokenValue(token.$value, $type, format);
2860
+ return formatTokenValue$2(token.$value, $type, format);
3462
2861
  }
3463
2862
  //#endregion
3464
2863
  //#region src/token-detail/CompositeBreakdown.tsx
@@ -4101,7 +3500,7 @@ function deriveTokenDetail(path, token, listing, colorFormat) {
4101
3500
  if (!token) return EMPTY_DERIVED;
4102
3501
  const isColor = token.$type === "color";
4103
3502
  const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
4104
- const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
3503
+ const value = formatTokenValue$2(token.$value, token.$type, colorFormat, listing[path]);
4105
3504
  const outOfGamut = gamut?.outOfGamut ?? false;
4106
3505
  const dep = token.$deprecated;
4107
3506
  return {
@@ -4921,7 +4320,7 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4921
4320
  className: "sb-token-navigator__preview-box",
4922
4321
  children: [/* @__PURE__ */ jsx("span", {
4923
4322
  className: "sb-token-navigator__value",
4924
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4323
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4925
4324
  }), /* @__PURE__ */ jsx("span", {
4926
4325
  className: "sb-token-navigator__color-swatch",
4927
4326
  style: { background: cssVar },
@@ -4933,12 +4332,15 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4933
4332
  className: "sb-token-navigator__preview-box",
4934
4333
  children: [/* @__PURE__ */ jsx("span", {
4935
4334
  className: "sb-token-navigator__value",
4936
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4335
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4937
4336
  }), /* @__PURE__ */ jsx("span", {
4938
4337
  className: "sb-token-navigator__preview-dimension",
4939
4338
  children: /* @__PURE__ */ jsx(DimensionSample, {
4940
4339
  path,
4941
- visual: "length"
4340
+ token,
4341
+ cssVar: resolveCssVar(path, project),
4342
+ colorFormat,
4343
+ options: { visual: "length" }
4942
4344
  })
4943
4345
  })]
4944
4346
  });
@@ -4946,28 +4348,43 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4946
4348
  className: "sb-token-navigator__preview-box",
4947
4349
  children: /* @__PURE__ */ jsx("span", {
4948
4350
  className: "sb-token-navigator__preview-scaled",
4949
- children: /* @__PURE__ */ jsx(ShadowSample, { path })
4351
+ children: /* @__PURE__ */ jsx(ShadowSample, {
4352
+ path,
4353
+ token,
4354
+ cssVar: resolveCssVar(path, project),
4355
+ colorFormat
4356
+ })
4950
4357
  })
4951
4358
  });
4952
4359
  if (type === "border") return /* @__PURE__ */ jsx("span", {
4953
4360
  className: "sb-token-navigator__preview-box",
4954
4361
  children: /* @__PURE__ */ jsx("span", {
4955
4362
  className: "sb-token-navigator__preview-scaled",
4956
- children: /* @__PURE__ */ jsx(BorderSample, { path })
4363
+ children: /* @__PURE__ */ jsx(BorderSample, {
4364
+ path,
4365
+ token,
4366
+ cssVar: resolveCssVar(path, project),
4367
+ colorFormat
4368
+ })
4957
4369
  })
4958
4370
  });
4959
4371
  if (type === "transition" || type === "duration" || type === "cubicBezier") return /* @__PURE__ */ jsx("span", {
4960
4372
  className: "sb-token-navigator__preview-box",
4961
4373
  children: /* @__PURE__ */ jsx("span", {
4962
4374
  className: "sb-token-navigator__preview-motion",
4963
- children: /* @__PURE__ */ jsx(MotionSample, { path })
4375
+ children: /* @__PURE__ */ jsx(MotionSample, {
4376
+ path,
4377
+ token,
4378
+ cssVar: resolveCssVar(path, project),
4379
+ colorFormat
4380
+ })
4964
4381
  })
4965
4382
  });
4966
4383
  return /* @__PURE__ */ jsx("span", {
4967
4384
  className: "sb-token-navigator__preview-box",
4968
4385
  children: /* @__PURE__ */ jsx("span", {
4969
4386
  className: "sb-token-navigator__value",
4970
- children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4387
+ children: formatTokenValue$2(token.$value, type, colorFormat, project.listing[path])
4971
4388
  })
4972
4389
  });
4973
4390
  });
@@ -4999,7 +4416,7 @@ function deriveTokenRows(resolved, listing, cssVarPrefix, varianceByPath, { filt
4999
4416
  return {
5000
4417
  path,
5001
4418
  type: token.$type ?? "",
5002
- value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4419
+ value: formatTokenValue$2(token.$value, token.$type, colorFormat, listing[path]),
5003
4420
  outOfGamut: color?.outOfGamut ?? false,
5004
4421
  cssVar: resolveCssVar(path, projectFields),
5005
4422
  isColor,
@@ -5293,63 +4710,30 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
5293
4710
  }
5294
4711
  //#endregion
5295
4712
  //#region src/TypographyScale.tsx
5296
- function asDimension(raw) {
5297
- if (raw == null) return void 0;
5298
- if (typeof raw === "string" || typeof raw === "number") return String(raw);
5299
- if (typeof raw === "object") {
5300
- const v = raw;
5301
- if (v.value !== void 0 && v.unit !== void 0) return `${String(v.value)}${String(v.unit)}`;
5302
- }
5303
- }
5304
- function asFontFamily(raw) {
5305
- if (typeof raw === "string") return raw;
5306
- if (Array.isArray(raw)) return raw.map(String).join(", ");
5307
- }
5308
- function buildRow(path, composite) {
5309
- const fontFamily = asFontFamily(composite.fontFamily);
5310
- const fontSize = asDimension(composite.fontSize);
5311
- const fontWeight = composite.fontWeight == null ? void 0 : String(composite.fontWeight);
5312
- const lineHeight = composite.lineHeight == null ? void 0 : String(composite.lineHeight);
5313
- const letterSpacing = asDimension(composite.letterSpacing);
5314
- const sampleStyle = {};
5315
- if (fontFamily) sampleStyle.fontFamily = fontFamily;
5316
- if (fontSize) sampleStyle.fontSize = fontSize;
5317
- if (fontWeight) sampleStyle.fontWeight = fontWeight;
5318
- if (lineHeight) sampleStyle.lineHeight = lineHeight;
5319
- if (letterSpacing) sampleStyle.letterSpacing = letterSpacing;
5320
- return {
5321
- path,
5322
- sampleStyle,
5323
- specs: [
5324
- fontSize,
5325
- fontWeight ? `w${fontWeight}` : void 0,
5326
- lineHeight ? `lh ${lineHeight}` : void 0
5327
- ].filter(Boolean).join(" · ")
5328
- };
5329
- }
5330
4713
  /**
5331
4714
  * Pure derivation of the scale's display rows from resolved project data.
5332
4715
  * Extracted so it is unit-testable without React or a store.
5333
4716
  */
5334
- function deriveTypographyRows(resolved, { filter, sortBy, sortDir }) {
4717
+ function deriveTypographyRows(resolved, project, { filter, sortBy, sortDir }) {
5335
4718
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
5336
4719
  if (token.$type !== "typography") return false;
5337
4720
  return matchPath(path, filter);
5338
4721
  }), {
5339
4722
  by: sortBy,
5340
4723
  dir: sortDir
5341
- }).map(([path, token]) => {
5342
- const value = token.$value;
5343
- if (!value || typeof value !== "object") return {
5344
- path,
5345
- sampleStyle: {},
5346
- specs: ""
5347
- };
5348
- return buildRow(path, value);
5349
- });
4724
+ }).map(([path, token]) => ({
4725
+ path,
4726
+ cssVar: resolveCssVar(path, project),
4727
+ token
4728
+ }));
5350
4729
  }
5351
- /** Pure presentation for the typography scale. Renders from plain props. */
5352
- 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");
5353
4737
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
5354
4738
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
5355
4739
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -5365,31 +4749,31 @@ function TypographyScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, samp
5365
4749
  children: captionText
5366
4750
  }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
5367
4751
  className: "sb-typography-scale__row",
5368
- children: [/* @__PURE__ */ jsxs("div", {
5369
- className: "sb-typography-scale__meta",
5370
- children: [/* @__PURE__ */ jsx("span", {
5371
- className: "sb-typography-scale__path",
5372
- children: row.path
5373
- }), row.specs && /* @__PURE__ */ jsx("span", {
5374
- className: "sb-typography-scale__specs",
5375
- children: row.specs
5376
- })]
5377
- }), /* @__PURE__ */ jsx("div", {
5378
- style: row.sampleStyle,
5379
- 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 }
5380
4761
  })]
5381
4762
  }, row.path))]
5382
4763
  });
5383
4764
  }
5384
4765
  function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
5385
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
4766
+ const project = useProject();
4767
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
4768
+ const colorFormat = useColorFormat();
5386
4769
  return /* @__PURE__ */ jsx(TypographyScaleView, {
5387
- rows: useMemo(() => deriveTypographyRows(resolved, {
4770
+ rows: useMemo(() => deriveTypographyRows(resolved, project, {
5388
4771
  filter,
5389
4772
  sortBy,
5390
4773
  sortDir
5391
4774
  }), [
5392
4775
  resolved,
4776
+ project,
5393
4777
  filter,
5394
4778
  sortBy,
5395
4779
  sortDir
@@ -5398,11 +4782,12 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
5398
4782
  cssVarPrefix,
5399
4783
  activeAxes,
5400
4784
  sample,
4785
+ colorFormat,
5401
4786
  filter,
5402
4787
  caption
5403
4788
  });
5404
4789
  }
5405
4790
  //#endregion
5406
- 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 };
5407
4792
 
5408
4793
  //# sourceMappingURL=index.mjs.map