@unpunnyfuns/swatchbook-blocks 0.57.1 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -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
@@ -868,7 +882,7 @@ function fixedPrefixLength(filter) {
868
882
  }
869
883
  function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
870
884
  const project = useProject();
871
- const { resolved, activeTheme, cssVarPrefix } = project;
885
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
872
886
  const colorFormat = useColorFormat();
873
887
  const groups = useMemo(() => {
874
888
  const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -909,14 +923,14 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
909
923
  const totalCount = groups.reduce((acc, [, swatches]) => acc + swatches.length, 0);
910
924
  const captionText = caption ?? `${totalCount} color${totalCount === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
911
925
  if (totalCount === 0) return /* @__PURE__ */ jsx("div", {
912
- ...themeAttrs(cssVarPrefix, activeTheme),
926
+ ...themeAttrs(cssVarPrefix, activeAxes),
913
927
  children: /* @__PURE__ */ jsx("div", {
914
928
  className: "sb-block__empty",
915
929
  children: "No color tokens match this filter."
916
930
  })
917
931
  });
918
932
  return /* @__PURE__ */ jsxs("div", {
919
- ...themeAttrs(cssVarPrefix, activeTheme),
933
+ ...themeAttrs(cssVarPrefix, activeAxes),
920
934
  children: [/* @__PURE__ */ jsx("div", {
921
935
  className: "sb-block__caption",
922
936
  children: captionText
@@ -980,7 +994,7 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
980
994
  const classes = ["sb-copy-button", `sb-copy-button--${variant}`];
981
995
  if (copied) classes.push("sb-copy-button--copied");
982
996
  if (className) classes.push(className);
983
- return /* @__PURE__ */ jsx("button", {
997
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
984
998
  type: "button",
985
999
  className: classes.join(" "),
986
1000
  onClick: handleClick,
@@ -995,7 +1009,12 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
995
1009
  "aria-hidden": true,
996
1010
  children: copied ? "✓" : "⧉"
997
1011
  })
998
- });
1012
+ }), /* @__PURE__ */ jsx("span", {
1013
+ role: "status",
1014
+ "aria-live": "polite",
1015
+ className: "sb-copy-button__sr-status",
1016
+ children: copied ? "Copied" : ""
1017
+ })] });
999
1018
  }
1000
1019
  //#endregion
1001
1020
  //#region src/ColorTable.tsx
@@ -1003,7 +1022,7 @@ const BASE_LABEL = "base";
1003
1022
  const COLUMN_COUNT = 6;
1004
1023
  function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
1005
1024
  const project = useProject();
1006
- const { resolved, activeTheme, cssVarPrefix } = project;
1025
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1007
1026
  const colorFormat = useColorFormat();
1008
1027
  const [query, setQuery] = useState("");
1009
1028
  const [selectedByBase, setSelectedByBase] = useState({});
@@ -1092,14 +1111,14 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1092
1111
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleGroups.length} matching "${query.trim()}"` : "";
1093
1112
  const captionText = caption ?? `${totalTokens} color${totalTokens === 1 ? "" : "s"} across ${groups.length} group${groups.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
1094
1113
  if (groups.length === 0) return /* @__PURE__ */ jsx("div", {
1095
- ...themeAttrs(cssVarPrefix, activeTheme),
1114
+ ...themeAttrs(cssVarPrefix, activeAxes),
1096
1115
  children: /* @__PURE__ */ jsx("div", {
1097
1116
  className: "sb-block__empty",
1098
1117
  children: "No color tokens match this filter."
1099
1118
  })
1100
1119
  });
1101
1120
  return /* @__PURE__ */ jsxs("div", {
1102
- ...themeAttrs(cssVarPrefix, activeTheme),
1121
+ ...themeAttrs(cssVarPrefix, activeAxes),
1103
1122
  children: [searchable && /* @__PURE__ */ jsx("div", {
1104
1123
  className: "sb-color-table__search",
1105
1124
  children: /* @__PURE__ */ jsx("input", {
@@ -1192,6 +1211,7 @@ function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVari
1192
1211
  },
1193
1212
  tabIndex: 0,
1194
1213
  "aria-label": onSelect ? `Inspect ${active.path}` : expanded ? `Collapse ${group.base}` : `Expand ${group.base}`,
1214
+ ...onSelect ? { "aria-haspopup": "dialog" } : {},
1195
1215
  "data-testid": "color-table-row",
1196
1216
  "data-path": active.path,
1197
1217
  "data-base": group.base,
@@ -1521,12 +1541,12 @@ function summaryVariant(diagnostics) {
1521
1541
  * on their own MDX pages.
1522
1542
  */
1523
1543
  function Diagnostics({ caption } = {}) {
1524
- const { activeTheme, cssVarPrefix, diagnostics } = useProject();
1544
+ const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1525
1545
  const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
1526
1546
  const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
1527
1547
  const variant = summaryVariant(diagnostics);
1528
1548
  return /* @__PURE__ */ jsx("div", {
1529
- ...themeAttrs(cssVarPrefix, activeTheme),
1549
+ ...themeAttrs(cssVarPrefix, activeAxes),
1530
1550
  "data-testid": "diagnostics",
1531
1551
  children: /* @__PURE__ */ jsxs("details", {
1532
1552
  open: hasErrorsOrWarnings,
@@ -1534,11 +1554,14 @@ function Diagnostics({ caption } = {}) {
1534
1554
  className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
1535
1555
  children: headingText
1536
1556
  }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
1557
+ role: "list",
1537
1558
  className: "sb-diagnostics__list",
1538
1559
  children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
1539
1560
  className: "sb-diagnostics__row",
1561
+ "aria-label": `${severityLabel[d.severity]}: ${d.message}`,
1540
1562
  children: [/* @__PURE__ */ jsx("span", {
1541
1563
  className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
1564
+ "aria-hidden": true,
1542
1565
  children: severityLabel[d.severity]
1543
1566
  }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
1544
1567
  className: "sb-diagnostics__meta",
@@ -1743,7 +1766,7 @@ function toPixels(raw) {
1743
1766
  }
1744
1767
  function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
1745
1768
  const project = useProject();
1746
- const { resolved, activeTheme, cssVarPrefix } = project;
1769
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1747
1770
  const rows = useMemo(() => {
1748
1771
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1749
1772
  if (token.$type !== "dimension") return false;
@@ -1770,14 +1793,14 @@ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", so
1770
1793
  ]);
1771
1794
  const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1772
1795
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1773
- ...themeAttrs(cssVarPrefix, activeTheme),
1796
+ ...themeAttrs(cssVarPrefix, activeAxes),
1774
1797
  children: /* @__PURE__ */ jsx("div", {
1775
1798
  className: "sb-block__empty",
1776
1799
  children: "No dimension tokens match this filter."
1777
1800
  })
1778
1801
  });
1779
1802
  return /* @__PURE__ */ jsxs("div", {
1780
- ...themeAttrs(cssVarPrefix, activeTheme),
1803
+ ...themeAttrs(cssVarPrefix, activeAxes),
1781
1804
  children: [/* @__PURE__ */ jsx("div", {
1782
1805
  className: "sb-block__caption",
1783
1806
  children: captionText
@@ -1825,7 +1848,7 @@ function stackString(raw) {
1825
1848
  }
1826
1849
  function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1827
1850
  const project = useProject();
1828
- const { resolved, activeTheme, cssVarPrefix } = project;
1851
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1829
1852
  const rows = useMemo(() => {
1830
1853
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1831
1854
  if (token.$type !== "fontFamily") return false;
@@ -1847,14 +1870,14 @@ function FontFamilySample({ filter, sample = "The quick brown fox jumps over the
1847
1870
  ]);
1848
1871
  const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1849
1872
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1850
- ...themeAttrs(cssVarPrefix, activeTheme),
1873
+ ...themeAttrs(cssVarPrefix, activeAxes),
1851
1874
  children: /* @__PURE__ */ jsx("div", {
1852
1875
  className: "sb-block__empty",
1853
1876
  children: "No fontFamily tokens match this filter."
1854
1877
  })
1855
1878
  });
1856
1879
  return /* @__PURE__ */ jsxs("div", {
1857
- ...themeAttrs(cssVarPrefix, activeTheme),
1880
+ ...themeAttrs(cssVarPrefix, activeAxes),
1858
1881
  children: [/* @__PURE__ */ jsx("div", {
1859
1882
  className: "sb-block__caption",
1860
1883
  children: captionText
@@ -1911,7 +1934,7 @@ function toWeight(raw) {
1911
1934
  }
1912
1935
  function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
1913
1936
  const project = useProject();
1914
- const { resolved, activeTheme, cssVarPrefix } = project;
1937
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1915
1938
  const rows = useMemo(() => {
1916
1939
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1917
1940
  if (token.$type !== "fontWeight") return false;
@@ -1934,14 +1957,14 @@ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sor
1934
1957
  ]);
1935
1958
  const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1936
1959
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1937
- ...themeAttrs(cssVarPrefix, activeTheme),
1960
+ ...themeAttrs(cssVarPrefix, activeAxes),
1938
1961
  children: /* @__PURE__ */ jsx("div", {
1939
1962
  className: "sb-block__empty",
1940
1963
  children: "No fontWeight tokens match this filter."
1941
1964
  })
1942
1965
  });
1943
1966
  return /* @__PURE__ */ jsxs("div", {
1944
- ...themeAttrs(cssVarPrefix, activeTheme),
1967
+ ...themeAttrs(cssVarPrefix, activeAxes),
1945
1968
  children: [/* @__PURE__ */ jsx("div", {
1946
1969
  className: "sb-block__caption",
1947
1970
  children: captionText
@@ -1991,7 +2014,7 @@ function stopKey(path, stop, fallback) {
1991
2014
  }
1992
2015
  function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1993
2016
  const project = useProject();
1994
- const { resolved, activeTheme, cssVarPrefix } = project;
2017
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1995
2018
  const colorFormat = useColorFormat();
1996
2019
  const rows = useMemo(() => {
1997
2020
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -2014,14 +2037,14 @@ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" })
2014
2037
  ]);
2015
2038
  const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2016
2039
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2017
- ...themeAttrs(cssVarPrefix, activeTheme),
2040
+ ...themeAttrs(cssVarPrefix, activeAxes),
2018
2041
  children: /* @__PURE__ */ jsx("div", {
2019
2042
  className: "sb-block__empty",
2020
2043
  children: "No gradient tokens match this filter."
2021
2044
  })
2022
2045
  });
2023
2046
  return /* @__PURE__ */ jsxs("div", {
2024
- ...themeAttrs(cssVarPrefix, activeTheme),
2047
+ ...themeAttrs(cssVarPrefix, activeAxes),
2025
2048
  children: [/* @__PURE__ */ jsx("div", {
2026
2049
  className: "sb-block__caption",
2027
2050
  children: captionText
@@ -2225,9 +2248,13 @@ function MotionSample({ path, speed = 1, runKey = 0 }) {
2225
2248
  runKey,
2226
2249
  reducedMotion
2227
2250
  ]);
2228
- if (reducedMotion) return /* @__PURE__ */ jsx("div", {
2251
+ if (reducedMotion) return /* @__PURE__ */ jsxs("div", {
2229
2252
  style: styles.reducedMotion,
2230
- children: "Animation suppressed by `prefers-reduced-motion: reduce`."
2253
+ children: [
2254
+ "Animation suppressed by ",
2255
+ /* @__PURE__ */ jsx("code", { children: "prefers-reduced-motion: reduce" }),
2256
+ "."
2257
+ ]
2231
2258
  });
2232
2259
  return /* @__PURE__ */ jsx("div", {
2233
2260
  style: styles.track,
@@ -2258,7 +2285,7 @@ function formatSpec(row) {
2258
2285
  }
2259
2286
  function MotionPreview({ filter, caption }) {
2260
2287
  const project = useProject();
2261
- const { resolved, activeTheme, cssVarPrefix } = project;
2288
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2262
2289
  const [speed, setSpeed] = useState(1);
2263
2290
  const [run, setRun] = useState(0);
2264
2291
  const reducedMotion = usePrefersReducedMotion();
@@ -2295,14 +2322,14 @@ function MotionPreview({ filter, caption }) {
2295
2322
  ]);
2296
2323
  const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2297
2324
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2298
- ...themeAttrs(cssVarPrefix, activeTheme),
2325
+ ...themeAttrs(cssVarPrefix, activeAxes),
2299
2326
  children: /* @__PURE__ */ jsx("div", {
2300
2327
  className: "sb-block__empty",
2301
2328
  children: "No motion tokens match this filter."
2302
2329
  })
2303
2330
  });
2304
2331
  return /* @__PURE__ */ jsxs("div", {
2305
- ...themeAttrs(cssVarPrefix, activeTheme),
2332
+ ...themeAttrs(cssVarPrefix, activeAxes),
2306
2333
  children: [
2307
2334
  /* @__PURE__ */ jsx("div", {
2308
2335
  className: "sb-block__caption",
@@ -2376,7 +2403,7 @@ function toOpacity(raw) {
2376
2403
  */
2377
2404
  function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg", caption, sortBy = "value", sortDir = "asc" }) {
2378
2405
  const project = useProject();
2379
- const { resolved, activeTheme, cssVarPrefix } = project;
2406
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2380
2407
  const rows = useMemo(() => {
2381
2408
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2382
2409
  if (token.$type !== type) return false;
@@ -2405,7 +2432,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2405
2432
  ]);
2406
2433
  const captionText = caption ?? `${rows.length} opacity token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2407
2434
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2408
- ...themeAttrs(cssVarPrefix, activeTheme),
2435
+ ...themeAttrs(cssVarPrefix, activeAxes),
2409
2436
  children: /* @__PURE__ */ jsx("div", {
2410
2437
  className: "sb-block__empty",
2411
2438
  children: "No opacity tokens match this filter."
@@ -2413,7 +2440,7 @@ function OpacityScale({ filter, type = "number", sampleColor = "color.accent.bg"
2413
2440
  });
2414
2441
  const sampleColorVar = resolveCssVar(sampleColor, project);
2415
2442
  return /* @__PURE__ */ jsxs("div", {
2416
- ...themeAttrs(cssVarPrefix, activeTheme),
2443
+ ...themeAttrs(cssVarPrefix, activeAxes),
2417
2444
  children: [/* @__PURE__ */ jsx("div", {
2418
2445
  className: "sb-block__caption",
2419
2446
  children: captionText
@@ -2521,7 +2548,7 @@ function layerKey(path, layer, fallback) {
2521
2548
  }
2522
2549
  function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2523
2550
  const project = useProject();
2524
- const { resolved, activeTheme, cssVarPrefix } = project;
2551
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2525
2552
  const colorFormat = useColorFormat();
2526
2553
  const rows = useMemo(() => {
2527
2554
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -2544,14 +2571,14 @@ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2544
2571
  ]);
2545
2572
  const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2546
2573
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2547
- ...themeAttrs(cssVarPrefix, activeTheme),
2574
+ ...themeAttrs(cssVarPrefix, activeAxes),
2548
2575
  children: /* @__PURE__ */ jsx("div", {
2549
2576
  className: "sb-block__empty",
2550
2577
  children: "No shadow tokens match this filter."
2551
2578
  })
2552
2579
  });
2553
2580
  return /* @__PURE__ */ jsxs("div", {
2554
- ...themeAttrs(cssVarPrefix, activeTheme),
2581
+ ...themeAttrs(cssVarPrefix, activeAxes),
2555
2582
  children: [/* @__PURE__ */ jsx("div", {
2556
2583
  className: "sb-block__caption",
2557
2584
  children: captionText
@@ -2634,7 +2661,7 @@ function extractCssStyle(value) {
2634
2661
  }
2635
2662
  function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2636
2663
  const project = useProject();
2637
- const { resolved, activeTheme, cssVarPrefix } = project;
2664
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
2638
2665
  const rows = useMemo(() => {
2639
2666
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2640
2667
  if (token.$type !== "strokeStyle") return false;
@@ -2657,14 +2684,14 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2657
2684
  ]);
2658
2685
  const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2659
2686
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2660
- ...themeAttrs(cssVarPrefix, activeTheme),
2687
+ ...themeAttrs(cssVarPrefix, activeAxes),
2661
2688
  children: /* @__PURE__ */ jsx("div", {
2662
2689
  className: "sb-block__empty",
2663
2690
  children: "No strokeStyle tokens match this filter."
2664
2691
  })
2665
2692
  });
2666
2693
  return /* @__PURE__ */ jsxs("div", {
2667
- ...themeAttrs(cssVarPrefix, activeTheme),
2694
+ ...themeAttrs(cssVarPrefix, activeAxes),
2668
2695
  children: [/* @__PURE__ */ jsx("div", {
2669
2696
  className: "sb-block__caption",
2670
2697
  children: captionText
@@ -2701,7 +2728,7 @@ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }
2701
2728
  //#region src/token-detail/internal.ts
2702
2729
  function useTokenDetailData(path) {
2703
2730
  const project = useProject();
2704
- const { activeTheme, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt, themeNameForTuple } = project;
2731
+ const { activeTheme, activeAxes, axes, resolved, cssVarPrefix, varianceByPath, resolveAt } = project;
2705
2732
  const typedResolved = resolved;
2706
2733
  return {
2707
2734
  token: typedResolved[path],
@@ -2712,8 +2739,7 @@ function useTokenDetailData(path) {
2712
2739
  resolved: typedResolved,
2713
2740
  cssVarPrefix,
2714
2741
  varianceByPath,
2715
- resolveAt,
2716
- themeNameForTuple
2742
+ resolveAt
2717
2743
  };
2718
2744
  }
2719
2745
  //#endregion
@@ -2838,7 +2864,7 @@ function treeHasTruncation(nodes) {
2838
2864
  //#endregion
2839
2865
  //#region src/token-detail/AxisVariance.tsx
2840
2866
  function AxisVariance({ path }) {
2841
- const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt, themeNameForTuple } = useTokenDetailData(path);
2867
+ const { token, cssVar, axes, activeAxes, cssVarPrefix, varianceByPath, resolveAt } = useTokenDetailData(path);
2842
2868
  const colorFormat = useColorFormat();
2843
2869
  const tokenType = token?.$type;
2844
2870
  const isColor = tokenType === "color";
@@ -2857,6 +2883,7 @@ function AxisVariance({ path }) {
2857
2883
  kind: "multi-axis",
2858
2884
  varyingAxes: result.varyingAxes
2859
2885
  };
2886
+ default: throw new Error(`unhandled AxisVarianceResult kind: ${JSON.stringify(result)}`);
2860
2887
  }
2861
2888
  }, [path, varianceByPath]);
2862
2889
  if (axes.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
@@ -2903,7 +2930,7 @@ function AxisVariance({ path }) {
2903
2930
  };
2904
2931
  return {
2905
2932
  ctx,
2906
- themeName: themeNameForTuple(target) ?? "",
2933
+ target,
2907
2934
  value: formatFn(resolveAt(target)[path])
2908
2935
  };
2909
2936
  });
@@ -2923,10 +2950,10 @@ function AxisVariance({ path }) {
2923
2950
  children: row.ctx
2924
2951
  }), /* @__PURE__ */ jsxs("td", {
2925
2952
  className: "sb-token-detail__theme-cell",
2926
- children: [isColor && row.themeName && /* @__PURE__ */ jsx("span", {
2953
+ children: [isColor && /* @__PURE__ */ jsx("span", {
2927
2954
  className: "sb-token-detail__swatch",
2928
2955
  style: { background: cssVar },
2929
- [dataAttr(cssVarPrefix, "theme")]: row.themeName,
2956
+ ...perAxisAttrs(cssVarPrefix, row.target),
2930
2957
  "aria-hidden": true
2931
2958
  }), row.value]
2932
2959
  })]
@@ -2975,16 +3002,15 @@ function AxisVariance({ path }) {
2975
3002
  [rowAxis.name]: row,
2976
3003
  [colAxis.name]: col
2977
3004
  };
2978
- const name = themeNameForTuple(target);
2979
3005
  const value = formatFn(resolveAt(target)[path]);
2980
3006
  return /* @__PURE__ */ jsxs("td", {
2981
3007
  className: "sb-token-detail__theme-cell",
2982
3008
  "data-row": row,
2983
3009
  "data-col": col,
2984
- children: [isColor && name && /* @__PURE__ */ jsx("span", {
3010
+ children: [isColor && /* @__PURE__ */ jsx("span", {
2985
3011
  className: "sb-token-detail__swatch",
2986
3012
  style: { background: cssVar },
2987
- [dataAttr(cssVarPrefix, "theme")]: name,
3013
+ ...perAxisAttrs(cssVarPrefix, target),
2988
3014
  "aria-hidden": true
2989
3015
  }), value]
2990
3016
  }, col);
@@ -3564,9 +3590,9 @@ function TokenUsageSnippet({ path }) {
3564
3590
  //#endregion
3565
3591
  //#region src/TokenDetail.tsx
3566
3592
  function TokenDetail({ path, heading }) {
3567
- const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
3593
+ const { token, cssVar, activeTheme, activeAxes, cssVarPrefix } = useTokenDetailData(path);
3568
3594
  const colorFormat = useColorFormat();
3569
- const theme = themeAttrs(cssVarPrefix, activeTheme);
3595
+ const theme = themeAttrs(cssVarPrefix, activeAxes);
3570
3596
  if (!token) return /* @__PURE__ */ jsx("div", {
3571
3597
  ...theme,
3572
3598
  className: cx(theme["className"], "sb-token-detail"),
@@ -3649,9 +3675,23 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3649
3675
  const panelRef = useRef(null);
3650
3676
  const openerRef = useRef(null);
3651
3677
  useEffect(() => {
3678
+ const panel = panelRef.current;
3679
+ if (!panel) return;
3652
3680
  openerRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3653
- panelRef.current?.focus();
3681
+ panel.focus();
3682
+ const overlayBranch = findBodyChildContaining(panel);
3683
+ const restorers = [];
3684
+ if (overlayBranch) for (const sibling of Array.from(document.body.children)) {
3685
+ if (sibling === overlayBranch) continue;
3686
+ if (!(sibling instanceof HTMLElement)) continue;
3687
+ const hadInert = sibling.inert;
3688
+ sibling.inert = true;
3689
+ restorers.push(() => {
3690
+ sibling.inert = hadInert;
3691
+ });
3692
+ }
3654
3693
  return () => {
3694
+ for (const restore of restorers) restore();
3655
3695
  openerRef.current?.focus();
3656
3696
  };
3657
3697
  }, []);
@@ -3715,6 +3755,17 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3715
3755
  })
3716
3756
  });
3717
3757
  }
3758
+ /**
3759
+ * Walk up from `node` to the direct child of `<body>` that contains it.
3760
+ * Returns `null` when the node isn't attached to the document (mid-mount,
3761
+ * post-unmount). Used to identify which top-level branch to *not* mark
3762
+ * inert when the overlay opens.
3763
+ */
3764
+ function findBodyChildContaining(node) {
3765
+ let cursor = node;
3766
+ while (cursor && cursor.parentElement !== document.body) cursor = cursor.parentElement;
3767
+ return cursor;
3768
+ }
3718
3769
  //#endregion
3719
3770
  //#region src/TokenNavigator.tsx
3720
3771
  function buildTree(resolved, root, typeFilter) {
@@ -3825,7 +3876,7 @@ function pruneTreeForMatches(nodes, matches, expandOut) {
3825
3876
  return out;
3826
3877
  }
3827
3878
  function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect }) {
3828
- const { resolved, activeTheme, cssVarPrefix } = useProject();
3879
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
3829
3880
  const typeFilter = useMemo(() => {
3830
3881
  if (type === void 0) return void 0;
3831
3882
  return new Set(Array.isArray(type) ? type : [type]);
@@ -4010,11 +4061,11 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4010
4061
  return n;
4011
4062
  }, [visibleTree, searchExpanded]);
4012
4063
  if (tree.length === 0) return /* @__PURE__ */ jsx("div", {
4013
- ...themeAttrs(cssVarPrefix, activeTheme),
4064
+ ...themeAttrs(cssVarPrefix, activeAxes),
4014
4065
  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
4066
  });
4016
4067
  return /* @__PURE__ */ jsxs("div", {
4017
- ...themeAttrs(cssVarPrefix, activeTheme),
4068
+ ...themeAttrs(cssVarPrefix, activeAxes),
4018
4069
  children: [
4019
4070
  searchable && /* @__PURE__ */ jsx("div", {
4020
4071
  className: "sb-token-navigator__search",
@@ -4038,6 +4089,12 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4038
4089
  activeTheme
4039
4090
  ]
4040
4091
  }),
4092
+ searchable && /* @__PURE__ */ jsx("span", {
4093
+ role: "status",
4094
+ "aria-live": "polite",
4095
+ className: "sb-token-navigator__sr-status",
4096
+ children: trimmedQuery !== "" ? `${matchCount} tokens matching "${trimmedQuery}"` : ""
4097
+ }),
4041
4098
  visibleTree.length === 0 ? /* @__PURE__ */ jsxs("div", {
4042
4099
  className: "sb-block__empty",
4043
4100
  children: [
@@ -4050,14 +4107,17 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4050
4107
  role: "tree",
4051
4108
  "aria-label": "Token graph",
4052
4109
  onKeyDown: handleTreeKeyDown,
4053
- children: visibleTree.map((node) => /* @__PURE__ */ jsx(TreeNodeRow, {
4110
+ children: visibleTree.map((node, i) => /* @__PURE__ */ jsx(TreeNodeRow, {
4054
4111
  node,
4055
4112
  expanded: effectiveExpanded,
4056
4113
  focusedPath,
4057
4114
  registerTreeItem,
4058
4115
  onToggle: toggle,
4059
4116
  onFocusPath: setFocusedPath,
4060
- onLeafClick: handleLeafClick
4117
+ onLeafClick: handleLeafClick,
4118
+ level: 1,
4119
+ setsize: visibleTree.length,
4120
+ posinset: i + 1
4061
4121
  }, node.path || node.segment))
4062
4122
  }),
4063
4123
  selectedPath !== null && /* @__PURE__ */ jsx(DetailOverlay, {
@@ -4068,13 +4128,16 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4068
4128
  ]
4069
4129
  });
4070
4130
  }
4071
- function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick }) {
4131
+ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, level, setsize, posinset }) {
4072
4132
  if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
4073
4133
  node,
4074
4134
  focusedPath,
4075
4135
  registerTreeItem,
4076
4136
  onFocusPath,
4077
- onLeafClick
4137
+ onLeafClick,
4138
+ level,
4139
+ setsize,
4140
+ posinset
4078
4141
  });
4079
4142
  const isOpen = expanded.has(node.path);
4080
4143
  const isFocused = focusedPath === node.path;
@@ -4082,6 +4145,9 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
4082
4145
  ref: registerTreeItem(node.path),
4083
4146
  role: "treeitem",
4084
4147
  "aria-expanded": isOpen,
4148
+ "aria-level": level,
4149
+ "aria-setsize": setsize,
4150
+ "aria-posinset": posinset,
4085
4151
  tabIndex: isFocused ? 0 : -1,
4086
4152
  onFocus: () => onFocusPath(node.path),
4087
4153
  "data-path": node.path,
@@ -4108,24 +4174,30 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
4108
4174
  }), isOpen && /* @__PURE__ */ jsx("ul", {
4109
4175
  className: "sb-token-navigator__nested",
4110
4176
  role: "group",
4111
- children: node.children.map((c) => /* @__PURE__ */ jsx(TreeNodeRow, {
4177
+ children: node.children.map((c, i) => /* @__PURE__ */ jsx(TreeNodeRow, {
4112
4178
  node: c,
4113
4179
  expanded,
4114
4180
  focusedPath,
4115
4181
  registerTreeItem,
4116
4182
  onToggle,
4117
4183
  onFocusPath,
4118
- onLeafClick
4184
+ onLeafClick,
4185
+ level: level + 1,
4186
+ setsize: node.children.length,
4187
+ posinset: i + 1
4119
4188
  }, c.path || c.segment))
4120
4189
  })]
4121
4190
  });
4122
4191
  }
4123
- function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick }) {
4192
+ function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick, level, setsize, posinset }) {
4124
4193
  const type = node.token.$type ?? "";
4125
4194
  const isFocused = focusedPath === node.path;
4126
4195
  return /* @__PURE__ */ jsx("li", {
4127
4196
  ref: registerTreeItem(node.path),
4128
4197
  role: "treeitem",
4198
+ "aria-level": level,
4199
+ "aria-setsize": setsize,
4200
+ "aria-posinset": posinset,
4129
4201
  tabIndex: isFocused ? 0 : -1,
4130
4202
  onFocus: () => onFocusPath(node.path),
4131
4203
  "data-path": node.path,
@@ -4223,7 +4295,7 @@ function LeafPreview({ path, token }) {
4223
4295
  //#region src/TokenTable.tsx
4224
4296
  function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
4225
4297
  const project = useProject();
4226
- const { resolved, activeTheme, cssVarPrefix } = project;
4298
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
4227
4299
  const colorFormat = useColorFormat();
4228
4300
  const [selectedPath, setSelectedPath] = useState(null);
4229
4301
  const [query, setQuery] = useState("");
@@ -4271,14 +4343,14 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4271
4343
  const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
4272
4344
  const captionText = caption ?? `${rows.length} token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${type ? ` · $type=${type}` : ""}${matchSuffix} · ${activeTheme}`;
4273
4345
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4274
- ...themeAttrs(cssVarPrefix, activeTheme),
4346
+ ...themeAttrs(cssVarPrefix, activeAxes),
4275
4347
  children: /* @__PURE__ */ jsx("div", {
4276
4348
  className: "sb-block__empty",
4277
4349
  children: "No tokens match this filter."
4278
4350
  })
4279
4351
  });
4280
4352
  return /* @__PURE__ */ jsxs("div", {
4281
- ...themeAttrs(cssVarPrefix, activeTheme),
4353
+ ...themeAttrs(cssVarPrefix, activeAxes),
4282
4354
  children: [
4283
4355
  searchable && /* @__PURE__ */ jsx("div", {
4284
4356
  className: "sb-token-table__search",
@@ -4292,6 +4364,12 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4292
4364
  "data-testid": "token-table-search"
4293
4365
  })
4294
4366
  }),
4367
+ searchable && /* @__PURE__ */ jsx("span", {
4368
+ role: "status",
4369
+ "aria-live": "polite",
4370
+ className: "sb-token-table__sr-status",
4371
+ children: query.trim() !== "" ? `${visibleRows.length} of ${rows.length} tokens match "${query.trim()}"` : ""
4372
+ }),
4295
4373
  /* @__PURE__ */ jsxs("table", {
4296
4374
  className: "sb-token-table__table",
4297
4375
  children: [
@@ -4324,6 +4402,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4324
4402
  }
4325
4403
  },
4326
4404
  tabIndex: 0,
4405
+ "aria-haspopup": "dialog",
4327
4406
  "aria-label": `Inspect ${row.path}`,
4328
4407
  "data-testid": "token-table-row",
4329
4408
  "data-path": row.path,
@@ -4418,7 +4497,7 @@ function buildRow(path, composite) {
4418
4497
  };
4419
4498
  }
4420
4499
  function TypographyScale({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
4421
- const { resolved, activeTheme, cssVarPrefix } = useProject();
4500
+ const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
4422
4501
  const rows = useMemo(() => {
4423
4502
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4424
4503
  if (token.$type !== "typography") return false;
@@ -4443,14 +4522,14 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
4443
4522
  ]);
4444
4523
  const captionText = caption ?? `${rows.length} typography token${rows.length === 1 ? "" : "s"}${filter && filter !== "typography" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
4445
4524
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
4446
- ...themeAttrs(cssVarPrefix, activeTheme),
4525
+ ...themeAttrs(cssVarPrefix, activeAxes),
4447
4526
  children: /* @__PURE__ */ jsx("div", {
4448
4527
  className: "sb-block__empty",
4449
4528
  children: "No typography tokens match this filter."
4450
4529
  })
4451
4530
  });
4452
4531
  return /* @__PURE__ */ jsxs("div", {
4453
- ...themeAttrs(cssVarPrefix, activeTheme),
4532
+ ...themeAttrs(cssVarPrefix, activeAxes),
4454
4533
  children: [/* @__PURE__ */ jsx("div", {
4455
4534
  className: "sb-block__caption",
4456
4535
  children: captionText