@unpunnyfuns/swatchbook-blocks 0.68.0 → 0.69.1

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
@@ -6,12 +6,35 @@ import { getVariance, listPaths, resolveAllWithProvenanceAt } from "@unpunnyfuns
6
6
  import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
7
7
  import { SWATCHBOOK_STYLE_ELEMENT_ID, ensureStyleElement } from "@unpunnyfuns/swatchbook-core/style-element";
8
8
  import { tupleToName } from "@unpunnyfuns/swatchbook-core/themes";
9
- import { addons } from "storybook/preview-api";
10
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
11
10
  import { dataAttr } from "@unpunnyfuns/swatchbook-core/data-attr";
12
11
  import { matchPath } from "@unpunnyfuns/swatchbook-core/match-path";
13
12
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
14
13
  import cx from "clsx";
14
+ //#region src/internal/channel.ts
15
+ let channel = null;
16
+ const pending = /* @__PURE__ */ new Set();
17
+ /**
18
+ * Inject the host channel. Called once by the addon preview at init. Runs any
19
+ * subscribers that registered before the channel existed, then clears the
20
+ * queue. Idempotent for consumers: each subscriber guards its own attach, so a
21
+ * re-register (HMR) does not double-wire.
22
+ */
23
+ function registerChannel(next) {
24
+ channel = next;
25
+ for (const attach of pending) attach(next);
26
+ pending.clear();
27
+ }
28
+ /**
29
+ * Attach a subscriber to the host channel now if one is registered, else queue
30
+ * it to run the moment {@link registerChannel} fires. Blocks call this at
31
+ * module load; the queue absorbs the gap until the addon injects the channel.
32
+ */
33
+ function onChannel(attach) {
34
+ if (channel) attach(channel);
35
+ else pending.add(attach);
36
+ }
37
+ //#endregion
15
38
  //#region src/internal/channel-globals.ts
16
39
  const AXES_GLOBAL_KEY = "swatchbookAxes";
17
40
  const COLOR_FORMAT_GLOBAL_KEY = "swatchbookColorFormat";
@@ -24,10 +47,9 @@ let subscribed$1 = false;
24
47
  function isColorFormat(value) {
25
48
  return typeof value === "string" && COLOR_FORMATS.includes(value);
26
49
  }
27
- function ensureSubscribed$1() {
28
- if (subscribed$1 || typeof window === "undefined") return;
50
+ function attach$1(channel) {
51
+ if (subscribed$1) return;
29
52
  subscribed$1 = true;
30
- const channel = addons.getChannel();
31
53
  let lastFingerprint = "";
32
54
  const onGlobals = (payload) => {
33
55
  const globals = payload.globals;
@@ -49,9 +71,8 @@ function ensureSubscribed$1() {
49
71
  channel.on("updateGlobals", onGlobals);
50
72
  channel.on("setGlobals", onGlobals);
51
73
  }
52
- ensureSubscribed$1();
74
+ onChannel(attach$1);
53
75
  function subscribe$2(cb) {
54
- ensureSubscribed$1();
55
76
  listeners$1.add(cb);
56
77
  return () => {
57
78
  listeners$1.delete(cb);
@@ -179,16 +200,15 @@ function applyPatch(patch) {
179
200
  function registerTokenSource(source) {
180
201
  applyPatch(source);
181
202
  }
182
- function ensureSubscribed() {
183
- if (subscribed || typeof window === "undefined") return;
203
+ function attach(channel) {
204
+ if (subscribed) return;
184
205
  subscribed = true;
185
- addons.getChannel().on(TOKENS_UPDATED_EVENT, (payload) => {
206
+ channel.on(TOKENS_UPDATED_EVENT, (payload) => {
186
207
  applyPatch(payload);
187
208
  });
188
209
  }
189
- ensureSubscribed();
210
+ onChannel(attach);
190
211
  function subscribe$1(cb) {
191
- ensureSubscribed();
192
212
  listeners.add(cb);
193
213
  return () => {
194
214
  listeners.delete(cb);
@@ -235,9 +255,9 @@ function snapshotResolveAt(snapshot) {
235
255
  *
236
256
  * The provider-less path is what makes the hook safe to call from MDX
237
257
  * doc blocks and autodocs renders where no story is active. It
238
- * self-mounts the virtual module's per-theme CSS and tracks the active
239
- * tuple via the `globalsUpdated` channel event; {@link useGlobals} from
240
- * `storybook/preview-api` would throw outside a story render.
258
+ * self-mounts the injected snapshot's per-theme CSS and tracks the active
259
+ * tuple via the injected channel's `globalsUpdated` event, since a
260
+ * story-scoped globals hook only works while a story is rendering.
241
261
  */
242
262
  function useProject() {
243
263
  const snapshot = useOptionalSwatchbookData();
@@ -363,13 +383,25 @@ function resolveColorValue(path, raw, colorFormat, project) {
363
383
  }
364
384
  //#endregion
365
385
  //#region src/border-preview/BorderSample.tsx
366
- function BorderSample({ path }) {
386
+ /**
387
+ * Pure derivation of a single border token's sample data from resolved
388
+ * project data. Extracted so it is unit-testable without React or a store.
389
+ */
390
+ function deriveBorderSample(path, project) {
391
+ return { cssVar: resolveCssVar(path, project) };
392
+ }
393
+ /** Pure presentation for a single border token's sample. Renders from plain props. */
394
+ function BorderSampleView({ cssVar }) {
367
395
  return /* @__PURE__ */ jsx("div", {
368
396
  className: "sb-border-sample",
369
- style: { border: resolveCssVar(path, useProject()) },
397
+ style: { border: cssVar },
370
398
  "aria-hidden": true
371
399
  });
372
400
  }
401
+ function BorderSample({ path }) {
402
+ const { cssVar } = deriveBorderSample(path, useProject());
403
+ return /* @__PURE__ */ jsx(BorderSampleView, { cssVar });
404
+ }
373
405
  //#endregion
374
406
  //#region src/internal/composite-sample-format.ts
375
407
  /**
@@ -552,29 +584,34 @@ function toDisplayable(v) {
552
584
  }
553
585
  //#endregion
554
586
  //#region src/BorderPreview.tsx
555
- function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
556
- const project = useProject();
557
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
558
- const colorFormat = useColorFormat();
559
- const rows = useMemo(() => {
560
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
561
- if (token.$type !== "border") return false;
562
- return matchPath(path, filter);
563
- }), {
564
- by: sortBy,
565
- dir: sortDir
566
- }).map(([path, token]) => ({
587
+ /**
588
+ * Pure derivation of the preview's display rows from resolved project data.
589
+ * Extracted so it is unit-testable without React or a store.
590
+ */
591
+ function deriveBorderRows(resolved, project, { filter, sortBy, sortDir, colorFormat }) {
592
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
593
+ if (token.$type !== "border") return false;
594
+ return matchPath(path, filter);
595
+ }), {
596
+ by: sortBy,
597
+ dir: sortDir
598
+ }).map(([path, token]) => {
599
+ const value = token.$value ?? {};
600
+ return {
567
601
  path,
568
602
  cssVar: resolveCssVar(path, project),
569
- value: token.$value ?? {}
570
- }));
571
- }, [
572
- resolved,
573
- filter,
574
- project,
575
- sortBy,
576
- sortDir
577
- ]);
603
+ width: formatDimension$1(value.width),
604
+ style: value.style != null ? String(value.style) : "—",
605
+ color: formatSubColor(value.color, colorFormat)
606
+ };
607
+ });
608
+ }
609
+ /**
610
+ * Pure presentation for the border preview. Renders from plain props;
611
+ * composes the connected `BorderSample` as a child (that child reads the
612
+ * project itself).
613
+ */
614
+ function BorderPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
578
615
  const captionText = caption ?? `${rows.length} border${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
579
616
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
580
617
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -612,23 +649,48 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
612
649
  className: "sb-border-preview__breakdown-key",
613
650
  children: "width"
614
651
  }),
615
- /* @__PURE__ */ jsx("span", { children: formatDimension$1(row.value.width) }),
652
+ /* @__PURE__ */ jsx("span", { children: row.width }),
616
653
  /* @__PURE__ */ jsx("span", {
617
654
  className: "sb-border-preview__breakdown-key",
618
655
  children: "style"
619
656
  }),
620
- /* @__PURE__ */ jsx("span", { children: row.value.style != null ? String(row.value.style) : "—" }),
657
+ /* @__PURE__ */ jsx("span", { children: row.style }),
621
658
  /* @__PURE__ */ jsx("span", {
622
659
  className: "sb-border-preview__breakdown-key",
623
660
  children: "color"
624
661
  }),
625
- /* @__PURE__ */ jsx("span", { children: formatSubColor(row.value.color, colorFormat) })
662
+ /* @__PURE__ */ jsx("span", { children: row.color })
626
663
  ]
627
664
  })
628
665
  ]
629
666
  }, row.path))]
630
667
  });
631
668
  }
669
+ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
670
+ const project = useProject();
671
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
672
+ const colorFormat = useColorFormat();
673
+ return /* @__PURE__ */ jsx(BorderPreviewView, {
674
+ rows: useMemo(() => deriveBorderRows(resolved, project, {
675
+ filter,
676
+ sortBy,
677
+ sortDir,
678
+ colorFormat
679
+ }), [
680
+ resolved,
681
+ project,
682
+ filter,
683
+ sortBy,
684
+ sortDir,
685
+ colorFormat
686
+ ]),
687
+ activeTheme,
688
+ cssVarPrefix,
689
+ activeAxes,
690
+ filter,
691
+ caption
692
+ });
693
+ }
632
694
  //#endregion
633
695
  //#region src/ColorPalette.tsx
634
696
  function fixedPrefixLength(filter) {
@@ -641,51 +703,48 @@ function fixedPrefixLength(filter) {
641
703
  }
642
704
  return fixed;
643
705
  }
644
- function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
645
- const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
646
- const colorFormat = useColorFormat();
647
- const groups = useMemo(() => {
648
- const projectFields = {
649
- listing,
650
- cssVarPrefix
651
- };
652
- const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
653
- if (token.$type !== "color") return false;
654
- return matchPath(path, filter);
655
- }), {
656
- by: sortBy,
657
- dir: sortDir
658
- });
659
- const maxDepth = entries.reduce((m, [p]) => Math.max(m, p.split(".").length), 0);
660
- const effectiveGroupBy = groupBy ?? Math.min(fixedPrefixLength(filter) + 1, Math.max(maxDepth - 1, 1));
661
- const bucket = /* @__PURE__ */ new Map();
662
- for (const [path, token] of entries) {
663
- const segments = path.split(".");
664
- const groupKey = segments.slice(0, effectiveGroupBy).join(".");
665
- const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
666
- const list = bucket.get(groupKey) ?? [];
667
- const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
668
- list.push({
669
- path,
670
- leaf,
671
- cssVar: resolveCssVar(path, projectFields),
672
- value: formatted.value,
673
- outOfGamut: formatted.outOfGamut
674
- });
675
- bucket.set(groupKey, list);
676
- }
677
- return [...bucket.entries()].toSorted(([a], [b]) => a.localeCompare(b, void 0, { numeric: true }));
678
- }, [
679
- resolved,
706
+ /**
707
+ * Pure derivation of the palette's grouped swatch rows from resolved project
708
+ * data. Extracted so it is unit-testable without React or a store.
709
+ */
710
+ function deriveColorPaletteGroups(resolved, listing, cssVarPrefix, { filter, groupBy, sortBy, sortDir, colorFormat }) {
711
+ const projectFields = {
680
712
  listing,
681
- cssVarPrefix,
682
- filter,
683
- groupBy,
684
- colorFormat,
685
- sortBy,
686
- sortDir
687
- ]);
688
- const totalCount = groups.reduce((acc, [, swatches]) => acc + swatches.length, 0);
713
+ cssVarPrefix
714
+ };
715
+ const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
716
+ if (token.$type !== "color") return false;
717
+ return matchPath(path, filter);
718
+ }), {
719
+ by: sortBy,
720
+ dir: sortDir
721
+ });
722
+ const maxDepth = entries.reduce((m, [p]) => Math.max(m, p.split(".").length), 0);
723
+ const effectiveGroupBy = groupBy ?? Math.min(fixedPrefixLength(filter) + 1, Math.max(maxDepth - 1, 1));
724
+ const bucket = /* @__PURE__ */ new Map();
725
+ for (const [path, token] of entries) {
726
+ const segments = path.split(".");
727
+ const groupKey = segments.slice(0, effectiveGroupBy).join(".");
728
+ const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
729
+ const list = bucket.get(groupKey) ?? [];
730
+ const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
731
+ list.push({
732
+ path,
733
+ leaf,
734
+ cssVar: resolveCssVar(path, projectFields),
735
+ value: formatted.value,
736
+ outOfGamut: formatted.outOfGamut
737
+ });
738
+ bucket.set(groupKey, list);
739
+ }
740
+ return [...bucket.entries()].toSorted(([a], [b]) => a.localeCompare(b, void 0, { numeric: true })).map(([group, swatches]) => ({
741
+ group,
742
+ swatches
743
+ }));
744
+ }
745
+ /** Pure presentation for the color palette. Renders from plain props. */
746
+ function ColorPaletteView({ groups, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
747
+ const totalCount = groups.reduce((acc, { swatches }) => acc + swatches.length, 0);
689
748
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
690
749
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
691
750
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -699,7 +758,7 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
699
758
  children: [/* @__PURE__ */ jsx("div", {
700
759
  className: "sb-block__caption",
701
760
  children: captionText
702
- }), groups.map(([group, swatches]) => /* @__PURE__ */ jsxs("section", {
761
+ }), groups.map(({ group, swatches }) => /* @__PURE__ */ jsxs("section", {
703
762
  className: "sb-color-palette__group",
704
763
  children: [/* @__PURE__ */ jsx("div", {
705
764
  className: "sb-color-palette__group-header",
@@ -732,6 +791,33 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
732
791
  }, group))]
733
792
  });
734
793
  }
794
+ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
795
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
796
+ const colorFormat = useColorFormat();
797
+ return /* @__PURE__ */ jsx(ColorPaletteView, {
798
+ groups: useMemo(() => deriveColorPaletteGroups(resolved, listing, cssVarPrefix, {
799
+ filter,
800
+ groupBy,
801
+ sortBy,
802
+ sortDir,
803
+ colorFormat
804
+ }), [
805
+ resolved,
806
+ listing,
807
+ cssVarPrefix,
808
+ filter,
809
+ groupBy,
810
+ sortBy,
811
+ sortDir,
812
+ colorFormat
813
+ ]),
814
+ activeTheme,
815
+ cssVarPrefix,
816
+ activeAxes,
817
+ filter,
818
+ caption
819
+ });
820
+ }
735
821
  //#endregion
736
822
  //#region src/indicators/resolve.ts
737
823
  /** Established-on set; `description` and `composes` are opt-in. */
@@ -1102,88 +1188,81 @@ function usePersistedState(key, initial) {
1102
1188
  const NOOP_REFERENCE = () => {};
1103
1189
  const BASE_LABEL = "base";
1104
1190
  const COLUMN_COUNT = 6;
1105
- function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants, id, indicators }) {
1106
- const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath } = useProject();
1107
- const colorFormat = useColorFormat();
1108
- const enabledIndicators = useMemo(() => ({
1109
- ...resolveIndicators(indicators),
1110
- gamut: false
1111
- }), [indicators]);
1112
- const blockKey = useBlockKey("ColorTable", [
1113
- filter,
1114
- caption,
1115
- id
1116
- ]);
1191
+ /**
1192
+ * Pure derivation of the table's display groups from resolved project data.
1193
+ * Carries per-variant `isDeprecated` / `token` / `variance` so the View
1194
+ * renders the indicator strip and deprecation state without reaching back
1195
+ * into the project. Extracted so it is unit-testable without React or a
1196
+ * store.
1197
+ */
1198
+ function deriveColorGroups(resolved, listing, cssVarPrefix, varianceByPath, { filter, variants, sortBy, sortDir, colorFormat }) {
1199
+ const defs = buildVariantDefs(variants);
1200
+ const projectFields = {
1201
+ listing,
1202
+ cssVarPrefix
1203
+ };
1204
+ const sorted = sortTokens(Object.entries(resolved).filter(([path, token]) => {
1205
+ if (token.$type !== "color") return false;
1206
+ return matchPath(path, filter);
1207
+ }), {
1208
+ by: sortBy,
1209
+ dir: sortDir
1210
+ });
1211
+ const groupMap = /* @__PURE__ */ new Map();
1212
+ for (const [path, token] of sorted) {
1213
+ const raw = token.$value;
1214
+ const hex = resolveColorValue(path, raw, "hex", projectFields);
1215
+ const hsl = formatColor(raw, "hsl");
1216
+ const oklch = formatColor(raw, "oklch");
1217
+ const active = pickActiveFormat(raw, colorFormat, hex, hsl, oklch);
1218
+ const match = matchVariant(path, defs.matchOrder);
1219
+ const dep = token.$deprecated;
1220
+ const variant = {
1221
+ label: match?.label ?? BASE_LABEL,
1222
+ path,
1223
+ token,
1224
+ variance: varianceByPath[path],
1225
+ cssVar: resolveCssVar(path, projectFields),
1226
+ value: active.value,
1227
+ outOfGamut: active.outOfGamut,
1228
+ hex: hex.value,
1229
+ hsl: hsl.value,
1230
+ oklch: oklch.value,
1231
+ isDeprecated: dep === true || typeof dep === "string" && dep.length > 0,
1232
+ ...token.$description !== void 0 && { description: token.$description },
1233
+ ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
1234
+ ...token.aliasChain !== void 0 && { aliasChain: token.aliasChain }
1235
+ };
1236
+ const basePath = match?.basePath ?? path;
1237
+ const existing = groupMap.get(basePath);
1238
+ if (existing) existing.variants.push(variant);
1239
+ else groupMap.set(basePath, {
1240
+ base: basePath,
1241
+ variants: [variant]
1242
+ });
1243
+ }
1244
+ const out = [];
1245
+ for (const { base, variants: vs } of groupMap.values()) {
1246
+ vs.sort((a, b) => orderIndex(a.label, defs) - orderIndex(b.label, defs));
1247
+ const searchText = vs.map((v) => `${v.path} ${v.value}`).join(" ");
1248
+ out.push({
1249
+ base,
1250
+ variants: vs,
1251
+ searchText
1252
+ });
1253
+ }
1254
+ return out;
1255
+ }
1256
+ /**
1257
+ * Pure presentation for the color table. Owns its own search, variant
1258
+ * selection, and row-expansion UI state; renders from the derived `groups`
1259
+ * view-model.
1260
+ */
1261
+ function ColorTableView({ groups, activeTheme, cssVarPrefix, activeAxes, colorFormat, enabledIndicators, blockKey, filter, caption, searchable = true, onSelect }) {
1117
1262
  const [query, setQuery] = usePersistedState(`${blockKey}::query`, "");
1118
1263
  const deferredQuery = useDeferredValue(query);
1119
1264
  const [selectedByBase, setSelectedByBase] = usePersistedState(`${blockKey}::selected`, {});
1120
1265
  const [expandedByBase, setExpandedByBase] = usePersistedState(`${blockKey}::expanded`, () => /* @__PURE__ */ new Set());
1121
- const defs = useMemo(() => buildVariantDefs(variants), [variants]);
1122
- const groups = useMemo(() => {
1123
- const projectFields = {
1124
- listing,
1125
- cssVarPrefix
1126
- };
1127
- const sorted = sortTokens(Object.entries(resolved).filter(([path, token]) => {
1128
- if (token.$type !== "color") return false;
1129
- return matchPath(path, filter);
1130
- }), {
1131
- by: sortBy,
1132
- dir: sortDir
1133
- });
1134
- const groupMap = /* @__PURE__ */ new Map();
1135
- for (const [path, token] of sorted) {
1136
- const raw = token.$value;
1137
- const hex = resolveColorValue(path, raw, "hex", projectFields);
1138
- const hsl = formatColor(raw, "hsl");
1139
- const oklch = formatColor(raw, "oklch");
1140
- const active = pickActiveFormat(raw, colorFormat, hex, hsl, oklch);
1141
- const match = matchVariant(path, defs.matchOrder);
1142
- const variant = {
1143
- label: match?.label ?? BASE_LABEL,
1144
- path,
1145
- token,
1146
- variance: varianceByPath[path],
1147
- cssVar: resolveCssVar(path, projectFields),
1148
- value: active.value,
1149
- outOfGamut: active.outOfGamut,
1150
- hex: hex.value,
1151
- hsl: hsl.value,
1152
- oklch: oklch.value,
1153
- ...token.$description !== void 0 && { description: token.$description },
1154
- ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
1155
- ...token.aliasChain !== void 0 && { aliasChain: token.aliasChain }
1156
- };
1157
- const basePath = match?.basePath ?? path;
1158
- const existing = groupMap.get(basePath);
1159
- if (existing) existing.variants.push(variant);
1160
- else groupMap.set(basePath, {
1161
- base: basePath,
1162
- variants: [variant]
1163
- });
1164
- }
1165
- const out = [];
1166
- for (const { base, variants: vs } of groupMap.values()) {
1167
- vs.sort((a, b) => orderIndex(a.label, defs) - orderIndex(b.label, defs));
1168
- const searchText = vs.map((v) => `${v.path} ${v.value}`).join(" ");
1169
- out.push({
1170
- base,
1171
- variants: vs,
1172
- searchText
1173
- });
1174
- }
1175
- return out;
1176
- }, [
1177
- resolved,
1178
- listing,
1179
- cssVarPrefix,
1180
- varianceByPath,
1181
- filter,
1182
- sortBy,
1183
- sortDir,
1184
- defs,
1185
- colorFormat
1186
- ]);
1187
1266
  const visibleGroups = useMemo(() => {
1188
1267
  if (!searchable || deferredQuery.trim() === "") return groups;
1189
1268
  return fuzzyFilter(groups, deferredQuery, (g) => g.searchText);
@@ -1296,12 +1375,59 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1296
1375
  })]
1297
1376
  });
1298
1377
  }
1378
+ /**
1379
+ * A grouped, searchable table of `$type: color` tokens. Suffix-matched
1380
+ * variants (see `variants` prop) collapse into a single row with a pill
1381
+ * selector; clicking a row expands it in place to show the description,
1382
+ * alias chain, and (for grouped rows) every variant's hex/HSL/OKLCH values.
1383
+ */
1384
+ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants, id, indicators }) {
1385
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath } = useProject();
1386
+ const colorFormat = useColorFormat();
1387
+ const enabledIndicators = useMemo(() => ({
1388
+ ...resolveIndicators(indicators),
1389
+ gamut: false
1390
+ }), [indicators]);
1391
+ const blockKey = useBlockKey("ColorTable", [
1392
+ filter,
1393
+ caption,
1394
+ id
1395
+ ]);
1396
+ return /* @__PURE__ */ jsx(ColorTableView, {
1397
+ groups: useMemo(() => deriveColorGroups(resolved, listing, cssVarPrefix, varianceByPath, {
1398
+ filter,
1399
+ variants,
1400
+ sortBy,
1401
+ sortDir,
1402
+ colorFormat
1403
+ }), [
1404
+ resolved,
1405
+ listing,
1406
+ cssVarPrefix,
1407
+ varianceByPath,
1408
+ filter,
1409
+ variants,
1410
+ sortBy,
1411
+ sortDir,
1412
+ colorFormat
1413
+ ]),
1414
+ activeTheme,
1415
+ cssVarPrefix,
1416
+ activeAxes,
1417
+ colorFormat,
1418
+ enabledIndicators,
1419
+ blockKey,
1420
+ filter,
1421
+ caption,
1422
+ searchable,
1423
+ onSelect
1424
+ });
1425
+ }
1299
1426
  const GroupRow = memo(function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect, enabled, colorFormat }) {
1300
1427
  const multi = group.variants.length > 1;
1301
1428
  const active = group.variants.find((v) => v.label === selectedLabel) ?? group.variants[0];
1302
1429
  const nameText = multi ? group.base : active.path;
1303
- const dep = active.token.$deprecated;
1304
- const isDeprecated = dep === true || typeof dep === "string" && dep.length > 0;
1430
+ const isDeprecated = active.isDeprecated;
1305
1431
  const handleRowActivate = () => {
1306
1432
  if (onSelect) onSelect(active.path);
1307
1433
  else onToggleExpand(group.base);
@@ -1590,6 +1716,7 @@ const severityLabel = {
1590
1716
  warn: "WARN",
1591
1717
  info: "INFO"
1592
1718
  };
1719
+ /** Aggregate diagnostics into a summary line + severity variant for the header. */
1593
1720
  function summarize(diagnostics) {
1594
1721
  if (diagnostics.length === 0) return {
1595
1722
  text: "✔ OK · no diagnostics",
@@ -1616,18 +1743,8 @@ function summarize(diagnostics) {
1616
1743
  function diagnosticKey(d, i) {
1617
1744
  return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
1618
1745
  }
1619
- /**
1620
- * Render the project's load diagnostics parser errors, resolver warnings,
1621
- * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
1622
- * when the project carries errors or warnings; stays collapsed for clean
1623
- * loads and info-only loads.
1624
- *
1625
- * Replaces the diagnostics section from the addon's (now-retired) Design
1626
- * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1627
- * on their own MDX pages.
1628
- */
1629
- function Diagnostics({ caption } = {}) {
1630
- const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1746
+ /** Pure presentation for the diagnostics list. Renders from plain props. */
1747
+ function DiagnosticsView({ caption, diagnostics, cssVarPrefix, activeAxes }) {
1631
1748
  const summary = useMemo(() => summarize(diagnostics), [diagnostics]);
1632
1749
  const headingText = caption ?? `Diagnostics · ${summary.text}`;
1633
1750
  return /* @__PURE__ */ jsx("div", {
@@ -1661,6 +1778,25 @@ function Diagnostics({ caption } = {}) {
1661
1778
  })
1662
1779
  });
1663
1780
  }
1781
+ /**
1782
+ * Render the project's load diagnostics — parser errors, resolver warnings,
1783
+ * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
1784
+ * when the project carries errors or warnings; stays collapsed for clean
1785
+ * loads and info-only loads.
1786
+ *
1787
+ * Replaces the diagnostics section from the addon's (now-retired) Design
1788
+ * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1789
+ * on their own MDX pages.
1790
+ */
1791
+ function Diagnostics({ caption } = {}) {
1792
+ const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1793
+ return /* @__PURE__ */ jsx(DiagnosticsView, {
1794
+ caption,
1795
+ diagnostics,
1796
+ cssVarPrefix,
1797
+ activeAxes
1798
+ });
1799
+ }
1664
1800
  //#endregion
1665
1801
  //#region src/dimension-scale/dimension-px.ts
1666
1802
  /**
@@ -1709,6 +1845,22 @@ function useRootFontSize() {
1709
1845
  }
1710
1846
  //#endregion
1711
1847
  //#region src/dimension-scale/DimensionBar.tsx
1848
+ /**
1849
+ * Pure derivation of a single dimension token's bar geometry from resolved
1850
+ * project data. Extracted so it is unit-testable without React or a store.
1851
+ */
1852
+ function deriveDimensionBar(path, project, rootFontSizePx) {
1853
+ const cssVar = resolveCssVar(path, project);
1854
+ const token = project.resolved[path];
1855
+ const pxValue = toPixels(token?.$value, rootFontSizePx);
1856
+ const capped = Number.isFinite(pxValue) && pxValue > 480;
1857
+ return {
1858
+ cssVar,
1859
+ pxValue,
1860
+ capped,
1861
+ cappedValue: capped ? `480px` : cssVar
1862
+ };
1863
+ }
1712
1864
  function withCap(visual) {
1713
1865
  return /* @__PURE__ */ jsxs("span", {
1714
1866
  className: "sb-dimension-bar sb-dimension-bar--capped",
@@ -1720,15 +1872,8 @@ function withCap(visual) {
1720
1872
  })]
1721
1873
  });
1722
1874
  }
1723
- function DimensionBar({ path, visual = "length" }) {
1724
- const project = useProject();
1725
- const { resolved } = project;
1726
- const rootFontSize = useRootFontSize();
1727
- const cssVar = resolveCssVar(path, project);
1728
- const token = resolved[path];
1729
- const pxValue = toPixels(token?.$value, rootFontSize);
1730
- const capped = Number.isFinite(pxValue) && pxValue > 480;
1731
- const cappedValue = capped ? `480px` : cssVar;
1875
+ /** Pure presentation for a single dimension token's bar/sample. Renders from plain props. */
1876
+ function DimensionBarView({ cssVar, capped, cappedValue, visual }) {
1732
1877
  switch (visual) {
1733
1878
  case "radius": return /* @__PURE__ */ jsx("div", {
1734
1879
  className: "sb-dimension-bar__radius-sample",
@@ -1756,6 +1901,15 @@ function DimensionBar({ path, visual = "length" }) {
1756
1901
  }
1757
1902
  }
1758
1903
  }
1904
+ function DimensionBar({ path, visual = "length" }) {
1905
+ const { cssVar, capped, cappedValue } = deriveDimensionBar(path, useProject(), useRootFontSize());
1906
+ return /* @__PURE__ */ jsx(DimensionBarView, {
1907
+ cssVar,
1908
+ capped,
1909
+ cappedValue,
1910
+ visual
1911
+ });
1912
+ }
1759
1913
  //#endregion
1760
1914
  //#region src/internal/format-token-value.ts
1761
1915
  /**
@@ -1860,31 +2014,30 @@ function formatUnknown(v) {
1860
2014
  }
1861
2015
  //#endregion
1862
2016
  //#region src/DimensionScale.tsx
1863
- function DimensionScale({ filter, visual = "length", caption, sortBy = "value", sortDir = "asc" }) {
1864
- const project = useProject();
1865
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1866
- const rootFontSize = useRootFontSize();
1867
- const rows = useMemo(() => {
1868
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1869
- if (token.$type !== "dimension") return false;
1870
- return matchPath(path, filter);
1871
- }), {
1872
- by: sortBy,
1873
- dir: sortDir,
1874
- rootFontSizePx: rootFontSize
1875
- }).map(([path, token]) => ({
1876
- path,
1877
- cssVar: resolveCssVar(path, project),
1878
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path])
1879
- }));
1880
- }, [
1881
- resolved,
1882
- filter,
1883
- project,
1884
- sortBy,
1885
- sortDir,
1886
- rootFontSize
1887
- ]);
2017
+ /**
2018
+ * Pure derivation of the scale's display rows from resolved project data.
2019
+ * Extracted so it is unit-testable without React or a store.
2020
+ */
2021
+ function deriveDimensionRows(resolved, project, { filter, sortBy, sortDir, rootFontSizePx }) {
2022
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2023
+ if (token.$type !== "dimension") return false;
2024
+ return matchPath(path, filter);
2025
+ }), {
2026
+ by: sortBy,
2027
+ dir: sortDir,
2028
+ rootFontSizePx
2029
+ }).map(([path, token]) => ({
2030
+ path,
2031
+ cssVar: resolveCssVar(path, project),
2032
+ displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path])
2033
+ }));
2034
+ }
2035
+ /**
2036
+ * Pure presentation for the dimension scale. Renders from plain props;
2037
+ * composes the connected `DimensionBar` as a child (that child reads the
2038
+ * project itself).
2039
+ */
2040
+ function DimensionScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, visual, filter, caption }) {
1888
2041
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1889
2042
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1890
2043
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -1926,6 +2079,32 @@ function DimensionScale({ filter, visual = "length", caption, sortBy = "value",
1926
2079
  }, row.path))]
1927
2080
  });
1928
2081
  }
2082
+ function DimensionScale({ filter, visual = "length", caption, sortBy = "value", sortDir = "asc" }) {
2083
+ const project = useProject();
2084
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2085
+ const rootFontSize = useRootFontSize();
2086
+ return /* @__PURE__ */ jsx(DimensionScaleView, {
2087
+ rows: useMemo(() => deriveDimensionRows(resolved, project, {
2088
+ filter,
2089
+ sortBy,
2090
+ sortDir,
2091
+ rootFontSizePx: rootFontSize
2092
+ }), [
2093
+ resolved,
2094
+ project,
2095
+ filter,
2096
+ sortBy,
2097
+ sortDir,
2098
+ rootFontSize
2099
+ ]),
2100
+ activeTheme,
2101
+ cssVarPrefix,
2102
+ activeAxes,
2103
+ visual,
2104
+ filter,
2105
+ caption
2106
+ });
2107
+ }
1929
2108
  //#endregion
1930
2109
  //#region src/FontFamilyPreview.tsx
1931
2110
  function stackString(raw) {
@@ -1933,28 +2112,25 @@ function stackString(raw) {
1933
2112
  if (Array.isArray(raw)) return raw.map(String).join(", ");
1934
2113
  return "";
1935
2114
  }
1936
- function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1937
- const project = useProject();
1938
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1939
- const rows = useMemo(() => {
1940
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1941
- if (token.$type !== "fontFamily") return false;
1942
- return matchPath(path, filter);
1943
- }), {
1944
- by: sortBy,
1945
- dir: sortDir
1946
- }).map(([path, token]) => ({
1947
- path,
1948
- cssVar: resolveCssVar(path, project),
1949
- stack: stackString(token.$value)
1950
- }));
1951
- }, [
1952
- resolved,
1953
- filter,
1954
- project,
1955
- sortBy,
1956
- sortDir
1957
- ]);
2115
+ /**
2116
+ * Pure derivation of the preview's display rows from resolved project data.
2117
+ * Extracted so it is unit-testable without React or a store.
2118
+ */
2119
+ function deriveFontFamilyRows(resolved, project, { filter, sortBy, sortDir }) {
2120
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2121
+ if (token.$type !== "fontFamily") return false;
2122
+ return matchPath(path, filter);
2123
+ }), {
2124
+ by: sortBy,
2125
+ dir: sortDir
2126
+ }).map(([path, token]) => ({
2127
+ path,
2128
+ cssVar: resolveCssVar(path, project),
2129
+ stack: stackString(token.$value)
2130
+ }));
2131
+ }
2132
+ /** Pure presentation for the font-family preview. Renders from plain props. */
2133
+ function FontFamilyPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
1958
2134
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1959
2135
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1960
2136
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -1994,6 +2170,29 @@ function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over th
1994
2170
  }, row.path))]
1995
2171
  });
1996
2172
  }
2173
+ function FontFamilyPreview({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
2174
+ const project = useProject();
2175
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2176
+ return /* @__PURE__ */ jsx(FontFamilyPreviewView, {
2177
+ rows: useMemo(() => deriveFontFamilyRows(resolved, project, {
2178
+ filter,
2179
+ sortBy,
2180
+ sortDir
2181
+ }), [
2182
+ resolved,
2183
+ project,
2184
+ filter,
2185
+ sortBy,
2186
+ sortDir
2187
+ ]),
2188
+ activeTheme,
2189
+ cssVarPrefix,
2190
+ activeAxes,
2191
+ sample,
2192
+ filter,
2193
+ caption
2194
+ });
2195
+ }
1997
2196
  //#endregion
1998
2197
  //#region src/internal/css-var-style.ts
1999
2198
  /**
@@ -2019,29 +2218,26 @@ function toWeight(raw) {
2019
2218
  }
2020
2219
  return NaN;
2021
2220
  }
2022
- function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
2023
- const project = useProject();
2024
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2025
- const rows = useMemo(() => {
2026
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2027
- if (token.$type !== "fontWeight") return false;
2028
- return matchPath(path, filter);
2029
- }), {
2030
- by: sortBy,
2031
- dir: sortDir
2032
- }).map(([path, token]) => ({
2033
- path,
2034
- cssVar: resolveCssVar(path, project),
2035
- display: token.$value == null ? "" : String(token.$value),
2036
- weight: toWeight(token.$value)
2037
- }));
2038
- }, [
2039
- resolved,
2040
- filter,
2041
- project,
2042
- sortBy,
2043
- sortDir
2044
- ]);
2221
+ /**
2222
+ * Pure derivation of the scale's display rows from resolved project data.
2223
+ * Extracted so it is unit-testable without React or a store.
2224
+ */
2225
+ function deriveFontWeightRows(resolved, project, { filter, sortBy, sortDir }) {
2226
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2227
+ if (token.$type !== "fontWeight") return false;
2228
+ return matchPath(path, filter);
2229
+ }), {
2230
+ by: sortBy,
2231
+ dir: sortDir
2232
+ }).map(([path, token]) => ({
2233
+ path,
2234
+ cssVar: resolveCssVar(path, project),
2235
+ display: token.$value == null ? "" : String(token.$value),
2236
+ weight: toWeight(token.$value)
2237
+ }));
2238
+ }
2239
+ /** Pure presentation for the font-weight scale. Renders from plain props. */
2240
+ function FontWeightScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
2045
2241
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2046
2242
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2047
2243
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2081,6 +2277,29 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
2081
2277
  }, row.path))]
2082
2278
  });
2083
2279
  }
2280
+ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
2281
+ const project = useProject();
2282
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2283
+ return /* @__PURE__ */ jsx(FontWeightScaleView, {
2284
+ rows: useMemo(() => deriveFontWeightRows(resolved, project, {
2285
+ filter,
2286
+ sortBy,
2287
+ sortDir
2288
+ }), [
2289
+ resolved,
2290
+ project,
2291
+ filter,
2292
+ sortBy,
2293
+ sortDir
2294
+ ]),
2295
+ activeTheme,
2296
+ cssVarPrefix,
2297
+ activeAxes,
2298
+ sample,
2299
+ filter,
2300
+ caption
2301
+ });
2302
+ }
2084
2303
  //#endregion
2085
2304
  //#region src/GradientPalette.tsx
2086
2305
  function asStops(raw) {
@@ -2095,33 +2314,37 @@ function stopCssColor(stop) {
2095
2314
  function stopKey(path, stop, fallback) {
2096
2315
  return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
2097
2316
  }
2098
- function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2099
- const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
2100
- const colorFormat = useColorFormat();
2101
- const rows = useMemo(() => {
2102
- const projectFields = {
2103
- listing,
2104
- cssVarPrefix
2105
- };
2106
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2107
- if (token.$type !== "gradient") return false;
2108
- return matchPath(path, filter);
2109
- }), {
2110
- by: sortBy,
2111
- dir: sortDir
2112
- }).map(([path, token]) => ({
2317
+ /**
2318
+ * Pure derivation of the palette's gradient rows from resolved project
2319
+ * data. Extracted so it is unit-testable without React or a store.
2320
+ */
2321
+ function deriveGradientRows(resolved, listing, cssVarPrefix, { filter, sortBy, sortDir, colorFormat }) {
2322
+ const projectFields = {
2323
+ listing,
2324
+ cssVarPrefix
2325
+ };
2326
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2327
+ if (token.$type !== "gradient") return false;
2328
+ return matchPath(path, filter);
2329
+ }), {
2330
+ by: sortBy,
2331
+ dir: sortDir
2332
+ }).map(([path, token]) => {
2333
+ const stops = asStops(token.$value);
2334
+ return {
2113
2335
  path,
2114
2336
  cssVar: resolveCssVar(path, projectFields),
2115
- stops: asStops(token.$value)
2116
- }));
2117
- }, [
2118
- resolved,
2119
- listing,
2120
- cssVarPrefix,
2121
- filter,
2122
- sortBy,
2123
- sortDir
2124
- ]);
2337
+ stops: stops.map((stop, i) => ({
2338
+ key: stopKey(path, stop, i),
2339
+ cssColor: stopCssColor(stop),
2340
+ value: formatColor(stop.color, colorFormat).value,
2341
+ positionPercent: ((stop.position ?? 0) * 100).toFixed(0)
2342
+ }))
2343
+ };
2344
+ });
2345
+ }
2346
+ /** Pure presentation for the gradient palette. Renders from plain props. */
2347
+ function GradientPaletteView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2125
2348
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2126
2349
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2127
2350
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2155,30 +2378,55 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2155
2378
  }),
2156
2379
  /* @__PURE__ */ jsx("div", {
2157
2380
  className: "sb-gradient-palette__stops",
2158
- children: row.stops.map((stop, i) => /* @__PURE__ */ jsxs("div", {
2381
+ children: row.stops.map((stop) => /* @__PURE__ */ jsxs("div", {
2159
2382
  className: "sb-gradient-palette__stop-row",
2160
2383
  children: [
2161
2384
  /* @__PURE__ */ jsx("span", {
2162
2385
  className: "sb-gradient-palette__stop-swatch",
2163
- style: { background: stopCssColor(stop) },
2386
+ style: { background: stop.cssColor },
2164
2387
  "aria-hidden": true
2165
2388
  }),
2166
- /* @__PURE__ */ jsx("span", { children: formatColor(stop.color, colorFormat).value }),
2389
+ /* @__PURE__ */ jsx("span", { children: stop.value }),
2167
2390
  /* @__PURE__ */ jsxs("span", {
2168
2391
  className: "sb-gradient-palette__stop-position",
2169
2392
  children: [
2170
2393
  "@ ",
2171
- ((stop.position ?? 0) * 100).toFixed(0),
2394
+ stop.positionPercent,
2172
2395
  "%"
2173
2396
  ]
2174
2397
  })
2175
2398
  ]
2176
- }, stopKey(row.path, stop, i)))
2399
+ }, stop.key))
2177
2400
  })
2178
2401
  ]
2179
2402
  }, row.path))]
2180
2403
  });
2181
2404
  }
2405
+ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2406
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
2407
+ const colorFormat = useColorFormat();
2408
+ return /* @__PURE__ */ jsx(GradientPaletteView, {
2409
+ rows: useMemo(() => deriveGradientRows(resolved, listing, cssVarPrefix, {
2410
+ filter,
2411
+ sortBy,
2412
+ sortDir,
2413
+ colorFormat
2414
+ }), [
2415
+ resolved,
2416
+ listing,
2417
+ cssVarPrefix,
2418
+ filter,
2419
+ sortBy,
2420
+ sortDir,
2421
+ colorFormat
2422
+ ]),
2423
+ activeTheme,
2424
+ cssVarPrefix,
2425
+ activeAxes,
2426
+ filter,
2427
+ caption
2428
+ });
2429
+ }
2182
2430
  //#endregion
2183
2431
  //#region src/internal/prefers-reduced-motion.ts
2184
2432
  function isChromatic() {
@@ -2280,10 +2528,16 @@ function resolveMotionSpec(token, themeTokens) {
2280
2528
  }
2281
2529
  return null;
2282
2530
  }
2283
- function MotionSample({ path, speed = 1, runKey = 0 }) {
2284
- const { resolved } = useProject();
2531
+ /**
2532
+ * Pure derivation of a single motion token's animation spec from resolved
2533
+ * project data. Extracted so it is unit-testable without React or a store.
2534
+ */
2535
+ function deriveMotionSample(path, project) {
2536
+ return { spec: resolveMotionSpec(project.resolved[path], project.resolved) };
2537
+ }
2538
+ /** Pure presentation + animation for a single motion token's sample. Renders from plain props. */
2539
+ function MotionSampleView({ spec, speed, runKey }) {
2285
2540
  const reducedMotion = usePrefersReducedMotion();
2286
- const spec = useMemo(() => resolveMotionSpec(resolved[path], resolved), [resolved, path]);
2287
2541
  const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
2288
2542
  const easing = spec?.easing ?? DEFAULT_EASING;
2289
2543
  const scaledDuration = Math.max(1, durationMs / speed);
@@ -2321,6 +2575,14 @@ function MotionSample({ path, speed = 1, runKey = 0 }) {
2321
2575
  })
2322
2576
  });
2323
2577
  }
2578
+ function MotionSample({ path, speed = 1, runKey = 0 }) {
2579
+ const { spec } = deriveMotionSample(path, useProject());
2580
+ return /* @__PURE__ */ jsx(MotionSampleView, {
2581
+ spec,
2582
+ speed,
2583
+ runKey
2584
+ });
2585
+ }
2324
2586
  //#endregion
2325
2587
  //#region src/MotionPreview.tsx
2326
2588
  const SPEEDS = [
@@ -2336,43 +2598,48 @@ function formatSpec(row) {
2336
2598
  case "cubicBezier": return `cubicBezier · ${row.easing}`;
2337
2599
  }
2338
2600
  }
2339
- function MotionPreview({ filter, caption }) {
2340
- const project = useProject();
2341
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2601
+ /**
2602
+ * Pure derivation of the preview's display rows from resolved project data.
2603
+ * Extracted so it is unit-testable without React or a store.
2604
+ */
2605
+ function deriveMotionRows(resolved, project, { filter }) {
2606
+ const collected = [];
2607
+ for (const [path, token] of Object.entries(resolved)) {
2608
+ if (filter && !matchPath(path, filter)) continue;
2609
+ if (!filter && ![
2610
+ "transition",
2611
+ "duration",
2612
+ "cubicBezier"
2613
+ ].includes(token.$type ?? "")) continue;
2614
+ const kind = token.$type;
2615
+ if (!kind) continue;
2616
+ const spec = resolveMotionSpec(token, resolved);
2617
+ if (!spec) continue;
2618
+ collected.push({
2619
+ path,
2620
+ cssVar: resolveCssVar(path, project),
2621
+ durationMs: spec.durationMs,
2622
+ easing: spec.easing,
2623
+ kind
2624
+ });
2625
+ }
2626
+ collected.sort((a, b) => {
2627
+ if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
2628
+ return a.path.localeCompare(b.path, void 0, { numeric: true });
2629
+ });
2630
+ return collected;
2631
+ }
2632
+ /**
2633
+ * Pure presentation for the motion preview. Owns the speed/replay controls'
2634
+ * local UI state and the `prefers-reduced-motion` read (a browser-environment
2635
+ * concern, not project data); renders from the derived `rows` view-model.
2636
+ * Composes the connected `MotionSample` as a child (that child reads the
2637
+ * project itself).
2638
+ */
2639
+ function MotionPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2342
2640
  const [speed, setSpeed] = useState(1);
2343
2641
  const [run, setRun] = useState(0);
2344
2642
  const reducedMotion = usePrefersReducedMotion();
2345
- const rows = useMemo(() => {
2346
- const collected = [];
2347
- for (const [path, token] of Object.entries(resolved)) {
2348
- if (filter && !matchPath(path, filter)) continue;
2349
- if (!filter && ![
2350
- "transition",
2351
- "duration",
2352
- "cubicBezier"
2353
- ].includes(token.$type ?? "")) continue;
2354
- const kind = token.$type;
2355
- if (!kind) continue;
2356
- const spec = resolveMotionSpec(token, resolved);
2357
- if (!spec) continue;
2358
- collected.push({
2359
- path,
2360
- cssVar: resolveCssVar(path, project),
2361
- durationMs: spec.durationMs,
2362
- easing: spec.easing,
2363
- kind
2364
- });
2365
- }
2366
- collected.sort((a, b) => {
2367
- if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
2368
- return a.path.localeCompare(b.path, void 0, { numeric: true });
2369
- });
2370
- return collected;
2371
- }, [
2372
- resolved,
2373
- filter,
2374
- project
2375
- ]);
2376
2643
  const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2377
2644
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2378
2645
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2438,51 +2705,56 @@ function MotionPreview({ filter, caption }) {
2438
2705
  ]
2439
2706
  });
2440
2707
  }
2708
+ function MotionPreview({ filter, caption }) {
2709
+ const project = useProject();
2710
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2711
+ return /* @__PURE__ */ jsx(MotionPreviewView, {
2712
+ rows: useMemo(() => deriveMotionRows(resolved, project, { filter }), [
2713
+ resolved,
2714
+ project,
2715
+ filter
2716
+ ]),
2717
+ activeTheme,
2718
+ cssVarPrefix,
2719
+ activeAxes,
2720
+ filter,
2721
+ caption
2722
+ });
2723
+ }
2441
2724
  //#endregion
2442
2725
  //#region src/OpacityScale.tsx
2443
2726
  function toOpacity(raw) {
2444
2727
  if (typeof raw === "number") return raw;
2445
2728
  return NaN;
2446
2729
  }
2447
- /**
2448
- * Render each opacity token as a colored sample at that opacity over a
2449
- * checkerboard backdrop, so the transparency is visually readable. The
2450
- * number by itself (`0.4`) doesn't convey what the token looks like
2451
- * applied to a surface; the sample does.
2452
- *
2453
- * Only tokens whose `$value` is a finite number between 0 and 1
2454
- * inclusive are rendered non-opacity `number` siblings (`line-height`,
2455
- * `z-index`) fall out naturally.
2456
- */
2457
- function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2458
- const project = useProject();
2459
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2460
- const rows = useMemo(() => {
2461
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2462
- if (token.$type !== type) return false;
2463
- const v = toOpacity(token.$value);
2464
- if (!Number.isFinite(v) || v < 0 || v > 1) return false;
2465
- return matchPath(path, filter);
2466
- }), {
2467
- by: sortBy,
2468
- dir: sortDir
2469
- }).map(([path, token]) => {
2470
- const opacity = toOpacity(token.$value);
2471
- return {
2472
- path,
2473
- cssVar: resolveCssVar(path, project),
2474
- opacity,
2475
- displayValue: String(opacity)
2476
- };
2477
- });
2478
- }, [
2479
- resolved,
2480
- filter,
2481
- type,
2482
- project,
2483
- sortBy,
2484
- sortDir
2485
- ]);
2730
+ /**
2731
+ * Pure derivation of the scale's display rows from resolved project data.
2732
+ * Only tokens whose `$value` is a finite number between 0 and 1 inclusive
2733
+ * are included non-opacity `number` siblings (`line-height`, `z-index`)
2734
+ * fall out naturally. Extracted so it is unit-testable without React or
2735
+ * a store.
2736
+ */
2737
+ function deriveOpacityRows(resolved, project, { filter, type, sortBy, sortDir }) {
2738
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2739
+ if (token.$type !== type) return false;
2740
+ const v = toOpacity(token.$value);
2741
+ if (!Number.isFinite(v) || v < 0 || v > 1) return false;
2742
+ return matchPath(path, filter);
2743
+ }), {
2744
+ by: sortBy,
2745
+ dir: sortDir
2746
+ }).map(([path, token]) => {
2747
+ const opacity = toOpacity(token.$value);
2748
+ return {
2749
+ path,
2750
+ cssVar: resolveCssVar(path, project),
2751
+ opacity,
2752
+ displayValue: String(opacity)
2753
+ };
2754
+ });
2755
+ }
2756
+ /** Pure presentation for the opacity scale. Renders from plain props. */
2757
+ function OpacityScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sampleColorVar, filter, caption }) {
2486
2758
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2487
2759
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2488
2760
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2491,7 +2763,6 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2491
2763
  children: "No opacity tokens match this filter."
2492
2764
  })
2493
2765
  });
2494
- const sampleColorVar = resolveCssVar(sampleColor, project);
2495
2766
  return /* @__PURE__ */ jsxs("div", {
2496
2767
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
2497
2768
  children: [/* @__PURE__ */ jsx("div", {
@@ -2529,6 +2800,37 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2529
2800
  })]
2530
2801
  });
2531
2802
  }
2803
+ /**
2804
+ * Render each opacity token as a colored sample at that opacity over a
2805
+ * checkerboard backdrop, so the transparency is visually readable. The
2806
+ * number by itself (`0.4`) doesn't convey what the token looks like
2807
+ * applied to a surface; the sample does.
2808
+ */
2809
+ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2810
+ const project = useProject();
2811
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2812
+ return /* @__PURE__ */ jsx(OpacityScaleView, {
2813
+ rows: useMemo(() => deriveOpacityRows(resolved, project, {
2814
+ filter,
2815
+ type,
2816
+ sortBy,
2817
+ sortDir
2818
+ }), [
2819
+ resolved,
2820
+ project,
2821
+ filter,
2822
+ type,
2823
+ sortBy,
2824
+ sortDir
2825
+ ]),
2826
+ activeTheme,
2827
+ cssVarPrefix,
2828
+ activeAxes,
2829
+ sampleColorVar: resolveCssVar(sampleColor, project),
2830
+ filter,
2831
+ caption
2832
+ });
2833
+ }
2532
2834
  //#endregion
2533
2835
  //#region src/provider.tsx
2534
2836
  /**
@@ -2558,46 +2860,67 @@ function useSwatchbookData() {
2558
2860
  }
2559
2861
  //#endregion
2560
2862
  //#region src/shadow-preview/ShadowSample.tsx
2561
- function ShadowSample({ path }) {
2863
+ /**
2864
+ * Pure derivation of a single shadow token's sample data from resolved
2865
+ * project data. Extracted so it is unit-testable without React or a store.
2866
+ */
2867
+ function deriveShadowSample(path, project) {
2868
+ return { cssVar: resolveCssVar(path, project) };
2869
+ }
2870
+ /** Pure presentation for a single shadow token's sample. Renders from plain props. */
2871
+ function ShadowSampleView({ cssVar }) {
2562
2872
  return /* @__PURE__ */ jsx("div", {
2563
2873
  className: "sb-shadow-sample",
2564
- style: { boxShadow: resolveCssVar(path, useProject()) },
2874
+ style: { boxShadow: cssVar },
2565
2875
  "aria-hidden": true
2566
2876
  });
2567
2877
  }
2878
+ function ShadowSample({ path }) {
2879
+ const { cssVar } = deriveShadowSample(path, useProject());
2880
+ return /* @__PURE__ */ jsx(ShadowSampleView, { cssVar });
2881
+ }
2568
2882
  //#endregion
2569
2883
  //#region src/ShadowPreview.tsx
2884
+ function layerKey(path, layer, fallback) {
2885
+ return `${path}|${layer.offset}|${layer.blur}|${layer.spread}|${fallback}`;
2886
+ }
2570
2887
  function asLayers(raw) {
2571
2888
  if (Array.isArray(raw)) return raw;
2572
2889
  if (raw && typeof raw === "object") return [raw];
2573
2890
  return [];
2574
2891
  }
2575
- function layerKey(path, layer, fallback) {
2576
- return `${path}|${`${formatDimension$1(layer.offsetX)},${formatDimension$1(layer.offsetY)}`}|${formatDimension$1(layer.blur)}|${formatDimension$1(layer.spread)}|${fallback}`;
2892
+ function formatLayer(layer, colorFormat) {
2893
+ return {
2894
+ offset: `${formatDimension$1(layer.offsetX)} ${formatDimension$1(layer.offsetY)}`,
2895
+ blur: formatDimension$1(layer.blur),
2896
+ spread: formatDimension$1(layer.spread),
2897
+ color: formatSubColor(layer.color, colorFormat),
2898
+ inset: layer.inset ? String(layer.inset) : void 0
2899
+ };
2577
2900
  }
2578
- function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2579
- const project = useProject();
2580
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2581
- const colorFormat = useColorFormat();
2582
- const rows = useMemo(() => {
2583
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2584
- if (token.$type !== "shadow") return false;
2585
- return matchPath(path, filter);
2586
- }), {
2587
- by: sortBy,
2588
- dir: sortDir
2589
- }).map(([path, token]) => ({
2590
- path,
2591
- cssVar: resolveCssVar(path, project),
2592
- layers: asLayers(token.$value)
2593
- }));
2594
- }, [
2595
- resolved,
2596
- filter,
2597
- project,
2598
- sortBy,
2599
- sortDir
2600
- ]);
2901
+ /**
2902
+ * Pure derivation of the preview's display rows from resolved project data.
2903
+ * Extracted so it is unit-testable without React or a store.
2904
+ */
2905
+ function deriveShadowRows(resolved, project, { filter, sortBy, sortDir, colorFormat }) {
2906
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2907
+ if (token.$type !== "shadow") return false;
2908
+ return matchPath(path, filter);
2909
+ }), {
2910
+ by: sortBy,
2911
+ dir: sortDir
2912
+ }).map(([path, token]) => ({
2913
+ path,
2914
+ cssVar: resolveCssVar(path, project),
2915
+ layers: asLayers(token.$value).map((layer) => formatLayer(layer, colorFormat))
2916
+ }));
2917
+ }
2918
+ /**
2919
+ * Pure presentation for the shadow preview. Renders from plain props;
2920
+ * composes the connected `ShadowSample` as a child (that child reads the
2921
+ * project itself).
2922
+ */
2923
+ function ShadowPreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2601
2924
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2602
2925
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2603
2926
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2630,32 +2953,31 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2630
2953
  }),
2631
2954
  /* @__PURE__ */ jsx("div", {
2632
2955
  className: "sb-shadow-preview__breakdown",
2633
- children: row.layers.length === 1 ? renderLayer(row.layers[0], colorFormat) : row.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, {
2956
+ children: row.layers.length === 1 ? renderLayerEntries(row.layers[0]) : row.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, {
2634
2957
  layer,
2635
2958
  index: i,
2636
- total: row.layers.length,
2637
- colorFormat
2959
+ total: row.layers.length
2638
2960
  }, layerKey(row.path, layer, i)))
2639
2961
  })
2640
2962
  ]
2641
2963
  }, row.path))]
2642
2964
  });
2643
2965
  }
2644
- function renderLayer(layer, format) {
2966
+ function renderLayerEntries(layer) {
2645
2967
  if (!layer) return [];
2646
2968
  const entries = [
2647
- ["offset", `${formatDimension$1(layer.offsetX)} ${formatDimension$1(layer.offsetY)}`],
2648
- ["blur", formatDimension$1(layer.blur)],
2649
- ["spread", formatDimension$1(layer.spread)],
2650
- ["color", formatSubColor(layer.color, format)]
2969
+ ["offset", layer.offset],
2970
+ ["blur", layer.blur],
2971
+ ["spread", layer.spread],
2972
+ ["color", layer.color]
2651
2973
  ];
2652
- if (layer.inset) entries.push(["inset", String(layer.inset)]);
2974
+ if (layer.inset) entries.push(["inset", layer.inset]);
2653
2975
  return entries.flatMap(([k, v]) => [/* @__PURE__ */ jsx("span", {
2654
2976
  className: "sb-shadow-preview__breakdown-key",
2655
2977
  children: k
2656
2978
  }, `k-${k}`), /* @__PURE__ */ jsx("span", { children: v }, `v-${k}`)]);
2657
2979
  }
2658
- function Layer({ layer, index, total, colorFormat }) {
2980
+ function Layer({ layer, index, total }) {
2659
2981
  return /* @__PURE__ */ jsxs("div", {
2660
2982
  className: "sb-shadow-preview__layer",
2661
2983
  children: [/* @__PURE__ */ jsxs("div", {
@@ -2668,10 +2990,35 @@ function Layer({ layer, index, total, colorFormat }) {
2668
2990
  ]
2669
2991
  }), /* @__PURE__ */ jsx("div", {
2670
2992
  className: cx("sb-shadow-preview__breakdown", "sb-shadow-preview__layer-breakdown"),
2671
- children: renderLayer(layer, colorFormat)
2993
+ children: renderLayerEntries(layer)
2672
2994
  })]
2673
2995
  });
2674
2996
  }
2997
+ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2998
+ const project = useProject();
2999
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
3000
+ const colorFormat = useColorFormat();
3001
+ return /* @__PURE__ */ jsx(ShadowPreviewView, {
3002
+ rows: useMemo(() => deriveShadowRows(resolved, project, {
3003
+ filter,
3004
+ sortBy,
3005
+ sortDir,
3006
+ colorFormat
3007
+ }), [
3008
+ resolved,
3009
+ project,
3010
+ filter,
3011
+ sortBy,
3012
+ sortDir,
3013
+ colorFormat
3014
+ ]),
3015
+ activeTheme,
3016
+ cssVarPrefix,
3017
+ activeAxes,
3018
+ filter,
3019
+ caption
3020
+ });
3021
+ }
2675
3022
  //#endregion
2676
3023
  //#region src/StrokeStylePreview.tsx
2677
3024
  const STRING_STYLES = new Set([
@@ -2688,29 +3035,26 @@ function extractCssStyle(value) {
2688
3035
  if (typeof value === "string" && STRING_STYLES.has(value)) return value;
2689
3036
  return null;
2690
3037
  }
2691
- function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2692
- const project = useProject();
2693
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2694
- const rows = useMemo(() => {
2695
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2696
- if (token.$type !== "strokeStyle") return false;
2697
- return matchPath(path, filter);
2698
- }), {
2699
- by: sortBy,
2700
- dir: sortDir
2701
- }).map(([path, token]) => ({
2702
- path,
2703
- cssVar: resolveCssVar(path, project),
2704
- displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
2705
- cssStyle: extractCssStyle(token.$value)
2706
- }));
2707
- }, [
2708
- resolved,
2709
- filter,
2710
- project,
2711
- sortBy,
2712
- sortDir
2713
- ]);
3038
+ /**
3039
+ * Pure derivation of the preview's display rows from resolved project data.
3040
+ * Extracted so it is unit-testable without React or a store.
3041
+ */
3042
+ function deriveStrokeStyleRows(resolved, project, { filter, sortBy, sortDir }) {
3043
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
3044
+ if (token.$type !== "strokeStyle") return false;
3045
+ return matchPath(path, filter);
3046
+ }), {
3047
+ by: sortBy,
3048
+ dir: sortDir
3049
+ }).map(([path, token]) => ({
3050
+ path,
3051
+ cssVar: resolveCssVar(path, project),
3052
+ displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
3053
+ cssStyle: extractCssStyle(token.$value)
3054
+ }));
3055
+ }
3056
+ /** Pure presentation for the stroke-style preview. Renders from plain props. */
3057
+ function StrokeStylePreviewView({ rows, activeTheme, cssVarPrefix, activeAxes, filter, caption }) {
2714
3058
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2715
3059
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2716
3060
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -2753,6 +3097,28 @@ function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc"
2753
3097
  }, row.path))]
2754
3098
  });
2755
3099
  }
3100
+ function StrokeStylePreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3101
+ const project = useProject();
3102
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
3103
+ return /* @__PURE__ */ jsx(StrokeStylePreviewView, {
3104
+ rows: useMemo(() => deriveStrokeStyleRows(resolved, project, {
3105
+ filter,
3106
+ sortBy,
3107
+ sortDir
3108
+ }), [
3109
+ resolved,
3110
+ project,
3111
+ filter,
3112
+ sortBy,
3113
+ sortDir
3114
+ ]),
3115
+ activeTheme,
3116
+ cssVarPrefix,
3117
+ activeAxes,
3118
+ filter,
3119
+ caption
3120
+ });
3121
+ }
2756
3122
  //#endregion
2757
3123
  //#region src/token-detail/internal.ts
2758
3124
  function useTokenDetailData(path) {
@@ -3495,13 +3861,22 @@ function TransitionSample({ transition, durationMs }) {
3495
3861
  }
3496
3862
  //#endregion
3497
3863
  //#region src/token-detail/ConsumerOutput.tsx
3498
- function ConsumerOutput({ path }) {
3499
- const { token, cssVar, activeAxes } = useTokenDetailData(path);
3500
- const { listing } = useProject();
3501
- if (!token) return null;
3502
- const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
3864
+ /**
3865
+ * Pure derivation of the consumer-output platform rows from the Token
3866
+ * Listing. Extracted so it is unit-testable without React or a store.
3867
+ */
3868
+ function deriveConsumerOutput(path, listing) {
3503
3869
  const names = listing[path]?.names ?? {};
3504
- const extraPlatforms = Object.keys(names).filter((platform) => platform !== "css" && names[platform]).toSorted();
3870
+ return { extraPlatforms: Object.keys(names).filter((platform) => platform !== "css" && names[platform]).toSorted().map((platform) => ({
3871
+ platform,
3872
+ label: formatPlatformLabel(platform),
3873
+ value: names[platform]
3874
+ })) };
3875
+ }
3876
+ /** Pure presentation for the consumer-output section. Renders from plain props; owns the copy-button's local feedback state. */
3877
+ function ConsumerOutputView({ path, cssVar, activeAxes, hasToken, extraPlatforms }) {
3878
+ if (!hasToken) return null;
3879
+ const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
3505
3880
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3506
3881
  /* @__PURE__ */ jsx("div", {
3507
3882
  className: "sb-token-detail__section-header",
@@ -3521,13 +3896,25 @@ function ConsumerOutput({ path }) {
3521
3896
  value: cssVar,
3522
3897
  testId: "consumer-output-css"
3523
3898
  }),
3524
- extraPlatforms.map((platform) => /* @__PURE__ */ jsx(OutputRow, {
3525
- label: formatPlatformLabel(platform),
3526
- value: names[platform],
3527
- testId: `consumer-output-${platform}`
3528
- }, platform))
3899
+ extraPlatforms.map((entry) => /* @__PURE__ */ jsx(OutputRow, {
3900
+ label: entry.label,
3901
+ value: entry.value,
3902
+ testId: `consumer-output-${entry.platform}`
3903
+ }, entry.platform))
3529
3904
  ] });
3530
3905
  }
3906
+ function ConsumerOutput({ path }) {
3907
+ const { token, cssVar, activeAxes } = useTokenDetailData(path);
3908
+ const { listing } = useProject();
3909
+ const { extraPlatforms } = deriveConsumerOutput(path, listing);
3910
+ return /* @__PURE__ */ jsx(ConsumerOutputView, {
3911
+ path,
3912
+ cssVar,
3913
+ activeAxes,
3914
+ hasToken: Boolean(token),
3915
+ extraPlatforms
3916
+ });
3917
+ }
3531
3918
  function formatPlatformLabel(platform) {
3532
3919
  if (platform.length === 0) return platform;
3533
3920
  return platform[0].toUpperCase() + platform.slice(1);
@@ -3648,10 +4035,43 @@ function TokenUsageSnippet({ path }) {
3648
4035
  }
3649
4036
  //#endregion
3650
4037
  //#region src/TokenDetail.tsx
3651
- function TokenDetail({ path, heading }) {
3652
- const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
3653
- const colorFormat = useColorFormat();
3654
- const { listing } = useProject();
4038
+ const EMPTY_DERIVED = {
4039
+ value: "",
4040
+ outOfGamut: false,
4041
+ isColor: false,
4042
+ isDeprecated: false,
4043
+ deprecationMessage: void 0
4044
+ };
4045
+ /**
4046
+ * Pure derivation of the token's display value and status flags. Mirrors
4047
+ * `formatTokenValue` + `formatColor`'s gamut check, both of which need the
4048
+ * resolved token's raw `$value`/`$type` — the listing alone (no `$value`)
4049
+ * can't drive this. Extracted so it is unit-testable without React or a
4050
+ * store; the View renders purely from this shape plus the token's presence.
4051
+ */
4052
+ function deriveTokenDetail(path, token, listing, colorFormat) {
4053
+ if (!token) return EMPTY_DERIVED;
4054
+ const isColor = token.$type === "color";
4055
+ const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
4056
+ const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
4057
+ const outOfGamut = gamut?.outOfGamut ?? false;
4058
+ const dep = token.$deprecated;
4059
+ return {
4060
+ value,
4061
+ outOfGamut,
4062
+ isColor,
4063
+ isDeprecated: dep === true || typeof dep === "string" && dep.length > 0,
4064
+ deprecationMessage: typeof dep === "string" ? dep : void 0
4065
+ };
4066
+ }
4067
+ /**
4068
+ * Pure presentation for the token detail panel. Renders from plain props;
4069
+ * composes the connected `TokenHeader` / `CompositePreview` /
4070
+ * `CompositeBreakdown` / `AliasChain` / `AliasedBy` / `TokenUsageSnippet` /
4071
+ * `ConsumerOutput` / `AxisVariance` as children (each reads the project
4072
+ * itself for `path`).
4073
+ */
4074
+ function TokenDetailView({ path, heading, token, cssVar, activeTheme, activeAxes, cssVarPrefix, value, outOfGamut, isColor, isDeprecated, deprecationMessage }) {
3655
4075
  const wrapperAttrs = blockWrapperAttrs(cssVarPrefix, activeAxes);
3656
4076
  if (!token) return /* @__PURE__ */ jsx("div", {
3657
4077
  ...wrapperAttrs,
@@ -3667,12 +4087,6 @@ function TokenDetail({ path, heading }) {
3667
4087
  ]
3668
4088
  })
3669
4089
  });
3670
- const isColor = token.$type === "color";
3671
- const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
3672
- const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
3673
- const outOfGamut = gamut?.outOfGamut ?? false;
3674
- const dep = token.$deprecated;
3675
- const isDeprecated = dep === true || typeof dep === "string" && dep.length > 0;
3676
4090
  return /* @__PURE__ */ jsxs("div", {
3677
4091
  ...wrapperAttrs,
3678
4092
  className: cx(wrapperAttrs["className"], "sb-token-detail"),
@@ -3691,7 +4105,7 @@ function TokenDetail({ path, heading }) {
3691
4105
  children: "⚠ "
3692
4106
  }),
3693
4107
  "Deprecated",
3694
- typeof dep === "string" ? `: ${dep}` : ""
4108
+ deprecationMessage ? `: ${deprecationMessage}` : ""
3695
4109
  ]
3696
4110
  }),
3697
4111
  /* @__PURE__ */ jsxs("div", {
@@ -3729,6 +4143,22 @@ function TokenDetail({ path, heading }) {
3729
4143
  ]
3730
4144
  });
3731
4145
  }
4146
+ function TokenDetail({ path, heading }) {
4147
+ const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
4148
+ const colorFormat = useColorFormat();
4149
+ const { listing } = useProject();
4150
+ const derived = deriveTokenDetail(path, token, listing, colorFormat);
4151
+ return /* @__PURE__ */ jsx(TokenDetailView, {
4152
+ path,
4153
+ ...heading !== void 0 && { heading },
4154
+ token,
4155
+ cssVar,
4156
+ activeTheme,
4157
+ activeAxes,
4158
+ cssVarPrefix,
4159
+ ...derived
4160
+ });
4161
+ }
3732
4162
  //#endregion
3733
4163
  //#region src/internal/DetailOverlay.tsx
3734
4164
  const FOCUSABLE_SELECTOR = [
@@ -3857,6 +4287,13 @@ function isInView(path, ctx) {
3857
4287
  }
3858
4288
  //#endregion
3859
4289
  //#region src/TokenNavigator.tsx
4290
+ /**
4291
+ * Pure derivation of the navigator's tree from the resolved token map:
4292
+ * groups paths by dotted segment (scoped to `root` when set, restricted to
4293
+ * `typeFilter`'s `$type`s when set), producing nested `GroupNode`s down to
4294
+ * `LeafNode`s, sorted groups-before-leaves and alphanumeric within a level.
4295
+ * Extracted so it is unit-testable without React or a store.
4296
+ */
3860
4297
  function buildTree(resolved, root, typeFilter) {
3861
4298
  const rootPrefix = root && root.length > 0 ? `${root}.` : "";
3862
4299
  const rootSegments = root ? root.split(".") : [];
@@ -3933,6 +4370,12 @@ function collectLeafPaths(nodes, out) {
3933
4370
  for (const node of nodes) if (node.kind === "leaf") out.push(node.path);
3934
4371
  else collectLeafPaths(node.children, out);
3935
4372
  }
4373
+ /**
4374
+ * Pure derivation of the flattened, currently-visible treeitem order from a
4375
+ * tree plus the set of expanded group paths — depth-first, only descending
4376
+ * into expanded groups. Appends to `out` (caller passes `[]`). Extracted so
4377
+ * it is unit-testable without React or a store.
4378
+ */
3936
4379
  function flattenVisible(nodes, expanded, parentPath, out) {
3937
4380
  for (const node of nodes) {
3938
4381
  out.push({
@@ -3959,18 +4402,18 @@ function pruneTreeForMatches(nodes, matches, expandOut) {
3959
4402
  }
3960
4403
  return out;
3961
4404
  }
3962
- function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect, id, indicators }) {
3963
- const { resolved, activeTheme, activeAxes, cssVarPrefix, indicators: indicatorBaseline } = useProject();
3964
- const blockKey = useBlockKey("TokenNavigator", [
3965
- root,
3966
- type === void 0 ? "" : typeof type === "string" ? type : type.join(","),
3967
- id
3968
- ]);
4405
+ /**
4406
+ * Pure presentation for the token navigator. Owns its own expand/collapse,
4407
+ * selection, search, and roving-tabindex keyboard-nav state; renders the
4408
+ * tree from the `resolved` view-model. Composes the connected `RowIndicators`
4409
+ * / `DetailOverlay` / sample previews as children (those read the project
4410
+ * themselves).
4411
+ */
4412
+ function TokenNavigatorView({ resolved, root, enabledIndicators, cssVarPrefix, activeAxes, activeTheme, blockKey, type, initiallyExpanded = 1, searchable = true, onSelect }) {
3969
4413
  const typeFilter = useMemo(() => {
3970
4414
  if (type === void 0) return void 0;
3971
4415
  return new Set(Array.isArray(type) ? type : [type]);
3972
4416
  }, [type]);
3973
- const enabledIndicators = useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]);
3974
4417
  const tree = useMemo(() => buildTree(resolved, root, typeFilter), [
3975
4418
  resolved,
3976
4419
  root,
@@ -4259,6 +4702,32 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4259
4702
  ]
4260
4703
  });
4261
4704
  }
4705
+ /**
4706
+ * A collapsible, searchable tree of tokens with roving-tabindex keyboard
4707
+ * navigation. Click a leaf to inspect it in a slide-over (unless `onSelect`
4708
+ * is provided, which hands the follow-up to the consumer).
4709
+ */
4710
+ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect, id, indicators }) {
4711
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, indicators: indicatorBaseline } = useProject();
4712
+ const blockKey = useBlockKey("TokenNavigator", [
4713
+ root,
4714
+ type === void 0 ? "" : typeof type === "string" ? type : type.join(","),
4715
+ id
4716
+ ]);
4717
+ return /* @__PURE__ */ jsx(TokenNavigatorView, {
4718
+ resolved,
4719
+ root,
4720
+ enabledIndicators: useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]),
4721
+ cssVarPrefix,
4722
+ activeAxes,
4723
+ activeTheme,
4724
+ blockKey,
4725
+ type,
4726
+ initiallyExpanded,
4727
+ searchable,
4728
+ onSelect
4729
+ });
4730
+ }
4262
4731
  function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, root, resolveInView, onNavigate, enabled, level, setsize, posinset }) {
4263
4732
  if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
4264
4733
  node,
@@ -4447,56 +4916,51 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
4447
4916
  });
4448
4917
  //#endregion
4449
4918
  //#region src/TokenTable.tsx
4450
- function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, id, indicators }) {
4451
- const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath, indicators: indicatorBaseline } = useProject();
4452
- const colorFormat = useColorFormat();
4453
- const rootFontSize = useRootFontSize();
4454
- const blockKey = useBlockKey("TokenTable", [
4455
- filter,
4456
- type,
4457
- caption,
4458
- id
4459
- ]);
4460
- const enabledIndicators = useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]);
4919
+ /**
4920
+ * Pure derivation of the table's display rows from resolved project data.
4921
+ * Carries per-row `isDeprecated` / `token` / `variance` so the View renders
4922
+ * the indicator strip and deprecation state without reaching back into the
4923
+ * project. Extracted so it is unit-testable without React or a store.
4924
+ */
4925
+ function deriveTokenRows(resolved, listing, cssVarPrefix, varianceByPath, { filter, type, sortBy, sortDir, colorFormat, rootFontSizePx }) {
4926
+ const projectFields = {
4927
+ listing,
4928
+ cssVarPrefix
4929
+ };
4930
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4931
+ if (!matchPath(path, filter)) return false;
4932
+ if (type && token.$type !== type) return false;
4933
+ return true;
4934
+ }), {
4935
+ by: sortBy,
4936
+ dir: sortDir,
4937
+ rootFontSizePx
4938
+ }).map(([path, token]) => {
4939
+ const isColor = token.$type === "color";
4940
+ const color = isColor ? resolveColorValue(path, token.$value, colorFormat, projectFields) : null;
4941
+ const dep = token.$deprecated;
4942
+ return {
4943
+ path,
4944
+ type: token.$type ?? "",
4945
+ value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4946
+ outOfGamut: color?.outOfGamut ?? false,
4947
+ cssVar: resolveCssVar(path, projectFields),
4948
+ isColor,
4949
+ isDeprecated: dep === true || typeof dep === "string" && dep.length > 0,
4950
+ token,
4951
+ variance: varianceByPath[path]
4952
+ };
4953
+ });
4954
+ }
4955
+ /**
4956
+ * Pure presentation for the token table. Owns its own search + selection UI
4957
+ * state; renders from the derived `rows` view-model. Composes the connected
4958
+ * DetailOverlay as a child (that child reads the project itself).
4959
+ */
4960
+ function TokenTableView({ rows, activeTheme, cssVarPrefix, activeAxes, colorFormat, enabledIndicators, validPaths, blockKey, filter, type, caption, searchable = true, onSelect }) {
4461
4961
  const [selectedPath, setSelectedPath] = usePersistedState(`${blockKey}::selected`, null);
4462
4962
  const [query, setQuery] = usePersistedState(`${blockKey}::query`, "");
4463
4963
  const deferredQuery = useDeferredValue(query);
4464
- const rows = useMemo(() => {
4465
- const projectFields = {
4466
- listing,
4467
- cssVarPrefix
4468
- };
4469
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4470
- if (!matchPath(path, filter)) return false;
4471
- if (type && token.$type !== type) return false;
4472
- return true;
4473
- }), {
4474
- by: sortBy,
4475
- dir: sortDir,
4476
- rootFontSizePx: rootFontSize
4477
- }).map(([path, token]) => {
4478
- const isColor = token.$type === "color";
4479
- const color = isColor ? resolveColorValue(path, token.$value, colorFormat, projectFields) : null;
4480
- return {
4481
- path,
4482
- type: token.$type ?? "",
4483
- value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4484
- outOfGamut: color?.outOfGamut ?? false,
4485
- cssVar: resolveCssVar(path, projectFields),
4486
- isColor
4487
- };
4488
- });
4489
- }, [
4490
- resolved,
4491
- listing,
4492
- cssVarPrefix,
4493
- filter,
4494
- type,
4495
- colorFormat,
4496
- sortBy,
4497
- sortDir,
4498
- rootFontSize
4499
- ]);
4500
4964
  const visibleRows = useMemo(() => {
4501
4965
  if (!searchable || deferredQuery.trim() === "") return rows;
4502
4966
  return fuzzyFilter(rows, deferredQuery, (row) => `${row.path} ${row.type} ${row.value}`);
@@ -4572,9 +5036,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4572
5036
  "\"."
4573
5037
  ]
4574
5038
  }) }), visibleRows.map((row) => {
4575
- const token = resolved[row.path];
4576
- const dep = token?.$deprecated;
4577
- const isDeprecated = dep === true || typeof dep === "string" && dep.length > 0;
5039
+ const token = row.token;
4578
5040
  return /* @__PURE__ */ jsxs("tr", {
4579
5041
  className: "sb-token-table__row",
4580
5042
  onClick: () => handleRowClick(row.path),
@@ -4592,7 +5054,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4592
5054
  children: [
4593
5055
  /* @__PURE__ */ jsx("td", {
4594
5056
  className: cx("sb-token-table__td", "sb-token-table__path"),
4595
- "data-deprecated": enabledIndicators.deprecation && isDeprecated ? "true" : void 0,
5057
+ "data-deprecated": enabledIndicators.deprecation && row.isDeprecated ? "true" : void 0,
4596
5058
  children: row.path
4597
5059
  }),
4598
5060
  /* @__PURE__ */ jsx("td", {
@@ -4641,9 +5103,9 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4641
5103
  path: row.path,
4642
5104
  token,
4643
5105
  root: void 0,
4644
- variance: varianceByPath[row.path],
5106
+ variance: row.variance,
4645
5107
  colorFormat,
4646
- canReference: (p) => p in resolved,
5108
+ canReference: (p) => validPaths.has(p),
4647
5109
  onReferenceClick: (p) => setSelectedPath(p),
4648
5110
  enabled: enabledIndicators
4649
5111
  })
@@ -4661,6 +5123,56 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4661
5123
  ]
4662
5124
  });
4663
5125
  }
5126
+ /**
5127
+ * A sortable, searchable table of tokens. Click a row to inspect it in a
5128
+ * slide-over (unless `onSelect` is provided, which hands the follow-up to the
5129
+ * consumer).
5130
+ */
5131
+ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, id, indicators }) {
5132
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing, varianceByPath, indicators: indicatorBaseline } = useProject();
5133
+ const colorFormat = useColorFormat();
5134
+ const rootFontSize = useRootFontSize();
5135
+ const blockKey = useBlockKey("TokenTable", [
5136
+ filter,
5137
+ type,
5138
+ caption,
5139
+ id
5140
+ ]);
5141
+ const enabledIndicators = useMemo(() => resolveIndicators(indicators, indicatorBaseline), [indicators, indicatorBaseline]);
5142
+ return /* @__PURE__ */ jsx(TokenTableView, {
5143
+ rows: useMemo(() => deriveTokenRows(resolved, listing, cssVarPrefix, varianceByPath, {
5144
+ filter,
5145
+ type,
5146
+ sortBy,
5147
+ sortDir,
5148
+ colorFormat,
5149
+ rootFontSizePx: rootFontSize
5150
+ }), [
5151
+ resolved,
5152
+ listing,
5153
+ cssVarPrefix,
5154
+ varianceByPath,
5155
+ filter,
5156
+ type,
5157
+ sortBy,
5158
+ sortDir,
5159
+ colorFormat,
5160
+ rootFontSize
5161
+ ]),
5162
+ activeTheme,
5163
+ cssVarPrefix,
5164
+ activeAxes,
5165
+ colorFormat,
5166
+ enabledIndicators,
5167
+ validPaths: useMemo(() => new Set(Object.keys(resolved)), [resolved]),
5168
+ blockKey,
5169
+ filter,
5170
+ type,
5171
+ caption,
5172
+ searchable,
5173
+ onSelect
5174
+ });
5175
+ }
4664
5176
  //#endregion
4665
5177
  //#region src/TypographyScale.tsx
4666
5178
  function asDimension(raw) {
@@ -4697,30 +5209,29 @@ function buildRow(path, composite) {
4697
5209
  ].filter(Boolean).join(" · ")
4698
5210
  };
4699
5211
  }
4700
- function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
4701
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
4702
- const rows = useMemo(() => {
4703
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4704
- if (token.$type !== "typography") return false;
4705
- return matchPath(path, filter);
4706
- }), {
4707
- by: sortBy,
4708
- dir: sortDir
4709
- }).map(([path, token]) => {
4710
- const value = token.$value;
4711
- if (!value || typeof value !== "object") return {
4712
- path,
4713
- sampleStyle: {},
4714
- specs: ""
4715
- };
4716
- return buildRow(path, value);
4717
- });
4718
- }, [
4719
- resolved,
4720
- filter,
4721
- sortBy,
4722
- sortDir
4723
- ]);
5212
+ /**
5213
+ * Pure derivation of the scale's display rows from resolved project data.
5214
+ * Extracted so it is unit-testable without React or a store.
5215
+ */
5216
+ function deriveTypographyRows(resolved, { filter, sortBy, sortDir }) {
5217
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
5218
+ if (token.$type !== "typography") return false;
5219
+ return matchPath(path, filter);
5220
+ }), {
5221
+ by: sortBy,
5222
+ dir: sortDir
5223
+ }).map(([path, token]) => {
5224
+ const value = token.$value;
5225
+ if (!value || typeof value !== "object") return {
5226
+ path,
5227
+ sampleStyle: {},
5228
+ specs: ""
5229
+ };
5230
+ return buildRow(path, value);
5231
+ });
5232
+ }
5233
+ /** Pure presentation for the typography scale. Renders from plain props. */
5234
+ function TypographyScaleView({ rows, activeTheme, cssVarPrefix, activeAxes, sample, filter, caption }) {
4724
5235
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
4725
5236
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4726
5237
  ...blockWrapperAttrs(cssVarPrefix, activeAxes),
@@ -4752,7 +5263,28 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
4752
5263
  }, row.path))]
4753
5264
  });
4754
5265
  }
5266
+ function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
5267
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
5268
+ return /* @__PURE__ */ jsx(TypographyScaleView, {
5269
+ rows: useMemo(() => deriveTypographyRows(resolved, {
5270
+ filter,
5271
+ sortBy,
5272
+ sortDir
5273
+ }), [
5274
+ resolved,
5275
+ filter,
5276
+ sortBy,
5277
+ sortDir
5278
+ ]),
5279
+ activeTheme,
5280
+ cssVarPrefix,
5281
+ activeAxes,
5282
+ sample,
5283
+ filter,
5284
+ caption
5285
+ });
5286
+ }
4755
5287
  //#endregion
4756
- export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilyPreview, FontWeightScale, GradientPalette, MotionPreview, MotionSample, OpacityScale, ShadowPreview, ShadowSample, StrokeStylePreview, SwatchbookContext, SwatchbookProvider, TOKENS_UPDATED_EVENT, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, registerTokenSource, useActiveAxes, useActiveTheme, useChannelGlobals, useColorFormat, useOptionalSwatchbookData, useSwatchbookData, useTokenSnapshot };
5288
+ export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilyPreview, FontWeightScale, GradientPalette, MotionPreview, MotionSample, OpacityScale, ShadowPreview, ShadowSample, StrokeStylePreview, SwatchbookContext, SwatchbookProvider, TOKENS_UPDATED_EVENT, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, onChannel, registerChannel, registerTokenSource, useActiveAxes, useActiveTheme, useChannelGlobals, useColorFormat, useOptionalSwatchbookData, useSwatchbookData, useTokenSnapshot };
4757
5289
 
4758
5290
  //# sourceMappingURL=index.mjs.map