@unpunnyfuns/swatchbook-blocks 0.53.0 → 0.55.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,6 @@
1
1
  import * as _$react from "react";
2
2
  import { ReactElement, ReactNode } from "react";
3
- import { Axis, Diagnostic, Permutation, Preset } from "@unpunnyfuns/swatchbook-core";
3
+ import { Axis, AxisVarianceResult, Diagnostic, Permutation, Preset } from "@unpunnyfuns/swatchbook-core";
4
4
 
5
5
  //#region src/format-color.d.ts
6
6
  /**
@@ -104,6 +104,21 @@ interface VirtualTokenListingShape {
104
104
  };
105
105
  }
106
106
  type VirtualPresetShape = Preset;
107
+ /**
108
+ * Wire shape of one `Project.jointOverrides` entry — same as core's
109
+ * `JointOverride` but with the block's `VirtualTokenShape` for the
110
+ * token values that ship over the wire.
111
+ */
112
+ interface VirtualJointOverrideShape {
113
+ axes: Record<string, string>;
114
+ tokens: Record<string, VirtualTokenShape>;
115
+ }
116
+ /**
117
+ * Map from path → cached `AxisVarianceResult`. Snapshot carries this
118
+ * so the `AxisVariance` block can O(1) look up which axes affect a
119
+ * token instead of re-running `analyzeAxisVariance` on every render.
120
+ */
121
+ type VirtualVarianceByPathShape = Record<string, AxisVarianceResult>;
107
122
  /**
108
123
  * Full project data read by blocks. Populated by the addon's preview
109
124
  * decorator (from the virtual module) or constructed by hand in
@@ -114,8 +129,17 @@ interface ProjectSnapshot {
114
129
  /** Axis names suppressed via `config.disabledAxes` — pinned to their defaults, hidden from the toolbar. */
115
130
  disabledAxes: readonly string[];
116
131
  presets: readonly VirtualPresetShape[];
117
- permutations: readonly VirtualPermutationShape[];
118
- permutationsResolved: Record<string, Record<string, VirtualTokenShape>>;
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>>;
119
143
  activePermutation: string;
120
144
  activeAxes: Readonly<Record<string, string>>;
121
145
  cssVarPrefix: string;
@@ -129,6 +153,41 @@ interface ProjectSnapshot {
129
153
  * absent.
130
154
  */
131
155
  listing?: Readonly<Record<string, VirtualTokenListingShape>>;
156
+ /**
157
+ * Per-axis cell maps — `cells[axis][context]` is the resolved token
158
+ * data for `{ ...defaults, [axis]: context }`. Bounded by
159
+ * `Σ(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.
162
+ */
163
+ cells?: Record<string, Record<string, Record<string, VirtualTokenShape>>>;
164
+ /**
165
+ * `Project.jointOverrides` flattened to entries for wire transport.
166
+ * Same ascending-arity iteration order the Map carries on the
167
+ * server side.
168
+ */
169
+ jointOverrides?: readonly (readonly [string, VirtualJointOverrideShape])[];
170
+ /**
171
+ * Cached per-path variance results. Blocks read this for O(1) axis
172
+ * variance lookup instead of recomputing on each render.
173
+ */
174
+ 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>;
180
+ /**
181
+ * Pre-built `resolveAt(tuple)` accessor. The addon's preview
182
+ * decorator instantiates this once per iframe lifetime — the
183
+ * underlying virtual-module exports (cells, jointOverrides, axes,
184
+ * defaultTuple) are stable, so a single resolver instance with
185
+ * internal per-tuple memoization is correct and avoids the
186
+ * per-render rebuild dance the blocks side used to do. Hand-built
187
+ * snapshots (tests, MDX) can omit this; blocks fall back to
188
+ * building locally from `cells` / `permutationsResolved`.
189
+ */
190
+ resolveAt?: (tuple: Record<string, string>) => Record<string, VirtualTokenShape>;
132
191
  }
133
192
  /**
134
193
  * Context carrying the full {@link ProjectSnapshot}. `null` sentinel lets
package/dist/index.mjs CHANGED
@@ -2,12 +2,12 @@ import './style.css';
2
2
  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
+ import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
5
6
  import { makeCSSVar } from "@terrazzo/token-tools/css";
6
7
  import { addons } from "storybook/preview-api";
7
- import { axes, css, cssVarPrefix, defaultPermutation, diagnostics, listing, permutations, permutationsResolved, presets } from "virtual:swatchbook/tokens";
8
+ import { axes, cells, css, cssVarPrefix, defaultTuple, diagnostics, jointOverrides, listing, presets, varianceByPath } from "virtual:swatchbook/tokens";
8
9
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
9
10
  import cx from "clsx";
10
- import { analyzeAxisVariance } from "@unpunnyfuns/swatchbook-core/variance";
11
11
  //#region src/format-color.ts
12
12
  const COLOR_FORMATS = [
13
13
  "hex",
@@ -343,13 +343,14 @@ const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
343
343
  let snapshot = {
344
344
  axes,
345
345
  presets,
346
- permutations,
347
- defaultPermutation,
348
- permutationsResolved,
349
346
  diagnostics,
350
347
  css,
351
348
  cssVarPrefix,
352
349
  listing: listing ?? {},
350
+ cells: cells ?? {},
351
+ jointOverrides: jointOverrides ?? [],
352
+ varianceByPath: varianceByPath ?? {},
353
+ defaultTuple: defaultTuple ?? {},
353
354
  version: 0
354
355
  };
355
356
  const listeners = /* @__PURE__ */ new Set();
@@ -361,13 +362,14 @@ function ensureSubscribed() {
361
362
  snapshot = {
362
363
  axes: payload.axes ?? snapshot.axes,
363
364
  presets: payload.presets ?? snapshot.presets,
364
- permutations: payload.permutations ?? snapshot.permutations,
365
- defaultPermutation: payload.defaultPermutation ?? snapshot.defaultPermutation,
366
- permutationsResolved: payload.permutationsResolved ?? snapshot.permutationsResolved,
367
365
  diagnostics: payload.diagnostics ?? snapshot.diagnostics,
368
366
  css: payload.css ?? snapshot.css,
369
367
  cssVarPrefix: payload.cssVarPrefix ?? snapshot.cssVarPrefix,
370
368
  listing: payload.listing ?? snapshot.listing,
369
+ cells: payload.cells ?? snapshot.cells,
370
+ jointOverrides: payload.jointOverrides ?? snapshot.jointOverrides,
371
+ varianceByPath: payload.varianceByPath ?? snapshot.varianceByPath,
372
+ defaultTuple: payload.defaultTuple ?? snapshot.defaultTuple,
371
373
  version: snapshot.version + 1
372
374
  };
373
375
  for (const cb of listeners) cb();
@@ -400,11 +402,31 @@ function ensureStylesheet(css) {
400
402
  }
401
403
  if (style.textContent !== css) style.textContent = css;
402
404
  }
403
- function defaultTuple(axes) {
405
+ function defaultTuple$1(axes) {
404
406
  const out = {};
405
407
  for (const axis of axes) out[axis.name] = axis.default;
406
408
  return out;
407
409
  }
410
+ /**
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."
414
+ */
415
+ function canonicalTupleKey(tuple) {
416
+ return Object.keys(tuple).toSorted().map((k) => `${k}:${tuple[k]}`).join("|");
417
+ }
418
+ /**
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;
408
430
  function tuplesEqual(a, b) {
409
431
  const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
410
432
  for (const k of keys) if (a[k] !== b[k]) return false;
@@ -413,17 +435,38 @@ function tuplesEqual(a, b) {
413
435
  function nameForTuple(themesList, tuple) {
414
436
  return themesList.find((t) => tuplesEqual(t.input, tuple))?.name;
415
437
  }
416
- function snapshotToData(snapshot) {
417
- return {
418
- activePermutation: snapshot.activePermutation,
419
- activeAxes: { ...snapshot.activeAxes },
420
- axes: snapshot.axes,
421
- permutations: snapshot.permutations,
422
- permutationsResolved: snapshot.permutationsResolved,
423
- resolved: snapshot.permutationsResolved[snapshot.activePermutation] ?? {},
424
- diagnostics: snapshot.diagnostics,
425
- cssVarPrefix: snapshot.cssVarPrefix,
426
- listing: snapshot.listing ?? {}
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.
445
+ */
446
+ function makeResolveAt(snapshot) {
447
+ const cells = snapshot.cells ?? {};
448
+ const jointOverrides = new Map(snapshot.jointOverrides ?? []);
449
+ const defaults = snapshot.defaultTuple ?? defaultTuple$1(snapshot.axes);
450
+ const resolver = buildResolveAt(snapshot.axes, cells, jointOverrides, defaults);
451
+ return (tuple) => resolver(tuple);
452
+ }
453
+ /**
454
+ * Build the `resolveAt` accessor for a snapshot. Prefers the
455
+ * snapshot's own `resolveAt` (the addon's preview decorator
456
+ * 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).
463
+ */
464
+ function snapshotResolveAt(snapshot) {
465
+ 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] ?? {};
427
470
  };
428
471
  }
429
472
  /**
@@ -440,8 +483,39 @@ function snapshotToData(snapshot) {
440
483
  */
441
484
  function useProject() {
442
485
  const snapshot = useOptionalSwatchbookData();
486
+ const axes = snapshot?.axes;
487
+ const cells = snapshot?.cells;
488
+ const jointOverrides = snapshot?.jointOverrides;
489
+ const dataDefaultTuple = snapshot?.defaultTuple;
490
+ const activePermutation = snapshot?.activePermutation;
491
+ const resolveAt = useMemo(() => {
492
+ if (!snapshot) return null;
493
+ return snapshotResolveAt(snapshot);
494
+ }, [
495
+ axes,
496
+ cells,
497
+ jointOverrides,
498
+ dataDefaultTuple,
499
+ activePermutation
500
+ ]);
501
+ const permutationNameByTuple = useMemo(() => buildPermutationNameByTuple(snapshot?.permutations ?? []), [snapshot?.permutations]);
443
502
  const fallback = useVirtualModuleFallback(snapshot === null);
444
- return snapshot !== null ? snapshotToData(snapshot) : fallback;
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 ?? {},
516
+ resolveAt,
517
+ permutationNameForTuple: (tuple) => permutationNameByTuple.get(canonicalTupleKey(tuple))
518
+ };
445
519
  }
446
520
  function useVirtualModuleFallback(enabled) {
447
521
  const contextPermutation = useActivePermutation();
@@ -459,19 +533,32 @@ function useVirtualModuleFallback(enabled) {
459
533
  if (!enabled) return;
460
534
  ensureStylesheet(tokens.css);
461
535
  }, [enabled, tokens.css]);
462
- const activeAxes = Object.keys(contextAxes).length > 0 ? { ...contextAxes } : channelGlobals.axes ?? defaultTuple(tokens.axes);
463
- const derivedName = nameForTuple(tokens.permutations, activeAxes);
464
- const activePermutation = contextPermutation || derivedName || tokens.defaultPermutation || tokens.permutations[0]?.name || "";
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(" · ") || "";
538
+ const resolveAt = useMemo(() => makeResolveAt({
539
+ axes: tokens.axes,
540
+ cells: tokens.cells,
541
+ jointOverrides: tokens.jointOverrides,
542
+ defaultTuple: tokens.defaultTuple
543
+ }), [
544
+ tokens.axes,
545
+ tokens.cells,
546
+ tokens.jointOverrides,
547
+ tokens.defaultTuple
548
+ ]);
549
+ const resolved = resolveAt(activeAxes);
550
+ const permutationNameForTuple = noPermutationName;
465
551
  return {
466
552
  activePermutation,
467
553
  activeAxes,
468
554
  axes: tokens.axes,
469
- permutations: tokens.permutations,
470
- permutationsResolved: tokens.permutationsResolved,
471
- resolved: tokens.permutationsResolved[activePermutation] ?? {},
555
+ resolved,
472
556
  diagnostics: tokens.diagnostics,
473
557
  cssVarPrefix: tokens.cssVarPrefix,
474
- listing: tokens.listing
558
+ listing: tokens.listing,
559
+ varianceByPath: tokens.varianceByPath,
560
+ resolveAt,
561
+ permutationNameForTuple
475
562
  };
476
563
  }
477
564
  /**
@@ -2656,7 +2743,7 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2656
2743
  //#region src/token-detail/internal.ts
2657
2744
  function useTokenDetailData(path) {
2658
2745
  const project = useProject();
2659
- const { activePermutation, activeAxes, axes, permutations, permutationsResolved, resolved, cssVarPrefix } = project;
2746
+ const { activePermutation, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt, permutationNameForTuple } = project;
2660
2747
  const typedResolved = resolved;
2661
2748
  return {
2662
2749
  token: typedResolved[path],
@@ -2664,10 +2751,11 @@ function useTokenDetailData(path) {
2664
2751
  activePermutation,
2665
2752
  activeAxes,
2666
2753
  axes,
2667
- permutations,
2668
- permutationsResolved,
2669
2754
  resolved: typedResolved,
2670
- cssVarPrefix
2755
+ cssVarPrefix,
2756
+ varianceByPath,
2757
+ resolveAt,
2758
+ permutationNameForTuple
2671
2759
  };
2672
2760
  }
2673
2761
  //#endregion
@@ -2792,27 +2880,25 @@ function treeHasTruncation(nodes) {
2792
2880
  //#endregion
2793
2881
  //#region src/token-detail/AxisVariance.tsx
2794
2882
  function AxisVariance({ path }) {
2795
- const { token, cssVar, axes, permutations, permutationsResolved, activeAxes, cssVarPrefix } = useTokenDetailData(path);
2883
+ const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt, permutationNameForTuple } = useTokenDetailData(path);
2796
2884
  const colorFormat = useColorFormat();
2797
2885
  const tokenType = token?.$type;
2798
2886
  const isColor = tokenType === "color";
2799
2887
  const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2800
2888
  const variance = useMemo(() => {
2801
- const result = analyzeAxisVariance(path, axes, permutations, permutationsResolved);
2889
+ const result = varianceByPath[path];
2890
+ if (!result) return {
2891
+ kind: "constant",
2892
+ varyingAxes: []
2893
+ };
2802
2894
  return {
2803
2895
  kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
2804
2896
  varyingAxes: result.varyingAxes
2805
2897
  };
2806
- }, [
2807
- path,
2808
- axes,
2809
- permutations,
2810
- permutationsResolved
2811
- ]);
2812
- if (permutations.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2898
+ }, [path, varianceByPath]);
2899
+ if (axes.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2813
2900
  if (variance.kind === "constant") {
2814
- const anyPermutation = permutations[0];
2815
- const value = anyPermutation ? formatFn(permutationsResolved[anyPermutation.name]?.[path]) : "—";
2901
+ const value = formatFn(resolveAt(activeAxes)[path]);
2816
2902
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2817
2903
  className: "sb-token-detail__section-header",
2818
2904
  children: "Values across axes"
@@ -2831,16 +2917,12 @@ function AxisVariance({ path }) {
2831
2917
  "aria-hidden": true
2832
2918
  }),
2833
2919
  value,
2834
- /* @__PURE__ */ jsxs("span", {
2920
+ /* @__PURE__ */ jsx("span", {
2835
2921
  style: {
2836
2922
  opacity: .6,
2837
2923
  marginLeft: 8
2838
2924
  },
2839
- children: [
2840
- "same across all ",
2841
- permutations.length,
2842
- " tuples"
2843
- ]
2925
+ children: "same across every axis"
2844
2926
  })
2845
2927
  ]
2846
2928
  })
@@ -2857,14 +2939,10 @@ function AxisVariance({ path }) {
2857
2939
  ...activeAxes,
2858
2940
  [axisName]: ctx
2859
2941
  };
2860
- const name = permutations.find((t) => {
2861
- const input = t.input;
2862
- return Object.keys(input).every((k) => input[k] === target[k]);
2863
- })?.name ?? "";
2864
2942
  return {
2865
2943
  ctx,
2866
- themeName: name,
2867
- value: name ? formatFn(permutationsResolved[name]?.[path]) : "—"
2944
+ themeName: permutationNameForTuple(target) ?? "",
2945
+ value: formatFn(resolveAt(target)[path])
2868
2946
  };
2869
2947
  });
2870
2948
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
@@ -2930,12 +3008,13 @@ function AxisVariance({ path }) {
2930
3008
  className: "sb-token-detail__theme-cell",
2931
3009
  children: row
2932
3010
  }), colAxis.contexts.map((col) => {
2933
- const name = tupleName(permutations, {
3011
+ const target = {
2934
3012
  ...activeAxes,
2935
3013
  [rowAxis.name]: row,
2936
3014
  [colAxis.name]: col
2937
- });
2938
- const value = name ? formatFn(permutationsResolved[name]?.[path]) : "—";
3015
+ };
3016
+ const name = permutationNameForTuple(target);
3017
+ const value = formatFn(resolveAt(target)[path]);
2939
3018
  return /* @__PURE__ */ jsxs("td", {
2940
3019
  className: "sb-token-detail__theme-cell",
2941
3020
  "data-row": row,
@@ -2965,12 +3044,6 @@ function valueFor(token, $type, format) {
2965
3044
  if (!token) return "—";
2966
3045
  return formatTokenValue(token.$value, $type, format);
2967
3046
  }
2968
- function tupleName(permutations, tuple) {
2969
- return permutations.find((t) => {
2970
- const input = t.input;
2971
- return Object.keys(input).every((k) => input[k] === tuple[k]);
2972
- })?.name;
2973
- }
2974
3047
  //#endregion
2975
3048
  //#region src/token-detail/CompositeBreakdown.tsx
2976
3049
  function CompositeBreakdown({ path }) {