@unpunnyfuns/swatchbook-blocks 0.55.0 → 0.56.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.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as _$react from "react";
2
2
  import { ReactElement, ReactNode } from "react";
3
- import { Axis, AxisVarianceResult, Diagnostic, Permutation, Preset } from "@unpunnyfuns/swatchbook-core";
3
+ import { Axis, AxisVarianceResult, Diagnostic, Preset } from "@unpunnyfuns/swatchbook-core";
4
+ import { SlimListedToken } from "@unpunnyfuns/swatchbook-core/snapshot-for-wire";
4
5
 
5
6
  //#region src/format-color.d.ts
6
7
  /**
@@ -65,15 +66,27 @@ declare function formatColor(value: unknown, format: ColorFormat, fallback?: str
65
66
  * in `packages/addon/src/virtual.d.ts` describe the same payload.
66
67
  */
67
68
  type VirtualAxisShape = Axis;
68
- type VirtualPermutationShape = Permutation;
69
69
  type VirtualDiagnosticShape = Diagnostic;
70
70
  interface VirtualTokenShape {
71
- $type?: string;
71
+ $type?: string | undefined;
72
72
  $value?: unknown;
73
- $description?: string;
74
- aliasOf?: string;
75
- aliasChain?: readonly string[];
76
- aliasedBy?: readonly string[];
73
+ $description?: string | undefined;
74
+ aliasOf?: string | undefined;
75
+ aliasChain?: readonly string[] | undefined;
76
+ aliasedBy?: readonly string[] | undefined;
77
+ /**
78
+ * Per-sub-field alias map for composite tokens whose value blends
79
+ * primitives with aliased fragments — Terrazzo populates this when
80
+ * one or more component fields of a composite ($type: 'border',
81
+ * 'shadow', 'typography', 'gradient', 'transition') resolve through
82
+ * an alias. The `CompositeBreakdown` block reads it to render the
83
+ * source path beside each component value. Typed as `unknown` because
84
+ * the shape varies per composite type (color carries a
85
+ * `{components: (string | undefined)[]}` sub-shape; typography/border
86
+ * carry a flat `Record<string, string | undefined>`); the block
87
+ * narrows it at the consumer.
88
+ */
89
+ partialAliasOf?: unknown;
77
90
  }
78
91
  /**
79
92
  * Subset of `@terrazzo/plugin-token-listing`'s `ListedToken` that the
@@ -81,28 +94,11 @@ interface VirtualTokenShape {
81
94
  * variable name and `previewValue` for the display-ready CSS string.
82
95
  * `source.loc` enables "jump to authoring source" affordances.
83
96
  *
84
- * Only the fields blocks consume are typed here; the plugin's full shape
85
- * lives in `@unpunnyfuns/swatchbook-core`.
97
+ * Aliases `SlimListedToken` from core's `/snapshot-for-wire` subpath —
98
+ * the single canonical wire shape; both the addon's plugin (server-side
99
+ * emit) and blocks (consumer-side read) reference the same definition.
86
100
  */
87
- interface VirtualTokenListingShape {
88
- names: Record<string, string>;
89
- previewValue?: string | number;
90
- source?: {
91
- resource: string;
92
- loc?: {
93
- start: {
94
- line: number;
95
- column: number;
96
- offset: number;
97
- };
98
- end: {
99
- line: number;
100
- column: number;
101
- offset: number;
102
- };
103
- };
104
- };
105
- }
101
+ type VirtualTokenListingShape = SlimListedToken;
106
102
  type VirtualPresetShape = Preset;
107
103
  /**
108
104
  * Wire shape of one `Project.jointOverrides` entry — same as core's
@@ -116,7 +112,7 @@ interface VirtualJointOverrideShape {
116
112
  /**
117
113
  * Map from path → cached `AxisVarianceResult`. Snapshot carries this
118
114
  * so the `AxisVariance` block can O(1) look up which axes affect a
119
- * token instead of re-running `analyzeAxisVariance` on every render.
115
+ * token instead of re-running variance analysis on every render.
120
116
  */
121
117
  type VirtualVarianceByPathShape = Record<string, AxisVarianceResult>;
122
118
  /**
@@ -129,17 +125,6 @@ interface ProjectSnapshot {
129
125
  /** Axis names suppressed via `config.disabledAxes` — pinned to their defaults, hidden from the toolbar. */
130
126
  disabledAxes: readonly string[];
131
127
  presets: readonly VirtualPresetShape[];
132
- /**
133
- * @deprecated Wire-shipped permutations were removed in PR 6a.
134
- * Hand-built snapshots (tests, MDX consumers) may still populate
135
- * this for backward compatibility with the legacy fallback path
136
- * in `useProject`. Production preview snapshots omit it.
137
- */
138
- permutations?: readonly VirtualPermutationShape[];
139
- /**
140
- * @deprecated See `permutations`. Wire format dropped in PR 6a.
141
- */
142
- permutationsResolved?: Record<string, Record<string, VirtualTokenShape>>;
143
128
  activePermutation: string;
144
129
  activeAxes: Readonly<Record<string, string>>;
145
130
  cssVarPrefix: string;
@@ -157,26 +142,23 @@ interface ProjectSnapshot {
157
142
  * Per-axis cell maps — `cells[axis][context]` is the resolved token
158
143
  * data for `{ ...defaults, [axis]: context }`. Bounded by
159
144
  * `Σ(axes × contexts)` regardless of cartesian product size.
160
- * Optional during the chain rollout empty fallback when the
161
- * snapshot pre-dates the wire format change.
145
+ * Non-default cells store only the tokens whose value differs from
146
+ * the default-cell baseline (delta cells).
162
147
  */
163
- cells?: Record<string, Record<string, Record<string, VirtualTokenShape>>>;
148
+ cells: Record<string, Record<string, Record<string, VirtualTokenShape>>>;
164
149
  /**
165
150
  * `Project.jointOverrides` flattened to entries for wire transport.
166
151
  * Same ascending-arity iteration order the Map carries on the
167
- * server side.
152
+ * server side. Empty array when no joint divergences exist.
168
153
  */
169
- jointOverrides?: readonly (readonly [string, VirtualJointOverrideShape])[];
154
+ jointOverrides: readonly (readonly [string, VirtualJointOverrideShape])[];
170
155
  /**
171
156
  * Cached per-path variance results. Blocks read this for O(1) axis
172
157
  * variance lookup instead of recomputing on each render.
173
158
  */
174
159
  varianceByPath?: VirtualVarianceByPathShape;
175
- /**
176
- * The default tuple — `{ axis: axis.default }` for every axis.
177
- * Replaces the legacy "look at `permutations[0].input`" pattern.
178
- */
179
- defaultTuple?: Record<string, string>;
160
+ /** The default tuple — `{ axis: axis.default }` for every axis. */
161
+ defaultTuple: Record<string, string>;
180
162
  /**
181
163
  * Pre-built `resolveAt(tuple)` accessor. The addon's preview
182
164
  * decorator instantiates this once per iframe lifetime — the
@@ -901,5 +883,5 @@ declare function TypographyScale({
901
883
  sortDir
902
884
  }: TypographyScaleProps): ReactElement;
903
885
  //#endregion
904
- export { AliasChain, type AliasChainProps, AliasedBy, type AliasedByProps, AxesContext, AxisVariance, type AxisVarianceProps, BorderPreview, type BorderPreviewProps, BorderSample, type BorderSampleProps, COLOR_FORMATS, type ColorFormat, ColorFormatContext, ColorPalette, type ColorPaletteProps, ColorTable, type ColorTableProps, CompositeBreakdown, type CompositeBreakdownProps, CompositePreview, type CompositePreviewProps, ConsumerOutput, type ConsumerOutputProps, Diagnostics, type DiagnosticsProps, DimensionBar, type DimensionBarProps, type DimensionKind, DimensionScale, type DimensionScaleProps, FontFamilySample, type FontFamilySampleProps, FontWeightScale, type FontWeightScaleProps, type FormatColorResult, GradientPalette, type GradientPaletteProps, MotionPreview, type MotionPreviewProps, MotionSample, type MotionSampleProps, type MotionSpeed, type NormalizedColor, OpacityScale, type OpacityScaleProps, PermutationContext, type ProjectSnapshot, ShadowPreview, type ShadowPreviewProps, ShadowSample, type ShadowSampleProps, StrokeStyleSample, type StrokeStyleSampleProps, SwatchbookContext, SwatchbookProvider, type SwatchbookProviderProps, TokenDetail, type TokenDetailProps, TokenHeader, type TokenHeaderProps, TokenNavigator, type TokenNavigatorProps, TokenTable, type TokenTableProps, TokenUsageSnippet, type TokenUsageSnippetProps, TypographyScale, type TypographyScaleProps, type VirtualAxisShape as VirtualAxis, type VirtualAxisShape, type VirtualDiagnosticShape as VirtualDiagnostic, type VirtualDiagnosticShape, type VirtualPermutationShape as VirtualPermutation, type VirtualPermutationShape, type VirtualPresetShape as VirtualPreset, type VirtualPresetShape, type VirtualTokenShape as VirtualToken, type VirtualTokenShape, type VirtualTokenListingShape, formatColor, useActiveAxes, useActivePermutation, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
886
+ export { AliasChain, type AliasChainProps, AliasedBy, type AliasedByProps, AxesContext, AxisVariance, type AxisVarianceProps, BorderPreview, type BorderPreviewProps, BorderSample, type BorderSampleProps, COLOR_FORMATS, type ColorFormat, ColorFormatContext, ColorPalette, type ColorPaletteProps, ColorTable, type ColorTableProps, CompositeBreakdown, type CompositeBreakdownProps, CompositePreview, type CompositePreviewProps, ConsumerOutput, type ConsumerOutputProps, Diagnostics, type DiagnosticsProps, DimensionBar, type DimensionBarProps, type DimensionKind, DimensionScale, type DimensionScaleProps, FontFamilySample, type FontFamilySampleProps, FontWeightScale, type FontWeightScaleProps, type FormatColorResult, GradientPalette, type GradientPaletteProps, MotionPreview, type MotionPreviewProps, MotionSample, type MotionSampleProps, type MotionSpeed, type NormalizedColor, OpacityScale, type OpacityScaleProps, PermutationContext, type ProjectSnapshot, ShadowPreview, type ShadowPreviewProps, ShadowSample, type ShadowSampleProps, StrokeStyleSample, type StrokeStyleSampleProps, SwatchbookContext, SwatchbookProvider, type SwatchbookProviderProps, TokenDetail, type TokenDetailProps, TokenHeader, type TokenHeaderProps, TokenNavigator, type TokenNavigatorProps, TokenTable, type TokenTableProps, TokenUsageSnippet, type TokenUsageSnippetProps, TypographyScale, type TypographyScaleProps, type VirtualAxisShape as VirtualAxis, type VirtualAxisShape, type VirtualDiagnosticShape as VirtualDiagnostic, type VirtualDiagnosticShape, type VirtualPresetShape as VirtualPreset, type VirtualPresetShape, type VirtualTokenShape as VirtualToken, type VirtualTokenShape, type VirtualTokenListingShape, formatColor, useActiveAxes, useActivePermutation, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
905
887
  //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs CHANGED
@@ -3,9 +3,10 @@ import Color from "colorjs.io";
3
3
  import { Fragment, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
6
- import { makeCSSVar } from "@terrazzo/token-tools/css";
6
+ import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
7
7
  import { addons } from "storybook/preview-api";
8
8
  import { axes, cells, css, cssVarPrefix, defaultTuple, diagnostics, jointOverrides, listing, presets, varianceByPath } from "virtual:swatchbook/tokens";
9
+ import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
9
10
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
10
11
  import cx from "clsx";
11
12
  //#region src/format-color.ts
@@ -56,7 +57,8 @@ function coerce(value) {
56
57
  return null;
57
58
  }
58
59
  const alpha = typeof v.alpha === "number" ? v.alpha : void 0;
59
- const hex = typeof v["hex"] === "string" ? v["hex"] : void 0;
60
+ const hexVal = v["hex"];
61
+ const hex = typeof hexVal === "string" ? hexVal : void 0;
60
62
  return {
61
63
  colorSpace,
62
64
  components,
@@ -217,33 +219,23 @@ function ensureSubscribed$1() {
217
219
  if (subscribed$1 || typeof window === "undefined") return;
218
220
  subscribed$1 = true;
219
221
  const channel = addons.getChannel();
222
+ let lastFingerprint = "";
220
223
  const onGlobals = (payload) => {
221
224
  const globals = payload.globals;
222
225
  if (!globals) return;
223
- let next = snapshot$1;
224
- const nextAxes = globals[AXES_GLOBAL_KEY];
225
- if (nextAxes && typeof nextAxes === "object") next = {
226
- ...next,
227
- axes: nextAxes
228
- };
229
- const nextFormat = globals[COLOR_FORMAT_GLOBAL_KEY];
230
- if (isColorFormat(nextFormat)) next = {
231
- ...next,
226
+ const incomingAxes = globals[AXES_GLOBAL_KEY];
227
+ const incomingFormat = globals[COLOR_FORMAT_GLOBAL_KEY];
228
+ const nextAxes = incomingAxes && typeof incomingAxes === "object" ? incomingAxes : snapshot$1.axes;
229
+ const nextFormat = isColorFormat(incomingFormat) ? incomingFormat : snapshot$1.format;
230
+ const fingerprint = `${nextFormat ?? ""}|${nextAxes ? JSON.stringify(nextAxes) : ""}`;
231
+ if (fingerprint === lastFingerprint) return;
232
+ lastFingerprint = fingerprint;
233
+ snapshot$1 = {
234
+ axes: nextAxes,
232
235
  format: nextFormat
233
236
  };
234
- if (next !== snapshot$1) {
235
- snapshot$1 = next;
236
- for (const cb of listeners$1) cb();
237
- }
237
+ for (const cb of listeners$1) cb();
238
238
  };
239
- /**
240
- * `setGlobals` fires once on preview init carrying the URL-persisted user
241
- * globals (Storybook stores toolbar selections in `?globals=…`). Without
242
- * this listener, deeplinking to an MDX page with a non-default axis tuple
243
- * or color format renders defaults for one frame before the first
244
- * `updateGlobals` arrives. `emitGlobals()` reads from `userGlobals.get()`
245
- * (current state), so the payload is never stale — safe to handle.
246
- */
247
239
  channel.on("globalsUpdated", onGlobals);
248
240
  channel.on("updateGlobals", onGlobals);
249
241
  channel.on("setGlobals", onGlobals);
@@ -408,44 +400,24 @@ function defaultTuple$1(axes) {
408
400
  return out;
409
401
  }
410
402
  /**
411
- * Stable string key for a tuple — axes sorted by name + `:` separator.
412
- * Matches the key form `buildResolveAt` uses in core so both surfaces
413
- * agree on what counts as "the same tuple."
403
+ * Synthesize a permutation name from a tuple — same form
404
+ * `permutationID` produces server-side (axis values joined by ` · `).
405
+ * Used by the AxisVariance grid for `data-<prefix>-theme` attribution
406
+ * and similar display-only callers.
414
407
  */
415
- function canonicalTupleKey(tuple) {
416
- return Object.keys(tuple).toSorted().map((k) => `${k}:${tuple[k]}`).join("|");
408
+ function tupleToName(axes, tuple) {
409
+ return axes.map((a) => tuple[a.name] ?? a.default).join(" · ");
417
410
  }
418
411
  /**
419
- * Build a `Map<canonicalKey, permutationName>` once per permutations
420
- * list so per-tuple lookups go through O(1) `Map.get` instead of an
421
- * `Array.prototype.find` scan per call. Bounded by the permutations
422
- * count regardless of how many lookups consumers do.
423
- */
424
- function buildPermutationNameByTuple(permutations) {
425
- const out = /* @__PURE__ */ new Map();
426
- for (const perm of permutations) out.set(canonicalTupleKey(perm.input), perm.name);
427
- return out;
428
- }
429
- const noPermutationName = () => void 0;
430
- function tuplesEqual(a, b) {
431
- const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
432
- for (const k of keys) if (a[k] !== b[k]) return false;
433
- return true;
434
- }
435
- function nameForTuple(themesList, tuple) {
436
- return themesList.find((t) => tuplesEqual(t.input, tuple))?.name;
437
- }
438
- /**
439
- * Reconstruct a `resolveAt` accessor from snapshot data. The wire
440
- * format ships `cells` as plain JSON and `jointOverrides` as an
441
- * array of `[key, entry]` pairs (Map doesn't survive JSON.stringify);
442
- * this hydrates them and wraps `buildResolveAt` from core. Stable
443
- * identity across calls with the same snapshot — `useMemo` keyed on
444
- * the snapshot fields produces a referentially stable function.
412
+ * Reconstruct a `resolveAt` accessor from snapshot data. Both `cells`
413
+ * and `jointOverrides` ship as plain JSON in the same shape core uses
414
+ * internally no Map reconstruction at the boundary. Stable identity
415
+ * across calls with the same snapshot `useMemo` keyed on the
416
+ * snapshot fields produces a referentially stable function.
445
417
  */
446
418
  function makeResolveAt(snapshot) {
447
419
  const cells = snapshot.cells ?? {};
448
- const jointOverrides = new Map(snapshot.jointOverrides ?? []);
420
+ const jointOverrides = snapshot.jointOverrides ?? [];
449
421
  const defaults = snapshot.defaultTuple ?? defaultTuple$1(snapshot.axes);
450
422
  const resolver = buildResolveAt(snapshot.axes, cells, jointOverrides, defaults);
451
423
  return (tuple) => resolver(tuple);
@@ -454,20 +426,14 @@ function makeResolveAt(snapshot) {
454
426
  * Build the `resolveAt` accessor for a snapshot. Prefers the
455
427
  * snapshot's own `resolveAt` (the addon's preview decorator
456
428
  * pre-builds one at module load — see `previewResolveAt` in
457
- * `packages/addon/src/preview.tsx`), falling back to constructing
458
- * one from `cells` + `jointOverrides` when present (covers
459
- * hand-built test snapshots and the no-provider path) and finally to
460
- * the legacy per-permutation lookup for snapshots that only carry
461
- * `permutationsResolved` + `permutations` (MDX consumers that
462
- * pre-date the wire format change).
429
+ * `packages/addon/src/preview.tsx`), otherwise composes one from
430
+ * `cells` + `jointOverrides` via `makeResolveAt`. Hand-built
431
+ * snapshots should provide both via the test `withCellsShape`
432
+ * helper or by populating the fields directly.
463
433
  */
464
434
  function snapshotResolveAt(snapshot) {
465
435
  if (snapshot.resolveAt) return snapshot.resolveAt;
466
- if (Object.keys(snapshot.cells ?? {}).length > 0) return makeResolveAt(snapshot);
467
- return (tuple) => {
468
- const perms = snapshot.permutations ?? [];
469
- return (snapshot.permutationsResolved ?? {})[nameForTuple(perms, tuple) ?? snapshot.activePermutation] ?? {};
470
- };
436
+ return makeResolveAt(snapshot);
471
437
  }
472
438
  /**
473
439
  * Reads project data either from a mounted {@link SwatchbookProvider}
@@ -487,7 +453,12 @@ function useProject() {
487
453
  const cells = snapshot?.cells;
488
454
  const jointOverrides = snapshot?.jointOverrides;
489
455
  const dataDefaultTuple = snapshot?.defaultTuple;
456
+ const activeAxes = snapshot?.activeAxes;
490
457
  const activePermutation = snapshot?.activePermutation;
458
+ const diagnostics = snapshot?.diagnostics;
459
+ const cssVarPrefix = snapshot?.cssVarPrefix;
460
+ const listing = snapshot?.listing;
461
+ const varianceByPath = snapshot?.varianceByPath;
491
462
  const resolveAt = useMemo(() => {
492
463
  if (!snapshot) return null;
493
464
  return snapshotResolveAt(snapshot);
@@ -498,24 +469,36 @@ function useProject() {
498
469
  dataDefaultTuple,
499
470
  activePermutation
500
471
  ]);
501
- const permutationNameByTuple = useMemo(() => buildPermutationNameByTuple(snapshot?.permutations ?? []), [snapshot?.permutations]);
502
- const fallback = useVirtualModuleFallback(snapshot === null);
503
- if (snapshot !== null && resolveAt !== null) return snapshotToData(snapshot, resolveAt, permutationNameByTuple);
504
- return fallback;
505
- }
506
- function snapshotToData(snapshot, resolveAt, permutationNameByTuple) {
507
- return {
508
- activePermutation: snapshot.activePermutation,
509
- activeAxes: snapshot.activeAxes,
510
- axes: snapshot.axes,
511
- resolved: resolveAt(snapshot.activeAxes),
512
- diagnostics: snapshot.diagnostics,
513
- cssVarPrefix: snapshot.cssVarPrefix,
514
- listing: snapshot.listing ?? {},
515
- varianceByPath: snapshot.varianceByPath ?? {},
472
+ const providerData = useMemo(() => {
473
+ if (!snapshot || !resolveAt || !axes || !activeAxes) return null;
474
+ return {
475
+ activePermutation: activePermutation ?? "",
476
+ activeAxes,
477
+ axes,
478
+ resolved: resolveAt(activeAxes),
479
+ diagnostics: diagnostics ?? [],
480
+ cssVarPrefix: cssVarPrefix ?? "",
481
+ listing: listing ?? {},
482
+ varianceByPath: varianceByPath ?? {},
483
+ resolveAt,
484
+ permutationNameForTuple: (tuple) => tupleToName(axes, tuple)
485
+ };
486
+ }, [
487
+ snapshot,
516
488
  resolveAt,
517
- permutationNameForTuple: (tuple) => permutationNameByTuple.get(canonicalTupleKey(tuple))
518
- };
489
+ axes,
490
+ cells,
491
+ jointOverrides,
492
+ dataDefaultTuple,
493
+ activePermutation,
494
+ activeAxes,
495
+ diagnostics,
496
+ cssVarPrefix,
497
+ listing,
498
+ varianceByPath
499
+ ]);
500
+ const fallback = useVirtualModuleFallback(snapshot === null);
501
+ return providerData ?? fallback;
519
502
  }
520
503
  function useVirtualModuleFallback(enabled) {
521
504
  const contextPermutation = useActivePermutation();
@@ -533,8 +516,14 @@ function useVirtualModuleFallback(enabled) {
533
516
  if (!enabled) return;
534
517
  ensureStylesheet(tokens.css);
535
518
  }, [enabled, tokens.css]);
536
- const activeAxes = Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple$1(tokens.axes);
537
- const activePermutation = contextPermutation || tokens.axes.map((a) => activeAxes[a.name] ?? a.default).join(" · ") || "";
519
+ const activeAxes = useMemo(() => {
520
+ return Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple$1(tokens.axes);
521
+ }, [
522
+ contextAxes,
523
+ channelGlobals.axes,
524
+ tokens.axes
525
+ ]);
526
+ const activePermutation = contextPermutation || tupleToName(tokens.axes, activeAxes);
538
527
  const resolveAt = useMemo(() => makeResolveAt({
539
528
  axes: tokens.axes,
540
529
  cells: tokens.cells,
@@ -546,32 +535,27 @@ function useVirtualModuleFallback(enabled) {
546
535
  tokens.jointOverrides,
547
536
  tokens.defaultTuple
548
537
  ]);
549
- const resolved = resolveAt(activeAxes);
550
- const permutationNameForTuple = noPermutationName;
551
- return {
538
+ return useMemo(() => ({
552
539
  activePermutation,
553
540
  activeAxes,
554
541
  axes: tokens.axes,
555
- resolved,
542
+ resolved: resolveAt(activeAxes),
556
543
  diagnostics: tokens.diagnostics,
557
544
  cssVarPrefix: tokens.cssVarPrefix,
558
545
  listing: tokens.listing,
559
546
  varianceByPath: tokens.varianceByPath,
560
547
  resolveAt,
561
- permutationNameForTuple
562
- };
563
- }
564
- /**
565
- * Thin wrapper around Terrazzo's `makeCSSVar` so the block-display surface
566
- * and `packages/core/src/css.ts`'s emitter share one implementation. Any
567
- * future naming-policy shift in Terrazzo (casing, unicode, prefix handling)
568
- * reaches both surfaces at once instead of needing a parallel update here.
569
- */
570
- function makeCssVar(path, prefix) {
571
- return prefix ? makeCSSVar(path, {
572
- prefix,
573
- wrapVar: true
574
- }) : makeCSSVar(path, { wrapVar: true });
548
+ permutationNameForTuple: (tuple) => tupleToName(tokens.axes, tuple)
549
+ }), [
550
+ activePermutation,
551
+ activeAxes,
552
+ tokens.axes,
553
+ tokens.diagnostics,
554
+ tokens.cssVarPrefix,
555
+ tokens.listing,
556
+ tokens.varianceByPath,
557
+ resolveAt
558
+ ]);
575
559
  }
576
560
  /**
577
561
  * Resolve a token's CSS var reference, preferring the authoritative name
@@ -654,15 +638,6 @@ function BorderSample({ path }) {
654
638
  //#endregion
655
639
  //#region src/internal/data-attr.ts
656
640
  /**
657
- * Produce a prefixed `data-*` attribute name when `prefix` is set, bare
658
- * `data-<key>` otherwise. Mirrors `dataAttr` in `@unpunnyfuns/swatchbook-core`
659
- * so block wrappers and emitted-CSS selectors stay in lockstep without
660
- * blocks taking a runtime dep on core.
661
- */
662
- function dataAttr(prefix, key) {
663
- return prefix ? `data-${prefix}-${key}` : `data-${key}`;
664
- }
665
- /**
666
641
  * Marker attribute set on every block wrapper. Retained as a stable hook
667
642
  * for consumer-side selectors (e.g. when a host app wants to target or
668
643
  * override block chrome without relying on hashed class names).
@@ -697,59 +672,73 @@ function themeAttrs(prefix, themeName) {
697
672
  }
698
673
  //#endregion
699
674
  //#region src/internal/sort-tokens.ts
700
- /**
701
- * Stable sort for a filtered `[path, token][]` list.
702
- *
703
- * `sortBy: 'path'` — lexicographic on the dot-path (locale-aware, numeric).
704
- * `sortBy: 'value'` — per-`$type` ordering:
705
- * - `dimension` / `duration` → numeric pixels / ms (via `toMagnitude`).
706
- * - `fontWeight` / `opacity` / `number` / `lineHeight` → numeric.
707
- * - `color` → perceptual by oklch L → C → H.
708
- * - `fontFamily` / `strokeStyle` (string form) → lexicographic.
709
- * - Composites (`typography`, `shadow`, `border`, `gradient`, `transition`)
710
- * fall back to path-alpha. No useful single-axis order.
711
- * `sortBy: 'none'` preserve input order (still respects `sortDir: 'desc'`
712
- * as a reverse).
713
- */
675
+ const NUMERIC_TYPES = new Set([
676
+ "dimension",
677
+ "duration",
678
+ "fontWeight",
679
+ "opacity",
680
+ "number",
681
+ "lineHeight"
682
+ ]);
683
+ const STRING_TYPES = new Set(["fontFamily", "strokeStyle"]);
684
+ function computeSortKey(token) {
685
+ const type = token.$type;
686
+ if (!type) return { kind: "none" };
687
+ if (NUMERIC_TYPES.has(type)) {
688
+ const value = toMagnitude(token.$value);
689
+ return {
690
+ kind: "numeric",
691
+ value,
692
+ valid: Number.isFinite(value)
693
+ };
694
+ }
695
+ if (type === "color") return {
696
+ kind: "color",
697
+ key: colorKey(token.$value)
698
+ };
699
+ if (STRING_TYPES.has(type)) return {
700
+ kind: "string",
701
+ value: toDisplayable(token.$value)
702
+ };
703
+ return { kind: "none" };
704
+ }
714
705
  function sortTokens(entries, options = {}) {
715
706
  const by = options.by ?? "path";
716
707
  const dir = options.dir ?? "asc";
717
708
  const sign = dir === "desc" ? -1 : 1;
718
709
  if (by === "none") return dir === "desc" ? [...entries].toReversed() : [...entries];
719
710
  if (by === "path") return [...entries].toSorted(([a], [b]) => sign * a.localeCompare(b, void 0, { numeric: true }));
711
+ const keys = /* @__PURE__ */ new Map();
712
+ for (const [, token] of entries) keys.set(token, computeSortKey(token));
720
713
  return [...entries].toSorted(([aPath, aTok], [bPath, bTok]) => {
721
- const cmp = compareValue(aTok, bTok);
714
+ const cmp = compareValue(aTok, bTok, keys);
722
715
  if (cmp !== 0) return sign * cmp;
723
716
  return sign * aPath.localeCompare(bPath, void 0, { numeric: true });
724
717
  });
725
718
  }
726
- function compareValue(a, b) {
727
- const type = a.$type;
728
- if (type !== b.$type) return String(type ?? "").localeCompare(String(b.$type ?? ""));
729
- if (!type) return 0;
730
- if (type === "dimension" || type === "duration" || type === "fontWeight" || type === "opacity" || type === "number" || type === "lineHeight") {
731
- const av = toMagnitude(a.$value);
732
- const bv = toMagnitude(b.$value);
733
- if (Number.isFinite(av) && Number.isFinite(bv)) return av - bv;
734
- if (Number.isFinite(av)) return -1;
735
- if (Number.isFinite(bv)) return 1;
719
+ function compareValue(a, b, keys) {
720
+ if (a.$type !== b.$type) return String(a.$type ?? "").localeCompare(String(b.$type ?? ""));
721
+ const ak = keys.get(a);
722
+ const bk = keys.get(b);
723
+ if (!ak || !bk) return 0;
724
+ if (ak.kind !== bk.kind) return 0;
725
+ if (ak.kind === "numeric" && bk.kind === "numeric") {
726
+ if (ak.valid && bk.valid) return ak.value - bk.value;
727
+ if (ak.valid) return -1;
728
+ if (bk.valid) return 1;
736
729
  return 0;
737
730
  }
738
- if (type === "color") {
739
- const ak = colorKey(a.$value);
740
- const bk = colorKey(b.$value);
741
- if (!ak && !bk) return 0;
742
- if (!ak) return 1;
743
- if (!bk) return -1;
744
- if (ak.l !== bk.l) return ak.l - bk.l;
745
- if (ak.c !== bk.c) return ak.c - bk.c;
746
- return ak.h - bk.h;
747
- }
748
- if (type === "fontFamily" || type === "strokeStyle") {
749
- const as = toDisplayable(a.$value);
750
- const bs = toDisplayable(b.$value);
751
- return as.localeCompare(bs, void 0, { numeric: true });
731
+ if (ak.kind === "color" && bk.kind === "color") {
732
+ const a3 = ak.key;
733
+ const b3 = bk.key;
734
+ if (!a3 && !b3) return 0;
735
+ if (!a3) return 1;
736
+ if (!b3) return -1;
737
+ if (a3.l !== b3.l) return a3.l - b3.l;
738
+ if (a3.c !== b3.c) return a3.c - b3.c;
739
+ return a3.h - b3.h;
752
740
  }
741
+ if (ak.kind === "string" && bk.kind === "string") return ak.value.localeCompare(bk.value, void 0, { numeric: true });
753
742
  return 0;
754
743
  }
755
744
  function toMagnitude(v) {
@@ -769,6 +758,14 @@ function toMagnitude(v) {
769
758
  }
770
759
  return NaN;
771
760
  }
761
+ /**
762
+ * Coerce a possibly-null/undefined number to 0 — `coords` returns
763
+ * `(number | null)[]` and `noUncheckedIndexedAccess` adds `undefined`
764
+ * on top. `typeof` narrows the union for the comparator below.
765
+ */
766
+ function safeNumber(v) {
767
+ return typeof v === "number" && Number.isFinite(v) ? v : 0;
768
+ }
772
769
  function colorKey(v) {
773
770
  if (!v || typeof v !== "object") return null;
774
771
  try {
@@ -785,9 +782,9 @@ function colorKey(v) {
785
782
  } else return null;
786
783
  const [l, chroma, h] = new Color(source).to("oklch").coords;
787
784
  return {
788
- l: Number.isFinite(l) ? l : 0,
789
- c: Number.isFinite(chroma) ? chroma : 0,
790
- h: Number.isFinite(h) ? h : 0
785
+ l: safeNumber(l),
786
+ c: safeNumber(chroma),
787
+ h: safeNumber(h)
791
788
  };
792
789
  } catch {
793
790
  return null;
@@ -2117,9 +2114,9 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2117
2114
  * True when rendering inside Chromatic's snapshot runner. Chromatic's
2118
2115
  * browser ships a recognisable user-agent string; checked here so
2119
2116
  * motion-looping components can fall back to their static state for
2120
- * deterministic snapshots without needing a global Chromatic parameter
2121
- * (globally forcing `prefersReducedMotion: true` broke Chromatic's
2122
- * verification parser in our setup see commit 893331f).
2117
+ * deterministic snapshots. Per-component detection rather than the
2118
+ * global `chromatic.prefersReducedMotion: true` parameter — that
2119
+ * parameter is incompatible with Chromatic's verification parser.
2123
2120
  */
2124
2121
  function isChromatic() {
2125
2122
  if (typeof navigator === "undefined") return false;
@@ -2887,14 +2884,19 @@ function AxisVariance({ path }) {
2887
2884
  const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2888
2885
  const variance = useMemo(() => {
2889
2886
  const result = varianceByPath[path];
2890
- if (!result) return {
2891
- kind: "constant",
2892
- varyingAxes: []
2893
- };
2894
- return {
2895
- kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
2896
- varyingAxes: result.varyingAxes
2897
- };
2887
+ if (!result) return { kind: "constant" };
2888
+ switch (result.kind) {
2889
+ case "constant": return { kind: "constant" };
2890
+ case "single": return {
2891
+ kind: "one-axis",
2892
+ axis: result.axis,
2893
+ varyingAxes: result.varyingAxes
2894
+ };
2895
+ case "multi": return {
2896
+ kind: "multi-axis",
2897
+ varyingAxes: result.varyingAxes
2898
+ };
2899
+ }
2898
2900
  }, [path, varianceByPath]);
2899
2901
  if (axes.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2900
2902
  if (variance.kind === "constant") {
@@ -2930,8 +2932,7 @@ function AxisVariance({ path }) {
2930
2932
  })] });
2931
2933
  }
2932
2934
  if (variance.kind === "one-axis") {
2933
- const axisName = variance.varyingAxes[0];
2934
- if (!axisName) return /* @__PURE__ */ jsx(Fragment$1, {});
2935
+ const axisName = variance.axis;
2935
2936
  const axis = axes.find((a) => a.name === axisName);
2936
2937
  if (!axis) return /* @__PURE__ */ jsx(Fragment$1, {});
2937
2938
  const contextValues = axis.contexts.map((ctx) => {
@@ -3775,6 +3776,7 @@ function buildTree(resolved, root, typeFilter) {
3775
3776
  let node = rootNode;
3776
3777
  for (let i = 0; i < segments.length - 1; i += 1) {
3777
3778
  const seg = segments[i];
3779
+ if (seg === void 0) continue;
3778
3780
  const prefix = [...rootSegments, ...segments.slice(0, i + 1)].join(".");
3779
3781
  let child = node.children.find((c) => c.kind === "group" && c.segment === seg);
3780
3782
  if (!child) {