@unpunnyfuns/swatchbook-blocks 0.57.1 → 0.58.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
@@ -1,6 +1,6 @@
1
1
  import './style.css';
2
2
  import Color from "colorjs.io";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
6
6
  import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
@@ -466,8 +466,7 @@ function useProject() {
466
466
  cssVarPrefix: cssVarPrefix ?? "",
467
467
  listing: listing ?? {},
468
468
  varianceByPath: varianceByPath ?? {},
469
- resolveAt,
470
- themeNameForTuple: (tuple) => tupleToName(axes, tuple)
469
+ resolveAt
471
470
  };
472
471
  }, [
473
472
  snapshot,
@@ -530,8 +529,7 @@ function useVirtualModuleFallback(enabled) {
530
529
  cssVarPrefix: tokens.cssVarPrefix,
531
530
  listing: tokens.listing,
532
531
  varianceByPath: tokens.varianceByPath,
533
- resolveAt,
534
- themeNameForTuple: (tuple) => tupleToName(tokens.axes, tuple)
532
+ resolveAt
535
533
  }), [
536
534
  activeTheme,
537
535
  activeAxes,
@@ -615,22 +613,38 @@ const BLOCK_ATTR = "data-swatchbook-block";
615
613
  const WRAPPER_CLASSES = "sb-unstyled sb-block";
616
614
  /**
617
615
  * Spread helper for the common block wrapper. Returns:
618
- * - `data-<prefix>-theme="<composed theme name>"` so theme-keyed CSS
619
- * emitted by `@unpunnyfuns/swatchbook-core` resolves against this
620
- * subtree.
616
+ * - One `data-<prefix>-<axisName>="<contextName>"` per axis in the tuple.
617
+ * These are what the smart CSS emitter actually targets — single-axis
618
+ * cell selectors (`[data-<prefix>-mode="Dark"]`) and joint compounds
619
+ * (`[data-<prefix>-mode="Dark"][data-<prefix>-brand="Brand A"]`).
620
+ * Wrapping a block subtree in these attrs lets the cascade resolve
621
+ * per-tuple values inside the block independently of the document
622
+ * root — `AxisVariance`'s grid uses this to render real per-cell
623
+ * swatches.
621
624
  * - `data-swatchbook-block` — stable consumer hook for targeting block
622
625
  * chrome from outside.
623
626
  * - `className="sb-unstyled sb-block"` — Storybook's opt-out class so
624
627
  * MDX docs house styles self-exclude the subtree, plus `sb-block`
625
628
  * which carries the shared chrome from `internal/styles.css`.
626
629
  */
627
- function themeAttrs(prefix, themeName) {
630
+ function themeAttrs(prefix, tuple) {
628
631
  return {
629
- [dataAttr(prefix, "theme")]: themeName,
632
+ ...perAxisAttrs(prefix, tuple),
630
633
  [BLOCK_ATTR]: "",
631
634
  className: WRAPPER_CLASSES
632
635
  };
633
636
  }
637
+ /**
638
+ * Spread helper for any element that wants per-axis cell semantics
639
+ * without the block-wrapper chrome — `AxisVariance`'s grid uses this
640
+ * on each swatch so the swatch's CSS vars resolve at the cell's own
641
+ * tuple, not the document root's active tuple.
642
+ */
643
+ function perAxisAttrs(prefix, tuple) {
644
+ const out = {};
645
+ for (const [axisName, contextName] of Object.entries(tuple)) out[dataAttr(prefix, axisName)] = contextName;
646
+ return out;
647
+ }
634
648
  //#endregion
635
649
  //#region src/internal/sort-tokens.ts
636
650
  const NUMERIC_TYPES = new Set([
@@ -775,7 +789,7 @@ function formatSubColor$1(raw, format) {
775
789
  }
776
790
  function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
777
791
  const project = useProject();
778
- const { resolved, activeTheme, cssVarPrefix } = project;
792
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
779
793
  const colorFormat = useColorFormat();
780
794
  const rows = useMemo(() => {
781
795
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -798,14 +812,14 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
798
812
  ]);
799
813
  const captionText = caption ?? `${rows.length} border${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
800
814
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
801
- ...themeAttrs(cssVarPrefix, activeTheme),
815
+ ...themeAttrs(cssVarPrefix, activeAxes),
802
816
  children: /* @__PURE__ */ jsx("div", {
803
817
  className: "sb-block__empty",
804
818
  children: "No border tokens match this filter."
805
819
  })
806
820
  });
807
821
  return /* @__PURE__ */ jsxs("div", {
808
- ...themeAttrs(cssVarPrefix, activeTheme),
822
+ ...themeAttrs(cssVarPrefix, activeAxes),
809
823
  children: [/* @__PURE__ */ jsx("div", {
810
824
  className: "sb-block__caption",
811
825
  children: captionText
@@ -867,10 +881,13 @@ function fixedPrefixLength(filter) {
867
881
  return fixed;
868
882
  }
869
883
  function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
870
- const project = useProject();
871
- const { resolved, activeTheme, cssVarPrefix } = project;
884
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
872
885
  const colorFormat = useColorFormat();
873
886
  const groups = useMemo(() => {
887
+ const projectFields = {
888
+ listing,
889
+ cssVarPrefix
890
+ };
874
891
  const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
875
892
  if (token.$type !== "color") return false;
876
893
  return matchPath(path, filter);
@@ -886,11 +903,11 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
886
903
  const groupKey = segments.slice(0, effectiveGroupBy).join(".");
887
904
  const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
888
905
  const list = bucket.get(groupKey) ?? [];
889
- const formatted = resolveColorValue(path, token.$value, colorFormat, project);
906
+ const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
890
907
  list.push({
891
908
  path,
892
909
  leaf,
893
- cssVar: resolveCssVar(path, project),
910
+ cssVar: resolveCssVar(path, projectFields),
894
911
  value: formatted.value,
895
912
  outOfGamut: formatted.outOfGamut
896
913
  });
@@ -899,9 +916,10 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
899
916
  return [...bucket.entries()].toSorted(([a], [b]) => a.localeCompare(b, void 0, { numeric: true }));
900
917
  }, [
901
918
  resolved,
919
+ listing,
920
+ cssVarPrefix,
902
921
  filter,
903
922
  groupBy,
904
- project,
905
923
  colorFormat,
906
924
  sortBy,
907
925
  sortDir
@@ -909,14 +927,14 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
909
927
  const totalCount = groups.reduce((acc, [, swatches]) => acc + swatches.length, 0);
910
928
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
911
929
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
912
- ...themeAttrs(cssVarPrefix, activeTheme),
930
+ ...themeAttrs(cssVarPrefix, activeAxes),
913
931
  children: /* @__PURE__ */ jsx("div", {
914
932
  className: "sb-block__empty",
915
933
  children: "No color tokens match this filter."
916
934
  })
917
935
  });
918
936
  return /* @__PURE__ */ jsxs("div", {
919
- ...themeAttrs(cssVarPrefix, activeTheme),
937
+ ...themeAttrs(cssVarPrefix, activeAxes),
920
938
  children: [/* @__PURE__ */ jsx("div", {
921
939
  className: "sb-block__caption",
922
940
  children: captionText
@@ -980,7 +998,7 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
980
998
  const classes = ["sb-copy-button", `sb-copy-button--${variant}`];
981
999
  if (copied) classes.push("sb-copy-button--copied");
982
1000
  if (className) classes.push(className);
983
- return /* @__PURE__ */ jsx("button", {
1001
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
984
1002
  type: "button",
985
1003
  className: classes.join(" "),
986
1004
  onClick: handleClick,
@@ -995,21 +1013,30 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
995
1013
  "aria-hidden": true,
996
1014
  children: copied ? "✓" : "⧉"
997
1015
  })
998
- });
1016
+ }), /* @__PURE__ */ jsx("span", {
1017
+ role: "status",
1018
+ "aria-live": "polite",
1019
+ className: "sb-copy-button__sr-status",
1020
+ children: copied ? "Copied" : ""
1021
+ })] });
999
1022
  }
1000
1023
  //#endregion
1001
1024
  //#region src/ColorTable.tsx
1002
1025
  const BASE_LABEL = "base";
1003
1026
  const COLUMN_COUNT = 6;
1004
1027
  function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
1005
- const project = useProject();
1006
- const { resolved, activeTheme, cssVarPrefix } = project;
1028
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
1007
1029
  const colorFormat = useColorFormat();
1008
1030
  const [query, setQuery] = useState("");
1031
+ const deferredQuery = useDeferredValue(query);
1009
1032
  const [selectedByBase, setSelectedByBase] = useState({});
1010
1033
  const [expandedByBase, setExpandedByBase] = useState(() => /* @__PURE__ */ new Set());
1011
1034
  const defs = useMemo(() => buildVariantDefs(variants), [variants]);
1012
1035
  const groups = useMemo(() => {
1036
+ const projectFields = {
1037
+ listing,
1038
+ cssVarPrefix
1039
+ };
1013
1040
  const sorted = sortTokens(Object.entries(resolved).filter(([path, token]) => {
1014
1041
  if (token.$type !== "color") return false;
1015
1042
  return matchPath(path, filter);
@@ -1020,7 +1047,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1020
1047
  const groupMap = /* @__PURE__ */ new Map();
1021
1048
  for (const [path, token] of sorted) {
1022
1049
  const raw = token.$value;
1023
- const hex = resolveColorValue(path, raw, "hex", project);
1050
+ const hex = resolveColorValue(path, raw, "hex", projectFields);
1024
1051
  const hsl = formatColor(raw, "hsl");
1025
1052
  const oklch = formatColor(raw, "oklch");
1026
1053
  const active = pickActiveFormat(raw, colorFormat, hex, hsl, oklch);
@@ -1028,7 +1055,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1028
1055
  const variant = {
1029
1056
  label: match?.label ?? BASE_LABEL,
1030
1057
  path,
1031
- cssVar: resolveCssVar(path, project),
1058
+ cssVar: resolveCssVar(path, projectFields),
1032
1059
  value: active.value,
1033
1060
  outOfGamut: active.outOfGamut,
1034
1061
  hex: hex.value,
@@ -1059,19 +1086,20 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1059
1086
  return out;
1060
1087
  }, [
1061
1088
  resolved,
1089
+ listing,
1090
+ cssVarPrefix,
1062
1091
  filter,
1063
- project,
1064
1092
  sortBy,
1065
1093
  sortDir,
1066
1094
  defs,
1067
1095
  colorFormat
1068
1096
  ]);
1069
1097
  const visibleGroups = useMemo(() => {
1070
- if (!searchable || query.trim() === "") return groups;
1071
- return fuzzyFilter(groups, query, (g) => g.searchText);
1098
+ if (!searchable || deferredQuery.trim() === "") return groups;
1099
+ return fuzzyFilter(groups, deferredQuery, (g) => g.searchText);
1072
1100
  }, [
1073
1101
  groups,
1074
- query,
1102
+ deferredQuery,
1075
1103
  searchable
1076
1104
  ]);
1077
1105
  const totalTokens = useMemo(() => groups.reduce((n, g) => n + g.variants.length, 0), [groups]);
@@ -1092,14 +1120,14 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1092
1120
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleGroups.length} matching "${query.trim()}"` : "";
1093
1121
  const captionText = caption ?? `${totalTokens} color${totalTokens === 1 ? "" : "s"} across ${groups.length} group${groups.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
1094
1122
  if (groups.length === 0) return /* @__PURE__ */ jsx("div", {
1095
- ...themeAttrs(cssVarPrefix, activeTheme),
1123
+ ...themeAttrs(cssVarPrefix, activeAxes),
1096
1124
  children: /* @__PURE__ */ jsx("div", {
1097
1125
  className: "sb-block__empty",
1098
1126
  children: "No color tokens match this filter."
1099
1127
  })
1100
1128
  });
1101
1129
  return /* @__PURE__ */ jsxs("div", {
1102
- ...themeAttrs(cssVarPrefix, activeTheme),
1130
+ ...themeAttrs(cssVarPrefix, activeAxes),
1103
1131
  children: [searchable && /* @__PURE__ */ jsx("div", {
1104
1132
  className: "sb-color-table__search",
1105
1133
  children: /* @__PURE__ */ jsx("input", {
@@ -1173,7 +1201,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1173
1201
  })]
1174
1202
  });
1175
1203
  }
1176
- function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect }) {
1204
+ const GroupRow = memo(function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect }) {
1177
1205
  const multi = group.variants.length > 1;
1178
1206
  const active = group.variants.find((v) => v.label === selectedLabel) ?? group.variants[0];
1179
1207
  const nameText = multi ? group.base : active.path;
@@ -1192,6 +1220,7 @@ function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVari
1192
1220
  },
1193
1221
  tabIndex: 0,
1194
1222
  "aria-label": onSelect ? `Inspect ${active.path}` : expanded ? `Collapse ${group.base}` : `Expand ${group.base}`,
1223
+ ...onSelect ? { "aria-haspopup": "dialog" } : {},
1195
1224
  "data-testid": "color-table-row",
1196
1225
  "data-path": active.path,
1197
1226
  "data-base": group.base,
@@ -1271,7 +1300,7 @@ function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVari
1271
1300
  })
1272
1301
  })
1273
1302
  })] });
1274
- }
1303
+ });
1275
1304
  function ExpandedDetail({ group, active }) {
1276
1305
  const hasDescription = active.description !== void 0 && active.description.length > 0;
1277
1306
  const chain = active.aliasChain && active.aliasChain.length > 0 ? active.aliasChain : void 0;
@@ -1487,29 +1516,32 @@ const severityLabel = {
1487
1516
  warn: "WARN",
1488
1517
  info: "INFO"
1489
1518
  };
1490
- function summaryText(diagnostics) {
1491
- if (diagnostics.length === 0) return "✔ OK · no diagnostics";
1492
- const counts = {
1493
- error: 0,
1494
- warn: 0,
1495
- info: 0
1519
+ function summarize(diagnostics) {
1520
+ if (diagnostics.length === 0) return {
1521
+ text: "✔ OK · no diagnostics",
1522
+ variant: "ok",
1523
+ hasErrorsOrWarnings: false
1496
1524
  };
1497
- for (const d of diagnostics) counts[d.severity] += 1;
1525
+ let errors = 0;
1526
+ let warnings = 0;
1527
+ let infos = 0;
1528
+ for (const d of diagnostics) if (d.severity === "error") errors += 1;
1529
+ else if (d.severity === "warn") warnings += 1;
1530
+ else infos += 1;
1498
1531
  const parts = [];
1499
- if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
1500
- if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
1501
- if (counts.info > 0) parts.push(`${counts.info} info`);
1502
- return parts.join(" · ");
1532
+ if (errors > 0) parts.push(`✖ ${errors} error${errors === 1 ? "" : "s"}`);
1533
+ if (warnings > 0) parts.push(`⚠ ${warnings} warning${warnings === 1 ? "" : "s"}`);
1534
+ if (infos > 0) parts.push(`${infos} info`);
1535
+ const variant = errors > 0 ? "error" : warnings > 0 ? "warn" : null;
1536
+ return {
1537
+ text: parts.join(" · "),
1538
+ variant,
1539
+ hasErrorsOrWarnings: errors > 0 || warnings > 0
1540
+ };
1503
1541
  }
1504
1542
  function diagnosticKey(d, i) {
1505
1543
  return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
1506
1544
  }
1507
- function summaryVariant(diagnostics) {
1508
- if (diagnostics.length === 0) return "ok";
1509
- if (diagnostics.some((d) => d.severity === "error")) return "error";
1510
- if (diagnostics.some((d) => d.severity === "warn")) return "warn";
1511
- return null;
1512
- }
1513
1545
  /**
1514
1546
  * Render the project's load diagnostics — parser errors, resolver warnings,
1515
1547
  * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
@@ -1521,24 +1553,26 @@ function summaryVariant(diagnostics) {
1521
1553
  * on their own MDX pages.
1522
1554
  */
1523
1555
  function Diagnostics({ caption } = {}) {
1524
- const { activeTheme, cssVarPrefix, diagnostics } = useProject();
1525
- const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
1526
- const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
1527
- const variant = summaryVariant(diagnostics);
1556
+ const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1557
+ const summary = useMemo(() => summarize(diagnostics), [diagnostics]);
1558
+ const headingText = caption ?? `Diagnostics · ${summary.text}`;
1528
1559
  return /* @__PURE__ */ jsx("div", {
1529
- ...themeAttrs(cssVarPrefix, activeTheme),
1560
+ ...themeAttrs(cssVarPrefix, activeAxes),
1530
1561
  "data-testid": "diagnostics",
1531
1562
  children: /* @__PURE__ */ jsxs("details", {
1532
- open: hasErrorsOrWarnings,
1563
+ open: summary.hasErrorsOrWarnings,
1533
1564
  children: [/* @__PURE__ */ jsx("summary", {
1534
- className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
1565
+ className: cx("sb-diagnostics__summary", summary.variant && `sb-diagnostics__summary--${summary.variant}`),
1535
1566
  children: headingText
1536
1567
  }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
1568
+ role: "list",
1537
1569
  className: "sb-diagnostics__list",
1538
1570
  children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
1539
1571
  className: "sb-diagnostics__row",
1572
+ "aria-label": `${severityLabel[d.severity]}: ${d.message}`,
1540
1573
  children: [/* @__PURE__ */ jsx("span", {
1541
1574
  className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
1575
+ "aria-hidden": true,
1542
1576
  children: severityLabel[d.severity]
1543
1577
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
1544
1578
  className: "sb-diagnostics__meta",
@@ -1743,7 +1777,7 @@ function toPixels(raw) {
1743
1777
  }
1744
1778
  function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
1745
1779
  const project = useProject();
1746
- const { resolved, activeTheme, cssVarPrefix } = project;
1780
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1747
1781
  const rows = useMemo(() => {
1748
1782
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1749
1783
  if (token.$type !== "dimension") return false;
@@ -1770,14 +1804,14 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1770
1804
  ]);
1771
1805
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1772
1806
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1773
- ...themeAttrs(cssVarPrefix, activeTheme),
1807
+ ...themeAttrs(cssVarPrefix, activeAxes),
1774
1808
  children: /* @__PURE__ */ jsx("div", {
1775
1809
  className: "sb-block__empty",
1776
1810
  children: "No dimension tokens match this filter."
1777
1811
  })
1778
1812
  });
1779
1813
  return /* @__PURE__ */ jsxs("div", {
1780
- ...themeAttrs(cssVarPrefix, activeTheme),
1814
+ ...themeAttrs(cssVarPrefix, activeAxes),
1781
1815
  children: [/* @__PURE__ */ jsx("div", {
1782
1816
  className: "sb-block__caption",
1783
1817
  children: captionText
@@ -1825,7 +1859,7 @@ function stackString(raw) {
1825
1859
  }
1826
1860
  function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1827
1861
  const project = useProject();
1828
- const { resolved, activeTheme, cssVarPrefix } = project;
1862
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1829
1863
  const rows = useMemo(() => {
1830
1864
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1831
1865
  if (token.$type !== "fontFamily") return false;
@@ -1847,14 +1881,14 @@ function FontFamilySample({ filter, sample = "The quick brown fox jumps over the
1847
1881
  ]);
1848
1882
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1849
1883
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1850
- ...themeAttrs(cssVarPrefix, activeTheme),
1884
+ ...themeAttrs(cssVarPrefix, activeAxes),
1851
1885
  children: /* @__PURE__ */ jsx("div", {
1852
1886
  className: "sb-block__empty",
1853
1887
  children: "No fontFamily tokens match this filter."
1854
1888
  })
1855
1889
  });
1856
1890
  return /* @__PURE__ */ jsxs("div", {
1857
- ...themeAttrs(cssVarPrefix, activeTheme),
1891
+ ...themeAttrs(cssVarPrefix, activeAxes),
1858
1892
  children: [/* @__PURE__ */ jsx("div", {
1859
1893
  className: "sb-block__caption",
1860
1894
  children: captionText
@@ -1911,7 +1945,7 @@ function toWeight(raw) {
1911
1945
  }
1912
1946
  function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
1913
1947
  const project = useProject();
1914
- const { resolved, activeTheme, cssVarPrefix } = project;
1948
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1915
1949
  const rows = useMemo(() => {
1916
1950
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1917
1951
  if (token.$type !== "fontWeight") return false;
@@ -1934,14 +1968,14 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
1934
1968
  ]);
1935
1969
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1936
1970
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1937
- ...themeAttrs(cssVarPrefix, activeTheme),
1971
+ ...themeAttrs(cssVarPrefix, activeAxes),
1938
1972
  children: /* @__PURE__ */ jsx("div", {
1939
1973
  className: "sb-block__empty",
1940
1974
  children: "No fontWeight tokens match this filter."
1941
1975
  })
1942
1976
  });
1943
1977
  return /* @__PURE__ */ jsxs("div", {
1944
- ...themeAttrs(cssVarPrefix, activeTheme),
1978
+ ...themeAttrs(cssVarPrefix, activeAxes),
1945
1979
  children: [/* @__PURE__ */ jsx("div", {
1946
1980
  className: "sb-block__caption",
1947
1981
  children: captionText
@@ -1991,7 +2025,7 @@ function stopKey(path, stop, fallback) {
1991
2025
  }
1992
2026
  function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1993
2027
  const project = useProject();
1994
- const { resolved, activeTheme, cssVarPrefix } = project;
2028
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1995
2029
  const colorFormat = useColorFormat();
1996
2030
  const rows = useMemo(() => {
1997
2031
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -2014,14 +2048,14 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2014
2048
  ]);
2015
2049
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2016
2050
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2017
- ...themeAttrs(cssVarPrefix, activeTheme),
2051
+ ...themeAttrs(cssVarPrefix, activeAxes),
2018
2052
  children: /* @__PURE__ */ jsx("div", {
2019
2053
  className: "sb-block__empty",
2020
2054
  children: "No gradient tokens match this filter."
2021
2055
  })
2022
2056
  });
2023
2057
  return /* @__PURE__ */ jsxs("div", {
2024
- ...themeAttrs(cssVarPrefix, activeTheme),
2058
+ ...themeAttrs(cssVarPrefix, activeAxes),
2025
2059
  children: [/* @__PURE__ */ jsx("div", {
2026
2060
  className: "sb-block__caption",
2027
2061
  children: captionText
@@ -2225,9 +2259,13 @@ function MotionSample({ path, speed = 1, runKey = 0 }) {
2225
2259
  runKey,
2226
2260
  reducedMotion
2227
2261
  ]);
2228
- if (reducedMotion) return /* @__PURE__ */ jsx("div", {
2262
+ if (reducedMotion) return /* @__PURE__ */ jsxs("div", {
2229
2263
  style: styles.reducedMotion,
2230
- children: "Animation suppressed by `prefers-reduced-motion: reduce`."
2264
+ children: [
2265
+ "Animation suppressed by ",
2266
+ /* @__PURE__ */ jsx("code", { children: "prefers-reduced-motion: reduce" }),
2267
+ "."
2268
+ ]
2231
2269
  });
2232
2270
  return /* @__PURE__ */ jsx("div", {
2233
2271
  style: styles.track,
@@ -2258,7 +2296,7 @@ function formatSpec(row) {
2258
2296
  }
2259
2297
  function MotionPreview({ filter, caption }) {
2260
2298
  const project = useProject();
2261
- const { resolved, activeTheme, cssVarPrefix } = project;
2299
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2262
2300
  const [speed, setSpeed] = useState(1);
2263
2301
  const [run, setRun] = useState(0);
2264
2302
  const reducedMotion = usePrefersReducedMotion();
@@ -2295,14 +2333,14 @@ function MotionPreview({ filter, caption }) {
2295
2333
  ]);
2296
2334
  const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2297
2335
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2298
- ...themeAttrs(cssVarPrefix, activeTheme),
2336
+ ...themeAttrs(cssVarPrefix, activeAxes),
2299
2337
  children: /* @__PURE__ */ jsx("div", {
2300
2338
  className: "sb-block__empty",
2301
2339
  children: "No motion tokens match this filter."
2302
2340
  })
2303
2341
  });
2304
2342
  return /* @__PURE__ */ jsxs("div", {
2305
- ...themeAttrs(cssVarPrefix, activeTheme),
2343
+ ...themeAttrs(cssVarPrefix, activeAxes),
2306
2344
  children: [
2307
2345
  /* @__PURE__ */ jsx("div", {
2308
2346
  className: "sb-block__caption",
@@ -2376,7 +2414,7 @@ function toOpacity(raw) {
2376
2414
  */
2377
2415
  function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2378
2416
  const project = useProject();
2379
- const { resolved, activeTheme, cssVarPrefix } = project;
2417
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2380
2418
  const rows = useMemo(() => {
2381
2419
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2382
2420
  if (token.$type !== type) return false;
@@ -2405,7 +2443,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2405
2443
  ]);
2406
2444
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2407
2445
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2408
- ...themeAttrs(cssVarPrefix, activeTheme),
2446
+ ...themeAttrs(cssVarPrefix, activeAxes),
2409
2447
  children: /* @__PURE__ */ jsx("div", {
2410
2448
  className: "sb-block__empty",
2411
2449
  children: "No opacity tokens match this filter."
@@ -2413,7 +2451,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2413
2451
  });
2414
2452
  const sampleColorVar = resolveCssVar(sampleColor, project);
2415
2453
  return /* @__PURE__ */ jsxs("div", {
2416
- ...themeAttrs(cssVarPrefix, activeTheme),
2454
+ ...themeAttrs(cssVarPrefix, activeAxes),
2417
2455
  children: [/* @__PURE__ */ jsx("div", {
2418
2456
  className: "sb-block__caption",
2419
2457
  children: captionText
@@ -2521,7 +2559,7 @@ function layerKey(path, layer, fallback) {
2521
2559
  }
2522
2560
  function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2523
2561
  const project = useProject();
2524
- const { resolved, activeTheme, cssVarPrefix } = project;
2562
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2525
2563
  const colorFormat = useColorFormat();
2526
2564
  const rows = useMemo(() => {
2527
2565
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -2544,14 +2582,14 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2544
2582
  ]);
2545
2583
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2546
2584
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2547
- ...themeAttrs(cssVarPrefix, activeTheme),
2585
+ ...themeAttrs(cssVarPrefix, activeAxes),
2548
2586
  children: /* @__PURE__ */ jsx("div", {
2549
2587
  className: "sb-block__empty",
2550
2588
  children: "No shadow tokens match this filter."
2551
2589
  })
2552
2590
  });
2553
2591
  return /* @__PURE__ */ jsxs("div", {
2554
- ...themeAttrs(cssVarPrefix, activeTheme),
2592
+ ...themeAttrs(cssVarPrefix, activeAxes),
2555
2593
  children: [/* @__PURE__ */ jsx("div", {
2556
2594
  className: "sb-block__caption",
2557
2595
  children: captionText
@@ -2634,7 +2672,7 @@ function extractCssStyle(value) {
2634
2672
  }
2635
2673
  function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2636
2674
  const project = useProject();
2637
- const { resolved, activeTheme, cssVarPrefix } = project;
2675
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2638
2676
  const rows = useMemo(() => {
2639
2677
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2640
2678
  if (token.$type !== "strokeStyle") return false;
@@ -2657,14 +2695,14 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2657
2695
  ]);
2658
2696
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2659
2697
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2660
- ...themeAttrs(cssVarPrefix, activeTheme),
2698
+ ...themeAttrs(cssVarPrefix, activeAxes),
2661
2699
  children: /* @__PURE__ */ jsx("div", {
2662
2700
  className: "sb-block__empty",
2663
2701
  children: "No strokeStyle tokens match this filter."
2664
2702
  })
2665
2703
  });
2666
2704
  return /* @__PURE__ */ jsxs("div", {
2667
- ...themeAttrs(cssVarPrefix, activeTheme),
2705
+ ...themeAttrs(cssVarPrefix, activeAxes),
2668
2706
  children: [/* @__PURE__ */ jsx("div", {
2669
2707
  className: "sb-block__caption",
2670
2708
  children: captionText
@@ -2701,7 +2739,7 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2701
2739
  //#region src/token-detail/internal.ts
2702
2740
  function useTokenDetailData(path) {
2703
2741
  const project = useProject();
2704
- const { activeTheme, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt, themeNameForTuple } = project;
2742
+ const { activeTheme, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt } = project;
2705
2743
  const typedResolved = resolved;
2706
2744
  return {
2707
2745
  token: typedResolved[path],
@@ -2712,8 +2750,7 @@ function useTokenDetailData(path) {
2712
2750
  resolved: typedResolved,
2713
2751
  cssVarPrefix,
2714
2752
  varianceByPath,
2715
- resolveAt,
2716
- themeNameForTuple
2753
+ resolveAt
2717
2754
  };
2718
2755
  }
2719
2756
  //#endregion
@@ -2838,7 +2875,7 @@ function treeHasTruncation(nodes) {
2838
2875
  //#endregion
2839
2876
  //#region src/token-detail/AxisVariance.tsx
2840
2877
  function AxisVariance({ path }) {
2841
- const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt, themeNameForTuple } = useTokenDetailData(path);
2878
+ const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt } = useTokenDetailData(path);
2842
2879
  const colorFormat = useColorFormat();
2843
2880
  const tokenType = token?.$type;
2844
2881
  const isColor = tokenType === "color";
@@ -2857,6 +2894,7 @@ function AxisVariance({ path }) {
2857
2894
  kind: "multi-axis",
2858
2895
  varyingAxes: result.varyingAxes
2859
2896
  };
2897
+ default: throw new Error(`unhandled AxisVarianceResult kind: ${JSON.stringify(result)}`);
2860
2898
  }
2861
2899
  }, [path, varianceByPath]);
2862
2900
  if (axes.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
@@ -2903,7 +2941,7 @@ function AxisVariance({ path }) {
2903
2941
  };
2904
2942
  return {
2905
2943
  ctx,
2906
- themeName: themeNameForTuple(target) ?? "",
2944
+ target,
2907
2945
  value: formatFn(resolveAt(target)[path])
2908
2946
  };
2909
2947
  });
@@ -2923,10 +2961,10 @@ function AxisVariance({ path }) {
2923
2961
  children: row.ctx
2924
2962
  }), /* @__PURE__ */ jsxs("td", {
2925
2963
  className: "sb-token-detail__theme-cell",
2926
- children: [isColor && row.themeName && /* @__PURE__ */ jsx("span", {
2964
+ children: [isColor && /* @__PURE__ */ jsx("span", {
2927
2965
  className: "sb-token-detail__swatch",
2928
2966
  style: { background: cssVar },
2929
- [dataAttr(cssVarPrefix, "theme")]: row.themeName,
2967
+ ...perAxisAttrs(cssVarPrefix, row.target),
2930
2968
  "aria-hidden": true
2931
2969
  }), row.value]
2932
2970
  })]
@@ -2975,16 +3013,15 @@ function AxisVariance({ path }) {
2975
3013
  [rowAxis.name]: row,
2976
3014
  [colAxis.name]: col
2977
3015
  };
2978
- const name = themeNameForTuple(target);
2979
3016
  const value = formatFn(resolveAt(target)[path]);
2980
3017
  return /* @__PURE__ */ jsxs("td", {
2981
3018
  className: "sb-token-detail__theme-cell",
2982
3019
  "data-row": row,
2983
3020
  "data-col": col,
2984
- children: [isColor && name && /* @__PURE__ */ jsx("span", {
3021
+ children: [isColor && /* @__PURE__ */ jsx("span", {
2985
3022
  className: "sb-token-detail__swatch",
2986
3023
  style: { background: cssVar },
2987
- [dataAttr(cssVarPrefix, "theme")]: name,
3024
+ ...perAxisAttrs(cssVarPrefix, target),
2988
3025
  "aria-hidden": true
2989
3026
  }), value]
2990
3027
  }, col);
@@ -3564,9 +3601,9 @@ function TokenUsageSnippet({ path }) {
3564
3601
  //#endregion
3565
3602
  //#region src/TokenDetail.tsx
3566
3603
  function TokenDetail({ path, heading }) {
3567
- const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
3604
+ const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
3568
3605
  const colorFormat = useColorFormat();
3569
- const theme = themeAttrs(cssVarPrefix, activeTheme);
3606
+ const theme = themeAttrs(cssVarPrefix, activeAxes);
3570
3607
  if (!token) return /* @__PURE__ */ jsx("div", {
3571
3608
  ...theme,
3572
3609
  className: cx(theme["className"], "sb-token-detail"),
@@ -3649,9 +3686,23 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3649
3686
  const panelRef = useRef(null);
3650
3687
  const openerRef = useRef(null);
3651
3688
  useEffect(() => {
3689
+ const panel = panelRef.current;
3690
+ if (!panel) return;
3652
3691
  openerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3653
- panelRef.current?.focus();
3692
+ panel.focus();
3693
+ const overlayBranch = findBodyChildContaining(panel);
3694
+ const restorers = [];
3695
+ if (overlayBranch) for (const sibling of Array.from(document.body.children)) {
3696
+ if (sibling === overlayBranch) continue;
3697
+ if (!(sibling instanceof HTMLElement)) continue;
3698
+ const hadInert = sibling.inert;
3699
+ sibling.inert = true;
3700
+ restorers.push(() => {
3701
+ sibling.inert = hadInert;
3702
+ });
3703
+ }
3654
3704
  return () => {
3705
+ for (const restore of restorers) restore();
3655
3706
  openerRef.current?.focus();
3656
3707
  };
3657
3708
  }, []);
@@ -3715,6 +3766,17 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3715
3766
  })
3716
3767
  });
3717
3768
  }
3769
+ /**
3770
+ * Walk up from `node` to the direct child of `<body>` that contains it.
3771
+ * Returns `null` when the node isn't attached to the document (mid-mount,
3772
+ * post-unmount). Used to identify which top-level branch to *not* mark
3773
+ * inert when the overlay opens.
3774
+ */
3775
+ function findBodyChildContaining(node) {
3776
+ let cursor = node;
3777
+ while (cursor && cursor.parentElement !== document.body) cursor = cursor.parentElement;
3778
+ return cursor;
3779
+ }
3718
3780
  //#endregion
3719
3781
  //#region src/TokenNavigator.tsx
3720
3782
  function buildTree(resolved, root, typeFilter) {
@@ -3825,7 +3887,7 @@ function pruneTreeForMatches(nodes, matches, expandOut) {
3825
3887
  return out;
3826
3888
  }
3827
3889
  function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect }) {
3828
- const { resolved, activeTheme, cssVarPrefix } = useProject();
3890
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
3829
3891
  const typeFilter = useMemo(() => {
3830
3892
  if (type === void 0) return void 0;
3831
3893
  return new Set(Array.isArray(type) ? type : [type]);
@@ -3846,14 +3908,15 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3846
3908
  }, [initialExpanded]);
3847
3909
  const [selectedPath, setSelectedPath] = useState(null);
3848
3910
  const [query, setQuery] = useState("");
3911
+ const deferredQuery = useDeferredValue(query);
3849
3912
  const { visibleTree, searchExpanded } = useMemo(() => {
3850
- if (!searchable || query.trim() === "") return {
3913
+ if (!searchable || deferredQuery.trim() === "") return {
3851
3914
  visibleTree: tree,
3852
3915
  searchExpanded: null
3853
3916
  };
3854
3917
  const leafPaths = [];
3855
3918
  collectLeafPaths(tree, leafPaths);
3856
- const matches = new Set(fuzzyFilter(leafPaths, query, (p) => p));
3919
+ const matches = new Set(fuzzyFilter(leafPaths, deferredQuery, (p) => p));
3857
3920
  const expandOut = /* @__PURE__ */ new Set();
3858
3921
  return {
3859
3922
  visibleTree: matches.size === 0 ? [] : pruneTreeForMatches(tree, matches, expandOut),
@@ -3861,7 +3924,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3861
3924
  };
3862
3925
  }, [
3863
3926
  tree,
3864
- query,
3927
+ deferredQuery,
3865
3928
  searchable
3866
3929
  ]);
3867
3930
  const effectiveExpanded = useMemo(() => {
@@ -3882,7 +3945,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3882
3945
  if (onSelect) onSelect(path);
3883
3946
  else setSelectedPath(path);
3884
3947
  }, [onSelect]);
3885
- const [focusedPath, setFocusedPath] = useState(null);
3948
+ const [storedFocus, setStoredFocus] = useState(null);
3886
3949
  const treeItemRefs = useRef(/* @__PURE__ */ new Map());
3887
3950
  const registerTreeItem = useCallback((path) => (el) => {
3888
3951
  if (el) treeItemRefs.current.set(path, el);
@@ -3893,22 +3956,15 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3893
3956
  flattenVisible(visibleTree, effectiveExpanded, null, out);
3894
3957
  return out;
3895
3958
  }, [visibleTree, effectiveExpanded]);
3896
- useEffect(() => {
3897
- if (flatVisible.length === 0) {
3898
- setFocusedPath(null);
3899
- return;
3900
- }
3901
- setFocusedPath((prev) => {
3902
- if (prev && flatVisible.some((entry) => entry.path === prev)) return prev;
3903
- return flatVisible[0]?.path ?? null;
3904
- });
3905
- }, [flatVisible]);
3959
+ const focusedPath = useMemo(() => {
3960
+ if (flatVisible.length === 0) return null;
3961
+ if (storedFocus && flatVisible.some((entry) => entry.path === storedFocus)) return storedFocus;
3962
+ return flatVisible[0]?.path ?? null;
3963
+ }, [flatVisible, storedFocus]);
3906
3964
  const focusByPath = useCallback((path) => {
3907
3965
  const node = treeItemRefs.current.get(path);
3908
- if (node) {
3909
- node.focus();
3910
- setFocusedPath(path);
3911
- } else setFocusedPath(path);
3966
+ if (node) node.focus();
3967
+ setStoredFocus(path);
3912
3968
  }, []);
3913
3969
  useEffect(() => {
3914
3970
  if (focusedPath === null) return;
@@ -4010,11 +4066,11 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4010
4066
  return n;
4011
4067
  }, [visibleTree, searchExpanded]);
4012
4068
  if (tree.length === 0) return /* @__PURE__ */ jsx("div", {
4013
- ...themeAttrs(cssVarPrefix, activeTheme),
4069
+ ...themeAttrs(cssVarPrefix, activeAxes),
4014
4070
  children: /* @__PURE__ */ jsx(EmptyState, { children: root ? `No tokens under "${root}"${typeFilter ? ` matching ${typeLabel.slice(3)}` : ""}.` : typeFilter ? `No tokens matching ${typeLabel.slice(3)} in the active theme.` : "No tokens in the active theme." })
4015
4071
  });
4016
4072
  return /* @__PURE__ */ jsxs("div", {
4017
- ...themeAttrs(cssVarPrefix, activeTheme),
4073
+ ...themeAttrs(cssVarPrefix, activeAxes),
4018
4074
  children: [
4019
4075
  searchable && /* @__PURE__ */ jsx("div", {
4020
4076
  className: "sb-token-navigator__search",
@@ -4038,6 +4094,12 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4038
4094
  activeTheme
4039
4095
  ]
4040
4096
  }),
4097
+ searchable && /* @__PURE__ */ jsx("span", {
4098
+ role: "status",
4099
+ "aria-live": "polite",
4100
+ className: "sb-token-navigator__sr-status",
4101
+ children: trimmedQuery !== "" ? `${matchCount} tokens matching "${trimmedQuery}"` : ""
4102
+ }),
4041
4103
  visibleTree.length === 0 ? /* @__PURE__ */ jsxs("div", {
4042
4104
  className: "sb-block__empty",
4043
4105
  children: [
@@ -4050,14 +4112,17 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4050
4112
  role: "tree",
4051
4113
  "aria-label": "Token graph",
4052
4114
  onKeyDown: handleTreeKeyDown,
4053
- children: visibleTree.map((node) => /* @__PURE__ */ jsx(TreeNodeRow, {
4115
+ children: visibleTree.map((node, i) => /* @__PURE__ */ jsx(TreeNodeRow, {
4054
4116
  node,
4055
4117
  expanded: effectiveExpanded,
4056
4118
  focusedPath,
4057
4119
  registerTreeItem,
4058
4120
  onToggle: toggle,
4059
- onFocusPath: setFocusedPath,
4060
- onLeafClick: handleLeafClick
4121
+ onFocusPath: setStoredFocus,
4122
+ onLeafClick: handleLeafClick,
4123
+ level: 1,
4124
+ setsize: visibleTree.length,
4125
+ posinset: i + 1
4061
4126
  }, node.path || node.segment))
4062
4127
  }),
4063
4128
  selectedPath !== null && /* @__PURE__ */ jsx(DetailOverlay, {
@@ -4068,13 +4133,16 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4068
4133
  ]
4069
4134
  });
4070
4135
  }
4071
- function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick }) {
4136
+ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, level, setsize, posinset }) {
4072
4137
  if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
4073
4138
  node,
4074
- focusedPath,
4139
+ isFocused: focusedPath === node.path,
4075
4140
  registerTreeItem,
4076
4141
  onFocusPath,
4077
- onLeafClick
4142
+ onLeafClick,
4143
+ level,
4144
+ setsize,
4145
+ posinset
4078
4146
  });
4079
4147
  const isOpen = expanded.has(node.path);
4080
4148
  const isFocused = focusedPath === node.path;
@@ -4082,6 +4150,9 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
4082
4150
  ref: registerTreeItem(node.path),
4083
4151
  role: "treeitem",
4084
4152
  "aria-expanded": isOpen,
4153
+ "aria-level": level,
4154
+ "aria-setsize": setsize,
4155
+ "aria-posinset": posinset,
4085
4156
  tabIndex: isFocused ? 0 : -1,
4086
4157
  onFocus: () => onFocusPath(node.path),
4087
4158
  "data-path": node.path,
@@ -4108,24 +4179,29 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
4108
4179
  }), isOpen && /* @__PURE__ */ jsx("ul", {
4109
4180
  className: "sb-token-navigator__nested",
4110
4181
  role: "group",
4111
- children: node.children.map((c) => /* @__PURE__ */ jsx(TreeNodeRow, {
4182
+ children: node.children.map((c, i) => /* @__PURE__ */ jsx(TreeNodeRow, {
4112
4183
  node: c,
4113
4184
  expanded,
4114
4185
  focusedPath,
4115
4186
  registerTreeItem,
4116
4187
  onToggle,
4117
4188
  onFocusPath,
4118
- onLeafClick
4189
+ onLeafClick,
4190
+ level: level + 1,
4191
+ setsize: node.children.length,
4192
+ posinset: i + 1
4119
4193
  }, c.path || c.segment))
4120
4194
  })]
4121
4195
  });
4122
4196
  }
4123
- function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick }) {
4197
+ const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFocusPath, onLeafClick, level, setsize, posinset }) {
4124
4198
  const type = node.token.$type ?? "";
4125
- const isFocused = focusedPath === node.path;
4126
4199
  return /* @__PURE__ */ jsx("li", {
4127
4200
  ref: registerTreeItem(node.path),
4128
4201
  role: "treeitem",
4202
+ "aria-level": level,
4203
+ "aria-setsize": setsize,
4204
+ "aria-posinset": posinset,
4129
4205
  tabIndex: isFocused ? 0 : -1,
4130
4206
  onFocus: () => onFocusPath(node.path),
4131
4207
  "data-path": node.path,
@@ -4158,8 +4234,8 @@ function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick
4158
4234
  ]
4159
4235
  })
4160
4236
  });
4161
- }
4162
- function LeafPreview({ path, token }) {
4237
+ });
4238
+ const LeafPreview = memo(function LeafPreview({ path, token }) {
4163
4239
  const project = useProject();
4164
4240
  const colorFormat = useColorFormat();
4165
4241
  const type = token.$type;
@@ -4218,16 +4294,20 @@ function LeafPreview({ path, token }) {
4218
4294
  children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4219
4295
  })
4220
4296
  });
4221
- }
4297
+ });
4222
4298
  //#endregion
4223
4299
  //#region src/TokenTable.tsx
4224
4300
  function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
4225
- const project = useProject();
4226
- const { resolved, activeTheme, cssVarPrefix } = project;
4301
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
4227
4302
  const colorFormat = useColorFormat();
4228
4303
  const [selectedPath, setSelectedPath] = useState(null);
4229
4304
  const [query, setQuery] = useState("");
4305
+ const deferredQuery = useDeferredValue(query);
4230
4306
  const rows = useMemo(() => {
4307
+ const projectFields = {
4308
+ listing,
4309
+ cssVarPrefix
4310
+ };
4231
4311
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4232
4312
  if (!matchPath(path, filter)) return false;
4233
4313
  if (type && token.$type !== type) return false;
@@ -4237,31 +4317,32 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4237
4317
  dir: sortDir
4238
4318
  }).map(([path, token]) => {
4239
4319
  const isColor = token.$type === "color";
4240
- const color = isColor ? resolveColorValue(path, token.$value, colorFormat, project) : null;
4320
+ const color = isColor ? resolveColorValue(path, token.$value, colorFormat, projectFields) : null;
4241
4321
  return {
4242
4322
  path,
4243
4323
  type: token.$type ?? "",
4244
- value: formatTokenValue(token.$value, token.$type, colorFormat, project.listing[path]),
4324
+ value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4245
4325
  outOfGamut: color?.outOfGamut ?? false,
4246
- cssVar: resolveCssVar(path, project),
4326
+ cssVar: resolveCssVar(path, projectFields),
4247
4327
  isColor
4248
4328
  };
4249
4329
  });
4250
4330
  }, [
4251
4331
  resolved,
4332
+ listing,
4333
+ cssVarPrefix,
4252
4334
  filter,
4253
4335
  type,
4254
- project,
4255
4336
  colorFormat,
4256
4337
  sortBy,
4257
4338
  sortDir
4258
4339
  ]);
4259
4340
  const visibleRows = useMemo(() => {
4260
- if (!searchable || query.trim() === "") return rows;
4261
- return fuzzyFilter(rows, query, (row) => `${row.path} ${row.type} ${row.value}`);
4341
+ if (!searchable || deferredQuery.trim() === "") return rows;
4342
+ return fuzzyFilter(rows, deferredQuery, (row) => `${row.path} ${row.type} ${row.value}`);
4262
4343
  }, [
4263
4344
  rows,
4264
- query,
4345
+ deferredQuery,
4265
4346
  searchable
4266
4347
  ]);
4267
4348
  const handleRowClick = useCallback((path) => {
@@ -4271,14 +4352,14 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4271
4352
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
4272
4353
  const captionText = caption ?? `${rows.length} token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${type ? ` · $type=${type}` : ""}${matchSuffix} · ${activeTheme}`;
4273
4354
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4274
- ...themeAttrs(cssVarPrefix, activeTheme),
4355
+ ...themeAttrs(cssVarPrefix, activeAxes),
4275
4356
  children: /* @__PURE__ */ jsx("div", {
4276
4357
  className: "sb-block__empty",
4277
4358
  children: "No tokens match this filter."
4278
4359
  })
4279
4360
  });
4280
4361
  return /* @__PURE__ */ jsxs("div", {
4281
- ...themeAttrs(cssVarPrefix, activeTheme),
4362
+ ...themeAttrs(cssVarPrefix, activeAxes),
4282
4363
  children: [
4283
4364
  searchable && /* @__PURE__ */ jsx("div", {
4284
4365
  className: "sb-token-table__search",
@@ -4292,6 +4373,12 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4292
4373
  "data-testid": "token-table-search"
4293
4374
  })
4294
4375
  }),
4376
+ searchable && /* @__PURE__ */ jsx("span", {
4377
+ role: "status",
4378
+ "aria-live": "polite",
4379
+ className: "sb-token-table__sr-status",
4380
+ children: query.trim() !== "" ? `${visibleRows.length} of ${rows.length} tokens match "${query.trim()}"` : ""
4381
+ }),
4295
4382
  /* @__PURE__ */ jsxs("table", {
4296
4383
  className: "sb-token-table__table",
4297
4384
  children: [
@@ -4324,6 +4411,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4324
4411
  }
4325
4412
  },
4326
4413
  tabIndex: 0,
4414
+ "aria-haspopup": "dialog",
4327
4415
  "aria-label": `Inspect ${row.path}`,
4328
4416
  "data-testid": "token-table-row",
4329
4417
  "data-path": row.path,
@@ -4418,7 +4506,7 @@ function buildRow(path, composite) {
4418
4506
  };
4419
4507
  }
4420
4508
  function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
4421
- const { resolved, activeTheme, cssVarPrefix } = useProject();
4509
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
4422
4510
  const rows = useMemo(() => {
4423
4511
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4424
4512
  if (token.$type !== "typography") return false;
@@ -4443,14 +4531,14 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
4443
4531
  ]);
4444
4532
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
4445
4533
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4446
- ...themeAttrs(cssVarPrefix, activeTheme),
4534
+ ...themeAttrs(cssVarPrefix, activeAxes),
4447
4535
  children: /* @__PURE__ */ jsx("div", {
4448
4536
  className: "sb-block__empty",
4449
4537
  children: "No typography tokens match this filter."
4450
4538
  })
4451
4539
  });
4452
4540
  return /* @__PURE__ */ jsxs("div", {
4453
- ...themeAttrs(cssVarPrefix, activeTheme),
4541
+ ...themeAttrs(cssVarPrefix, activeAxes),
4454
4542
  children: [/* @__PURE__ */ jsx("div", {
4455
4543
  className: "sb-block__caption",
4456
4544
  children: captionText