@unpunnyfuns/swatchbook-blocks 0.54.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.mjs CHANGED
@@ -2,12 +2,13 @@ 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 { makeCSSVar } from "@terrazzo/token-tools/css";
5
+ import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
6
+ import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
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";
9
+ import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
8
10
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
9
11
  import cx from "clsx";
10
- import { analyzeAxisVariance } from "@unpunnyfuns/swatchbook-core/variance";
11
12
  //#region src/format-color.ts
12
13
  const COLOR_FORMATS = [
13
14
  "hex",
@@ -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);
@@ -343,13 +335,14 @@ const TOKENS_UPDATED_EVENT = "swatchbook/tokens-updated";
343
335
  let snapshot = {
344
336
  axes,
345
337
  presets,
346
- permutations,
347
- defaultPermutation,
348
- permutationsResolved,
349
338
  diagnostics,
350
339
  css,
351
340
  cssVarPrefix,
352
341
  listing: listing ?? {},
342
+ cells: cells ?? {},
343
+ jointOverrides: jointOverrides ?? [],
344
+ varianceByPath: varianceByPath ?? {},
345
+ defaultTuple: defaultTuple ?? {},
353
346
  version: 0
354
347
  };
355
348
  const listeners = /* @__PURE__ */ new Set();
@@ -361,13 +354,14 @@ function ensureSubscribed() {
361
354
  snapshot = {
362
355
  axes: payload.axes ?? snapshot.axes,
363
356
  presets: payload.presets ?? snapshot.presets,
364
- permutations: payload.permutations ?? snapshot.permutations,
365
- defaultPermutation: payload.defaultPermutation ?? snapshot.defaultPermutation,
366
- permutationsResolved: payload.permutationsResolved ?? snapshot.permutationsResolved,
367
357
  diagnostics: payload.diagnostics ?? snapshot.diagnostics,
368
358
  css: payload.css ?? snapshot.css,
369
359
  cssVarPrefix: payload.cssVarPrefix ?? snapshot.cssVarPrefix,
370
360
  listing: payload.listing ?? snapshot.listing,
361
+ cells: payload.cells ?? snapshot.cells,
362
+ jointOverrides: payload.jointOverrides ?? snapshot.jointOverrides,
363
+ varianceByPath: payload.varianceByPath ?? snapshot.varianceByPath,
364
+ defaultTuple: payload.defaultTuple ?? snapshot.defaultTuple,
371
365
  version: snapshot.version + 1
372
366
  };
373
367
  for (const cb of listeners) cb();
@@ -400,31 +394,46 @@ function ensureStylesheet(css) {
400
394
  }
401
395
  if (style.textContent !== css) style.textContent = css;
402
396
  }
403
- function defaultTuple(axes) {
397
+ function defaultTuple$1(axes) {
404
398
  const out = {};
405
399
  for (const axis of axes) out[axis.name] = axis.default;
406
400
  return out;
407
401
  }
408
- function tuplesEqual(a, b) {
409
- const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
410
- for (const k of keys) if (a[k] !== b[k]) return false;
411
- return true;
402
+ /**
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.
407
+ */
408
+ function tupleToName(axes, tuple) {
409
+ return axes.map((a) => tuple[a.name] ?? a.default).join(" · ");
412
410
  }
413
- function nameForTuple(themesList, tuple) {
414
- return themesList.find((t) => tuplesEqual(t.input, tuple))?.name;
411
+ /**
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.
417
+ */
418
+ function makeResolveAt(snapshot) {
419
+ const cells = snapshot.cells ?? {};
420
+ const jointOverrides = snapshot.jointOverrides ?? [];
421
+ const defaults = snapshot.defaultTuple ?? defaultTuple$1(snapshot.axes);
422
+ const resolver = buildResolveAt(snapshot.axes, cells, jointOverrides, defaults);
423
+ return (tuple) => resolver(tuple);
415
424
  }
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 ?? {}
427
- };
425
+ /**
426
+ * Build the `resolveAt` accessor for a snapshot. Prefers the
427
+ * snapshot's own `resolveAt` (the addon's preview decorator
428
+ * pre-builds one at module load — see `previewResolveAt` in
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.
433
+ */
434
+ function snapshotResolveAt(snapshot) {
435
+ if (snapshot.resolveAt) return snapshot.resolveAt;
436
+ return makeResolveAt(snapshot);
428
437
  }
429
438
  /**
430
439
  * Reads project data either from a mounted {@link SwatchbookProvider}
@@ -440,8 +449,56 @@ function snapshotToData(snapshot) {
440
449
  */
441
450
  function useProject() {
442
451
  const snapshot = useOptionalSwatchbookData();
452
+ const axes = snapshot?.axes;
453
+ const cells = snapshot?.cells;
454
+ const jointOverrides = snapshot?.jointOverrides;
455
+ const dataDefaultTuple = snapshot?.defaultTuple;
456
+ const activeAxes = snapshot?.activeAxes;
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;
462
+ const resolveAt = useMemo(() => {
463
+ if (!snapshot) return null;
464
+ return snapshotResolveAt(snapshot);
465
+ }, [
466
+ axes,
467
+ cells,
468
+ jointOverrides,
469
+ dataDefaultTuple,
470
+ activePermutation
471
+ ]);
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,
488
+ resolveAt,
489
+ axes,
490
+ cells,
491
+ jointOverrides,
492
+ dataDefaultTuple,
493
+ activePermutation,
494
+ activeAxes,
495
+ diagnostics,
496
+ cssVarPrefix,
497
+ listing,
498
+ varianceByPath
499
+ ]);
443
500
  const fallback = useVirtualModuleFallback(snapshot === null);
444
- return snapshot !== null ? snapshotToData(snapshot) : fallback;
501
+ return providerData ?? fallback;
445
502
  }
446
503
  function useVirtualModuleFallback(enabled) {
447
504
  const contextPermutation = useActivePermutation();
@@ -459,32 +516,46 @@ function useVirtualModuleFallback(enabled) {
459
516
  if (!enabled) return;
460
517
  ensureStylesheet(tokens.css);
461
518
  }, [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 || "";
465
- return {
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);
527
+ const resolveAt = useMemo(() => makeResolveAt({
528
+ axes: tokens.axes,
529
+ cells: tokens.cells,
530
+ jointOverrides: tokens.jointOverrides,
531
+ defaultTuple: tokens.defaultTuple
532
+ }), [
533
+ tokens.axes,
534
+ tokens.cells,
535
+ tokens.jointOverrides,
536
+ tokens.defaultTuple
537
+ ]);
538
+ return useMemo(() => ({
466
539
  activePermutation,
467
540
  activeAxes,
468
541
  axes: tokens.axes,
469
- permutations: tokens.permutations,
470
- permutationsResolved: tokens.permutationsResolved,
471
- resolved: tokens.permutationsResolved[activePermutation] ?? {},
542
+ resolved: resolveAt(activeAxes),
472
543
  diagnostics: tokens.diagnostics,
473
544
  cssVarPrefix: tokens.cssVarPrefix,
474
- listing: tokens.listing
475
- };
476
- }
477
- /**
478
- * Thin wrapper around Terrazzo's `makeCSSVar` so the block-display surface
479
- * and `packages/core/src/css.ts`'s emitter share one implementation. Any
480
- * future naming-policy shift in Terrazzo (casing, unicode, prefix handling)
481
- * reaches both surfaces at once instead of needing a parallel update here.
482
- */
483
- function makeCssVar(path, prefix) {
484
- return prefix ? makeCSSVar(path, {
485
- prefix,
486
- wrapVar: true
487
- }) : makeCSSVar(path, { wrapVar: true });
545
+ listing: tokens.listing,
546
+ varianceByPath: tokens.varianceByPath,
547
+ resolveAt,
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
+ ]);
488
559
  }
489
560
  /**
490
561
  * Resolve a token's CSS var reference, preferring the authoritative name
@@ -567,15 +638,6 @@ function BorderSample({ path }) {
567
638
  //#endregion
568
639
  //#region src/internal/data-attr.ts
569
640
  /**
570
- * Produce a prefixed `data-*` attribute name when `prefix` is set, bare
571
- * `data-<key>` otherwise. Mirrors `dataAttr` in `@unpunnyfuns/swatchbook-core`
572
- * so block wrappers and emitted-CSS selectors stay in lockstep without
573
- * blocks taking a runtime dep on core.
574
- */
575
- function dataAttr(prefix, key) {
576
- return prefix ? `data-${prefix}-${key}` : `data-${key}`;
577
- }
578
- /**
579
641
  * Marker attribute set on every block wrapper. Retained as a stable hook
580
642
  * for consumer-side selectors (e.g. when a host app wants to target or
581
643
  * override block chrome without relying on hashed class names).
@@ -610,59 +672,73 @@ function themeAttrs(prefix, themeName) {
610
672
  }
611
673
  //#endregion
612
674
  //#region src/internal/sort-tokens.ts
613
- /**
614
- * Stable sort for a filtered `[path, token][]` list.
615
- *
616
- * `sortBy: 'path'` — lexicographic on the dot-path (locale-aware, numeric).
617
- * `sortBy: 'value'` — per-`$type` ordering:
618
- * - `dimension` / `duration` → numeric pixels / ms (via `toMagnitude`).
619
- * - `fontWeight` / `opacity` / `number` / `lineHeight` → numeric.
620
- * - `color` → perceptual by oklch L → C → H.
621
- * - `fontFamily` / `strokeStyle` (string form) → lexicographic.
622
- * - Composites (`typography`, `shadow`, `border`, `gradient`, `transition`)
623
- * fall back to path-alpha. No useful single-axis order.
624
- * `sortBy: 'none'` preserve input order (still respects `sortDir: 'desc'`
625
- * as a reverse).
626
- */
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
+ }
627
705
  function sortTokens(entries, options = {}) {
628
706
  const by = options.by ?? "path";
629
707
  const dir = options.dir ?? "asc";
630
708
  const sign = dir === "desc" ? -1 : 1;
631
709
  if (by === "none") return dir === "desc" ? [...entries].toReversed() : [...entries];
632
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));
633
713
  return [...entries].toSorted(([aPath, aTok], [bPath, bTok]) => {
634
- const cmp = compareValue(aTok, bTok);
714
+ const cmp = compareValue(aTok, bTok, keys);
635
715
  if (cmp !== 0) return sign * cmp;
636
716
  return sign * aPath.localeCompare(bPath, void 0, { numeric: true });
637
717
  });
638
718
  }
639
- function compareValue(a, b) {
640
- const type = a.$type;
641
- if (type !== b.$type) return String(type ?? "").localeCompare(String(b.$type ?? ""));
642
- if (!type) return 0;
643
- if (type === "dimension" || type === "duration" || type === "fontWeight" || type === "opacity" || type === "number" || type === "lineHeight") {
644
- const av = toMagnitude(a.$value);
645
- const bv = toMagnitude(b.$value);
646
- if (Number.isFinite(av) && Number.isFinite(bv)) return av - bv;
647
- if (Number.isFinite(av)) return -1;
648
- 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;
649
729
  return 0;
650
730
  }
651
- if (type === "color") {
652
- const ak = colorKey(a.$value);
653
- const bk = colorKey(b.$value);
654
- if (!ak && !bk) return 0;
655
- if (!ak) return 1;
656
- if (!bk) return -1;
657
- if (ak.l !== bk.l) return ak.l - bk.l;
658
- if (ak.c !== bk.c) return ak.c - bk.c;
659
- return ak.h - bk.h;
660
- }
661
- if (type === "fontFamily" || type === "strokeStyle") {
662
- const as = toDisplayable(a.$value);
663
- const bs = toDisplayable(b.$value);
664
- 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;
665
740
  }
741
+ if (ak.kind === "string" && bk.kind === "string") return ak.value.localeCompare(bk.value, void 0, { numeric: true });
666
742
  return 0;
667
743
  }
668
744
  function toMagnitude(v) {
@@ -682,6 +758,14 @@ function toMagnitude(v) {
682
758
  }
683
759
  return NaN;
684
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
+ }
685
769
  function colorKey(v) {
686
770
  if (!v || typeof v !== "object") return null;
687
771
  try {
@@ -698,9 +782,9 @@ function colorKey(v) {
698
782
  } else return null;
699
783
  const [l, chroma, h] = new Color(source).to("oklch").coords;
700
784
  return {
701
- l: Number.isFinite(l) ? l : 0,
702
- c: Number.isFinite(chroma) ? chroma : 0,
703
- h: Number.isFinite(h) ? h : 0
785
+ l: safeNumber(l),
786
+ c: safeNumber(chroma),
787
+ h: safeNumber(h)
704
788
  };
705
789
  } catch {
706
790
  return null;
@@ -2030,9 +2114,9 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2030
2114
  * True when rendering inside Chromatic's snapshot runner. Chromatic's
2031
2115
  * browser ships a recognisable user-agent string; checked here so
2032
2116
  * motion-looping components can fall back to their static state for
2033
- * deterministic snapshots without needing a global Chromatic parameter
2034
- * (globally forcing `prefersReducedMotion: true` broke Chromatic's
2035
- * 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.
2036
2120
  */
2037
2121
  function isChromatic() {
2038
2122
  if (typeof navigator === "undefined") return false;
@@ -2656,7 +2740,7 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2656
2740
  //#region src/token-detail/internal.ts
2657
2741
  function useTokenDetailData(path) {
2658
2742
  const project = useProject();
2659
- const { activePermutation, activeAxes, axes, permutations, permutationsResolved, resolved, cssVarPrefix } = project;
2743
+ const { activePermutation, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt, permutationNameForTuple } = project;
2660
2744
  const typedResolved = resolved;
2661
2745
  return {
2662
2746
  token: typedResolved[path],
@@ -2664,10 +2748,11 @@ function useTokenDetailData(path) {
2664
2748
  activePermutation,
2665
2749
  activeAxes,
2666
2750
  axes,
2667
- permutations,
2668
- permutationsResolved,
2669
2751
  resolved: typedResolved,
2670
- cssVarPrefix
2752
+ cssVarPrefix,
2753
+ varianceByPath,
2754
+ resolveAt,
2755
+ permutationNameForTuple
2671
2756
  };
2672
2757
  }
2673
2758
  //#endregion
@@ -2792,27 +2877,30 @@ function treeHasTruncation(nodes) {
2792
2877
  //#endregion
2793
2878
  //#region src/token-detail/AxisVariance.tsx
2794
2879
  function AxisVariance({ path }) {
2795
- const { token, cssVar, axes, permutations, permutationsResolved, activeAxes, cssVarPrefix } = useTokenDetailData(path);
2880
+ const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt, permutationNameForTuple } = useTokenDetailData(path);
2796
2881
  const colorFormat = useColorFormat();
2797
2882
  const tokenType = token?.$type;
2798
2883
  const isColor = tokenType === "color";
2799
2884
  const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2800
2885
  const variance = useMemo(() => {
2801
- const result = analyzeAxisVariance(path, axes, permutations, permutationsResolved);
2802
- return {
2803
- kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
2804
- varyingAxes: result.varyingAxes
2805
- };
2806
- }, [
2807
- path,
2808
- axes,
2809
- permutations,
2810
- permutationsResolved
2811
- ]);
2812
- if (permutations.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2886
+ const result = varianceByPath[path];
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
+ }
2900
+ }, [path, varianceByPath]);
2901
+ if (axes.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2813
2902
  if (variance.kind === "constant") {
2814
- const anyPermutation = permutations[0];
2815
- const value = anyPermutation ? formatFn(permutationsResolved[anyPermutation.name]?.[path]) : "—";
2903
+ const value = formatFn(resolveAt(activeAxes)[path]);
2816
2904
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2817
2905
  className: "sb-token-detail__section-header",
2818
2906
  children: "Values across axes"
@@ -2831,16 +2919,12 @@ function AxisVariance({ path }) {
2831
2919
  "aria-hidden": true
2832
2920
  }),
2833
2921
  value,
2834
- /* @__PURE__ */ jsxs("span", {
2922
+ /* @__PURE__ */ jsx("span", {
2835
2923
  style: {
2836
2924
  opacity: .6,
2837
2925
  marginLeft: 8
2838
2926
  },
2839
- children: [
2840
- "same across all ",
2841
- permutations.length,
2842
- " tuples"
2843
- ]
2927
+ children: "same across every axis"
2844
2928
  })
2845
2929
  ]
2846
2930
  })
@@ -2848,8 +2932,7 @@ function AxisVariance({ path }) {
2848
2932
  })] });
2849
2933
  }
2850
2934
  if (variance.kind === "one-axis") {
2851
- const axisName = variance.varyingAxes[0];
2852
- if (!axisName) return /* @__PURE__ */ jsx(Fragment$1, {});
2935
+ const axisName = variance.axis;
2853
2936
  const axis = axes.find((a) => a.name === axisName);
2854
2937
  if (!axis) return /* @__PURE__ */ jsx(Fragment$1, {});
2855
2938
  const contextValues = axis.contexts.map((ctx) => {
@@ -2857,14 +2940,10 @@ function AxisVariance({ path }) {
2857
2940
  ...activeAxes,
2858
2941
  [axisName]: ctx
2859
2942
  };
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
2943
  return {
2865
2944
  ctx,
2866
- themeName: name,
2867
- value: name ? formatFn(permutationsResolved[name]?.[path]) : "—"
2945
+ themeName: permutationNameForTuple(target) ?? "",
2946
+ value: formatFn(resolveAt(target)[path])
2868
2947
  };
2869
2948
  });
2870
2949
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
@@ -2930,12 +3009,13 @@ function AxisVariance({ path }) {
2930
3009
  className: "sb-token-detail__theme-cell",
2931
3010
  children: row
2932
3011
  }), colAxis.contexts.map((col) => {
2933
- const name = tupleName(permutations, {
3012
+ const target = {
2934
3013
  ...activeAxes,
2935
3014
  [rowAxis.name]: row,
2936
3015
  [colAxis.name]: col
2937
- });
2938
- const value = name ? formatFn(permutationsResolved[name]?.[path]) : "—";
3016
+ };
3017
+ const name = permutationNameForTuple(target);
3018
+ const value = formatFn(resolveAt(target)[path]);
2939
3019
  return /* @__PURE__ */ jsxs("td", {
2940
3020
  className: "sb-token-detail__theme-cell",
2941
3021
  "data-row": row,
@@ -2965,12 +3045,6 @@ function valueFor(token, $type, format) {
2965
3045
  if (!token) return "—";
2966
3046
  return formatTokenValue(token.$value, $type, format);
2967
3047
  }
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
3048
  //#endregion
2975
3049
  //#region src/token-detail/CompositeBreakdown.tsx
2976
3050
  function CompositeBreakdown({ path }) {
@@ -3702,6 +3776,7 @@ function buildTree(resolved, root, typeFilter) {
3702
3776
  let node = rootNode;
3703
3777
  for (let i = 0; i < segments.length - 1; i += 1) {
3704
3778
  const seg = segments[i];
3779
+ if (seg === void 0) continue;
3705
3780
  const prefix = [...rootSegments, ...segments.slice(0, i + 1)].join(".");
3706
3781
  let child = node.children.find((c) => c.kind === "group" && c.segment === seg);
3707
3782
  if (!child) {