@unpunnyfuns/swatchbook-blocks 0.16.0 → 0.18.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
@@ -1,9 +1,10 @@
1
1
  import './style.css';
2
2
  import Color from "colorjs.io";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Fragment, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
5
+ import { makeCSSVar } from "@terrazzo/token-tools/css";
5
6
  import { addons } from "storybook/preview-api";
6
- import { axes, css, cssVarPrefix, defaultTheme, diagnostics, presets, themes, themesResolved } from "virtual:swatchbook/tokens";
7
+ import { axes, css, cssVarPrefix, defaultTheme, diagnostics, listing, presets, themes, themesResolved } from "virtual:swatchbook/tokens";
7
8
  import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
8
9
  import cx from "clsx";
9
10
  import { analyzeAxisVariance } from "@unpunnyfuns/swatchbook-core/variance";
@@ -355,6 +356,7 @@ let snapshot = {
355
356
  diagnostics,
356
357
  css,
357
358
  cssVarPrefix,
359
+ listing: listing ?? {},
358
360
  version: 0
359
361
  };
360
362
  const listeners = /* @__PURE__ */ new Set();
@@ -372,6 +374,7 @@ function ensureSubscribed() {
372
374
  diagnostics: payload.diagnostics ?? snapshot.diagnostics,
373
375
  css: payload.css ?? snapshot.css,
374
376
  cssVarPrefix: payload.cssVarPrefix ?? snapshot.cssVarPrefix,
377
+ listing: payload.listing ?? snapshot.listing,
375
378
  version: snapshot.version + 1
376
379
  };
377
380
  for (const cb of listeners) cb();
@@ -429,7 +432,8 @@ function snapshotToData(snapshot) {
429
432
  themesResolved: snapshot.themesResolved,
430
433
  resolved: snapshot.themesResolved[snapshot.activeTheme] ?? {},
431
434
  diagnostics: snapshot.diagnostics,
432
- cssVarPrefix: snapshot.cssVarPrefix
435
+ cssVarPrefix: snapshot.cssVarPrefix,
436
+ listing: snapshot.listing ?? {}
433
437
  };
434
438
  }
435
439
  /**
@@ -478,12 +482,56 @@ function useVirtualModuleFallback(enabled) {
478
482
  themesResolved: tokens.themesResolved,
479
483
  resolved: tokens.themesResolved[activeTheme] ?? {},
480
484
  diagnostics: tokens.diagnostics,
481
- cssVarPrefix: tokens.cssVarPrefix
485
+ cssVarPrefix: tokens.cssVarPrefix,
486
+ listing: tokens.listing
482
487
  };
483
488
  }
489
+ /**
490
+ * Thin wrapper around Terrazzo's `makeCSSVar` so the block-display surface
491
+ * and `packages/core/src/css.ts`'s emitter share one implementation. Any
492
+ * future naming-policy shift in Terrazzo (casing, unicode, prefix handling)
493
+ * reaches both surfaces at once instead of needing a parallel update here.
494
+ */
484
495
  function makeCssVar(path, prefix) {
485
- const tail = path.split(".").map((segment) => segment.replaceAll(/([a-z\d])([A-Z])/g, "$1-$2").toLowerCase()).join("-");
486
- return prefix ? `var(--${prefix}-${tail})` : `var(--${tail})`;
496
+ return prefix ? makeCSSVar(path, {
497
+ prefix,
498
+ wrapVar: true
499
+ }) : makeCSSVar(path, { wrapVar: true });
500
+ }
501
+ /**
502
+ * Resolve a token's CSS var reference, preferring the authoritative name
503
+ * emitted by `@terrazzo/plugin-css` (as recorded by
504
+ * `@terrazzo/plugin-token-listing` in the snapshot's `listing` field).
505
+ * Falls back to `makeCssVar` when the listing lacks an entry for this
506
+ * path — covers non-resolver projects, hand-built snapshots, and any
507
+ * listing-plugin miss.
508
+ */
509
+ function resolveCssVar(path, project) {
510
+ const listed = project.listing[path]?.names?.["css"];
511
+ if (listed) return `var(${listed})`;
512
+ return makeCssVar(path, project.cssVarPrefix);
513
+ }
514
+ /**
515
+ * Resolve a color value's display string + gamut flag, preferring the
516
+ * listing's `previewValue` when the user's active color-format matches
517
+ * plugin-css's output (hex). For any other format we fall back to
518
+ * `formatColor` so the toolbar's inspection modes (rgb / hsl / oklch /
519
+ * raw) keep working — the listing has only one canonical format.
520
+ *
521
+ * Pass `path === undefined` when resolving a sub-color inside a composite
522
+ * (shadow / border / gradient stop): composites' `previewValue` covers
523
+ * the whole token's rendering, not the individual channel, so there's no
524
+ * listing entry to key against.
525
+ */
526
+ function resolveColorValue(path, raw, colorFormat, project) {
527
+ if (path !== void 0 && colorFormat === "hex") {
528
+ const listed = project.listing[path]?.previewValue;
529
+ if (typeof listed === "string") return {
530
+ value: listed,
531
+ outOfGamut: false
532
+ };
533
+ }
534
+ return formatColor(raw, colorFormat);
487
535
  }
488
536
  /**
489
537
  * Match a dot-separated DTCG token path against a block `filter` prop.
@@ -519,8 +567,7 @@ const sampleStyle$1 = {
519
567
  borderRadius: 6
520
568
  };
521
569
  function BorderSample({ path }) {
522
- const { cssVarPrefix } = useProject();
523
- const cssVar = makeCssVar(path, cssVarPrefix);
570
+ const cssVar = resolveCssVar(path, useProject());
524
571
  return /* @__PURE__ */ jsx("div", {
525
572
  style: {
526
573
  ...sampleStyle$1,
@@ -694,7 +741,8 @@ function formatSubColor$1(raw, format) {
694
741
  return formatColor(raw, format).value;
695
742
  }
696
743
  function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
697
- const { resolved, activeTheme, cssVarPrefix } = useProject();
744
+ const project = useProject();
745
+ const { resolved, activeTheme, cssVarPrefix } = project;
698
746
  const colorFormat = useColorFormat();
699
747
  const rows = useMemo(() => {
700
748
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -705,13 +753,13 @@ function BorderPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
705
753
  dir: sortDir
706
754
  }).map(([path, token]) => ({
707
755
  path,
708
- cssVar: makeCssVar(path, cssVarPrefix),
756
+ cssVar: resolveCssVar(path, project),
709
757
  value: token.$value ?? {}
710
758
  }));
711
759
  }, [
712
760
  resolved,
713
761
  filter,
714
- cssVarPrefix,
762
+ project,
715
763
  sortBy,
716
764
  sortDir
717
765
  ]);
@@ -786,7 +834,8 @@ function fixedPrefixLength(filter) {
786
834
  return fixed;
787
835
  }
788
836
  function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
789
- const { resolved, activeTheme, cssVarPrefix } = useProject();
837
+ const project = useProject();
838
+ const { resolved, activeTheme, cssVarPrefix } = project;
790
839
  const colorFormat = useColorFormat();
791
840
  const groups = useMemo(() => {
792
841
  const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
@@ -804,11 +853,11 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
804
853
  const groupKey = segments.slice(0, effectiveGroupBy).join(".");
805
854
  const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
806
855
  const list = bucket.get(groupKey) ?? [];
807
- const formatted = formatColor(token.$value, colorFormat);
856
+ const formatted = resolveColorValue(path, token.$value, colorFormat, project);
808
857
  list.push({
809
858
  path,
810
859
  leaf,
811
- cssVar: makeCssVar(path, cssVarPrefix),
860
+ cssVar: resolveCssVar(path, project),
812
861
  value: formatted.value,
813
862
  outOfGamut: formatted.outOfGamut
814
863
  });
@@ -819,7 +868,7 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
819
868
  resolved,
820
869
  filter,
821
870
  groupBy,
822
- cssVarPrefix,
871
+ project,
823
872
  colorFormat,
824
873
  sortBy,
825
874
  sortDir
@@ -920,70 +969,701 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
920
969
  });
921
970
  }
922
971
  //#endregion
923
- //#region src/internal/format-token-value.ts
972
+ //#region src/ColorTable.tsx
973
+ const BASE_LABEL = "base";
974
+ const COLUMN_COUNT = 6;
975
+ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
976
+ const project = useProject();
977
+ const { resolved, activeTheme, cssVarPrefix } = project;
978
+ const colorFormat = useColorFormat();
979
+ const [query, setQuery] = useState("");
980
+ const [selectedByBase, setSelectedByBase] = useState({});
981
+ const [expandedByBase, setExpandedByBase] = useState(() => /* @__PURE__ */ new Set());
982
+ const defs = useMemo(() => buildVariantDefs(variants), [variants]);
983
+ const groups = useMemo(() => {
984
+ const sorted = sortTokens(Object.entries(resolved).filter(([path, token]) => {
985
+ if (token.$type !== "color") return false;
986
+ return globMatch(path, filter);
987
+ }), {
988
+ by: sortBy,
989
+ dir: sortDir
990
+ });
991
+ const groupMap = /* @__PURE__ */ new Map();
992
+ for (const [path, token] of sorted) {
993
+ const raw = token.$value;
994
+ const hex = resolveColorValue(path, raw, "hex", project);
995
+ const hsl = formatColor(raw, "hsl");
996
+ const oklch = formatColor(raw, "oklch");
997
+ const active = pickActiveFormat(raw, colorFormat, hex, hsl, oklch);
998
+ const match = matchVariant(path, defs.matchOrder);
999
+ const variant = {
1000
+ label: match?.label ?? BASE_LABEL,
1001
+ path,
1002
+ cssVar: resolveCssVar(path, project),
1003
+ value: active.value,
1004
+ outOfGamut: active.outOfGamut,
1005
+ hex: hex.value,
1006
+ hsl: hsl.value,
1007
+ oklch: oklch.value,
1008
+ ...token.$description !== void 0 && { description: token.$description },
1009
+ ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
1010
+ ...token.aliasChain !== void 0 && { aliasChain: token.aliasChain }
1011
+ };
1012
+ const basePath = match?.basePath ?? path;
1013
+ const existing = groupMap.get(basePath);
1014
+ if (existing) existing.variants.push(variant);
1015
+ else groupMap.set(basePath, {
1016
+ base: basePath,
1017
+ variants: [variant]
1018
+ });
1019
+ }
1020
+ const out = [];
1021
+ for (const { base, variants: vs } of groupMap.values()) {
1022
+ vs.sort((a, b) => orderIndex(a.label, defs) - orderIndex(b.label, defs));
1023
+ const searchText = vs.map((v) => `${v.path} ${v.value}`).join(" ");
1024
+ out.push({
1025
+ base,
1026
+ variants: vs,
1027
+ searchText
1028
+ });
1029
+ }
1030
+ return out;
1031
+ }, [
1032
+ resolved,
1033
+ filter,
1034
+ project,
1035
+ sortBy,
1036
+ sortDir,
1037
+ defs,
1038
+ colorFormat
1039
+ ]);
1040
+ const visibleGroups = useMemo(() => {
1041
+ if (!searchable || query.trim() === "") return groups;
1042
+ return fuzzyFilter(groups, query, (g) => g.searchText);
1043
+ }, [
1044
+ groups,
1045
+ query,
1046
+ searchable
1047
+ ]);
1048
+ const totalTokens = useMemo(() => groups.reduce((n, g) => n + g.variants.length, 0), [groups]);
1049
+ const toggleExpand = useCallback((base) => {
1050
+ setExpandedByBase((prev) => {
1051
+ const next = new Set(prev);
1052
+ if (next.has(base)) next.delete(base);
1053
+ else next.add(base);
1054
+ return next;
1055
+ });
1056
+ }, []);
1057
+ const selectVariant = useCallback((base, label) => {
1058
+ setSelectedByBase((prev) => ({
1059
+ ...prev,
1060
+ [base]: label
1061
+ }));
1062
+ }, []);
1063
+ const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleGroups.length} matching "${query.trim()}"` : "";
1064
+ const captionText = caption ?? `${totalTokens} color${totalTokens === 1 ? "" : "s"} across ${groups.length} group${groups.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
1065
+ if (groups.length === 0) return /* @__PURE__ */ jsx("div", {
1066
+ ...themeAttrs(cssVarPrefix, activeTheme),
1067
+ children: /* @__PURE__ */ jsx("div", {
1068
+ className: "sb-block__empty",
1069
+ children: "No color tokens match this filter."
1070
+ })
1071
+ });
1072
+ return /* @__PURE__ */ jsxs("div", {
1073
+ ...themeAttrs(cssVarPrefix, activeTheme),
1074
+ children: [searchable && /* @__PURE__ */ jsx("div", {
1075
+ className: "sb-color-table__search",
1076
+ children: /* @__PURE__ */ jsx("input", {
1077
+ type: "search",
1078
+ className: "sb-color-table__search-input",
1079
+ placeholder: "Search colors…",
1080
+ value: query,
1081
+ onChange: (e) => setQuery(e.target.value),
1082
+ "aria-label": "Fuzzy-search colors by path or value",
1083
+ "data-testid": "color-table-search"
1084
+ })
1085
+ }), /* @__PURE__ */ jsxs("table", {
1086
+ className: "sb-color-table__table",
1087
+ children: [
1088
+ /* @__PURE__ */ jsx("caption", {
1089
+ className: "sb-color-table__caption",
1090
+ children: captionText
1091
+ }),
1092
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1093
+ /* @__PURE__ */ jsx("th", {
1094
+ className: "sb-color-table__th sb-color-table__th--swatch",
1095
+ children: /* @__PURE__ */ jsx("span", {
1096
+ className: "sb-color-table__sr-only",
1097
+ children: "Swatch"
1098
+ })
1099
+ }),
1100
+ /* @__PURE__ */ jsx("th", {
1101
+ className: "sb-color-table__th sb-color-table__th--path",
1102
+ children: "Name"
1103
+ }),
1104
+ /* @__PURE__ */ jsx("th", {
1105
+ className: "sb-color-table__th",
1106
+ children: "Value"
1107
+ }),
1108
+ /* @__PURE__ */ jsx("th", {
1109
+ className: "sb-color-table__th",
1110
+ children: "CSS var"
1111
+ }),
1112
+ /* @__PURE__ */ jsx("th", {
1113
+ className: "sb-color-table__th",
1114
+ children: "Alias"
1115
+ }),
1116
+ /* @__PURE__ */ jsx("th", {
1117
+ className: "sb-color-table__th sb-color-table__th--expand",
1118
+ children: /* @__PURE__ */ jsx("span", {
1119
+ className: "sb-color-table__sr-only",
1120
+ children: "Expand"
1121
+ })
1122
+ })
1123
+ ] }) }),
1124
+ /* @__PURE__ */ jsxs("tbody", { children: [visibleGroups.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
1125
+ colSpan: COLUMN_COUNT,
1126
+ className: "sb-color-table__td sb-color-table__empty-row",
1127
+ children: [
1128
+ "No colors match \"",
1129
+ query.trim(),
1130
+ "\"."
1131
+ ]
1132
+ }) }), visibleGroups.map((group) => /* @__PURE__ */ jsx(GroupRow, {
1133
+ group,
1134
+ selectedLabel: selectedByBase[group.base],
1135
+ expanded: expandedByBase.has(group.base),
1136
+ onToggleExpand: toggleExpand,
1137
+ onSelectVariant: selectVariant,
1138
+ ...onSelect !== void 0 && { onSelect }
1139
+ }, group.base))] })
1140
+ ]
1141
+ })]
1142
+ });
1143
+ }
1144
+ function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect }) {
1145
+ const multi = group.variants.length > 1;
1146
+ const active = group.variants.find((v) => v.label === selectedLabel) ?? group.variants[0];
1147
+ const nameText = multi ? group.base : active.path;
1148
+ const handleRowActivate = () => {
1149
+ if (onSelect) onSelect(active.path);
1150
+ else onToggleExpand(group.base);
1151
+ };
1152
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("tr", {
1153
+ className: cx("sb-color-table__row", { "sb-color-table__row--expanded": expanded }),
1154
+ onClick: handleRowActivate,
1155
+ onKeyDown: (e) => {
1156
+ if (e.key === "Enter" || e.key === " ") {
1157
+ e.preventDefault();
1158
+ handleRowActivate();
1159
+ }
1160
+ },
1161
+ tabIndex: 0,
1162
+ "aria-label": onSelect ? `Inspect ${active.path}` : expanded ? `Collapse ${group.base}` : `Expand ${group.base}`,
1163
+ "data-testid": "color-table-row",
1164
+ "data-path": active.path,
1165
+ "data-base": group.base,
1166
+ children: [
1167
+ /* @__PURE__ */ jsx("td", {
1168
+ className: "sb-color-table__td sb-color-table__swatch-cell",
1169
+ children: /* @__PURE__ */ jsx("span", {
1170
+ className: "sb-color-table__swatch",
1171
+ style: { background: active.cssVar },
1172
+ "aria-hidden": true
1173
+ })
1174
+ }),
1175
+ /* @__PURE__ */ jsxs("td", {
1176
+ className: cx("sb-color-table__td", "sb-color-table__path"),
1177
+ children: [/* @__PURE__ */ jsx("span", {
1178
+ className: "sb-color-table__path-text",
1179
+ children: nameText
1180
+ }), multi && /* @__PURE__ */ jsx("span", {
1181
+ className: "sb-color-table__variant-pills",
1182
+ onClick: (e) => e.stopPropagation(),
1183
+ onKeyDown: (e) => e.stopPropagation(),
1184
+ role: "presentation",
1185
+ children: group.variants.map((v) => /* @__PURE__ */ jsx("button", {
1186
+ type: "button",
1187
+ className: cx("sb-color-table__variant-pill", { "sb-color-table__variant-pill--active": v.label === active.label }),
1188
+ onClick: () => onSelectVariant(group.base, v.label),
1189
+ "aria-pressed": v.label === active.label,
1190
+ "data-testid": "color-table-variant",
1191
+ "data-variant": v.label,
1192
+ children: v.label
1193
+ }, v.label))
1194
+ })]
1195
+ }),
1196
+ /* @__PURE__ */ jsx(ValueCell, {
1197
+ value: active.value,
1198
+ label: `Copy value ${active.value}`,
1199
+ children: active.outOfGamut && /* @__PURE__ */ jsx("span", {
1200
+ title: "Out of sRGB gamut for this format",
1201
+ "aria-label": "out of gamut",
1202
+ className: "sb-color-table__gamut-warn",
1203
+ children: "⚠"
1204
+ })
1205
+ }),
1206
+ /* @__PURE__ */ jsx(ValueCell, {
1207
+ value: active.cssVar,
1208
+ label: `Copy CSS var ${active.cssVar}`
1209
+ }),
1210
+ /* @__PURE__ */ jsx("td", {
1211
+ className: "sb-color-table__td sb-color-table__alias",
1212
+ children: active.aliasOf ? /* @__PURE__ */ jsx("span", {
1213
+ className: "sb-color-table__alias-text",
1214
+ children: active.aliasOf
1215
+ }) : /* @__PURE__ */ jsx("span", {
1216
+ className: "sb-color-table__alias-empty",
1217
+ "aria-hidden": true,
1218
+ children: "—"
1219
+ })
1220
+ }),
1221
+ /* @__PURE__ */ jsx("td", {
1222
+ className: "sb-color-table__td sb-color-table__expand-cell",
1223
+ children: !onSelect && /* @__PURE__ */ jsx("span", {
1224
+ className: cx("sb-color-table__chevron", { "sb-color-table__chevron--expanded": expanded }),
1225
+ "aria-hidden": true,
1226
+ children: "▸"
1227
+ })
1228
+ })
1229
+ ]
1230
+ }), expanded && !onSelect && /* @__PURE__ */ jsx("tr", {
1231
+ className: "sb-color-table__detail-row",
1232
+ "data-testid": "color-table-detail",
1233
+ children: /* @__PURE__ */ jsx("td", {
1234
+ colSpan: COLUMN_COUNT,
1235
+ className: "sb-color-table__td sb-color-table__detail-cell",
1236
+ children: /* @__PURE__ */ jsx(ExpandedDetail, {
1237
+ group,
1238
+ active
1239
+ })
1240
+ })
1241
+ })] });
1242
+ }
1243
+ function ExpandedDetail({ group, active }) {
1244
+ const hasDescription = active.description !== void 0 && active.description.length > 0;
1245
+ const chain = active.aliasChain && active.aliasChain.length > 0 ? active.aliasChain : void 0;
1246
+ const multi = group.variants.length > 1;
1247
+ return /* @__PURE__ */ jsxs("div", {
1248
+ className: "sb-color-table__detail",
1249
+ children: [
1250
+ hasDescription && /* @__PURE__ */ jsx("p", {
1251
+ className: "sb-color-table__description",
1252
+ children: active.description
1253
+ }),
1254
+ chain && /* @__PURE__ */ jsxs("p", {
1255
+ className: "sb-color-table__chain",
1256
+ children: [
1257
+ /* @__PURE__ */ jsx("span", {
1258
+ className: "sb-color-table__detail-label",
1259
+ children: "Alias chain:"
1260
+ }),
1261
+ " ",
1262
+ chain.join(" → ")
1263
+ ]
1264
+ }),
1265
+ multi && /* @__PURE__ */ jsxs("div", {
1266
+ className: "sb-color-table__variant-grid",
1267
+ children: [/* @__PURE__ */ jsx("div", {
1268
+ className: "sb-color-table__variant-grid-header",
1269
+ children: "All variants"
1270
+ }), /* @__PURE__ */ jsxs("table", {
1271
+ className: "sb-color-table__subtable",
1272
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1273
+ /* @__PURE__ */ jsx("th", {
1274
+ className: "sb-color-table__subth",
1275
+ children: "Variant"
1276
+ }),
1277
+ /* @__PURE__ */ jsx("th", {
1278
+ className: "sb-color-table__subth",
1279
+ children: "Path"
1280
+ }),
1281
+ /* @__PURE__ */ jsx("th", {
1282
+ className: "sb-color-table__subth",
1283
+ children: "HEX"
1284
+ }),
1285
+ /* @__PURE__ */ jsx("th", {
1286
+ className: "sb-color-table__subth",
1287
+ children: "HSL"
1288
+ }),
1289
+ /* @__PURE__ */ jsx("th", {
1290
+ className: "sb-color-table__subth",
1291
+ children: "OKLCH"
1292
+ })
1293
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: group.variants.map((v) => /* @__PURE__ */ jsxs("tr", {
1294
+ className: cx({ "sb-color-table__subrow--active": v.label === active.label }),
1295
+ children: [
1296
+ /* @__PURE__ */ jsx("td", {
1297
+ className: "sb-color-table__subtd",
1298
+ children: v.label
1299
+ }),
1300
+ /* @__PURE__ */ jsx("td", {
1301
+ className: "sb-color-table__subtd sb-color-table__subtd--path",
1302
+ children: v.path
1303
+ }),
1304
+ /* @__PURE__ */ jsx("td", {
1305
+ className: "sb-color-table__subtd",
1306
+ children: v.hex
1307
+ }),
1308
+ /* @__PURE__ */ jsx("td", {
1309
+ className: "sb-color-table__subtd",
1310
+ children: v.hsl
1311
+ }),
1312
+ /* @__PURE__ */ jsx("td", {
1313
+ className: "sb-color-table__subtd",
1314
+ children: v.oklch
1315
+ })
1316
+ ]
1317
+ }, v.label)) })]
1318
+ })]
1319
+ }),
1320
+ !hasDescription && !chain && !multi && /* @__PURE__ */ jsx("p", {
1321
+ className: "sb-color-table__detail-empty",
1322
+ children: "No description or alias chain authored for this token."
1323
+ })
1324
+ ]
1325
+ });
1326
+ }
1327
+ function ValueCell({ value, label, children }) {
1328
+ return /* @__PURE__ */ jsxs("td", {
1329
+ className: "sb-color-table__td sb-color-table__value-cell",
1330
+ children: [
1331
+ /* @__PURE__ */ jsx("span", {
1332
+ className: "sb-color-table__value-text",
1333
+ title: value,
1334
+ children: value
1335
+ }),
1336
+ children,
1337
+ /* @__PURE__ */ jsx("span", {
1338
+ className: "sb-color-table__copy-wrap",
1339
+ onClick: (e) => e.stopPropagation(),
1340
+ onKeyDown: (e) => e.stopPropagation(),
1341
+ role: "presentation",
1342
+ children: /* @__PURE__ */ jsx(CopyButton$1, {
1343
+ value,
1344
+ label,
1345
+ className: "sb-color-table__copy"
1346
+ })
1347
+ })
1348
+ ]
1349
+ });
1350
+ }
924
1351
  /**
925
- * Produce a single-line display string for any DTCG token `$value`,
926
- * respecting the active color format for color-typed tokens and the
927
- * color sub-values of composite types (border, shadow, gradient).
928
- *
929
- * Shape by type:
930
- * - `color` → `formatColor(value, colorFormat)` — e.g. `#3b82f6`, `oklch(...)`, `raw` JSON.
931
- * - `dimension|duration` → `value + unit` — e.g. `16px`, `200ms`.
932
- * - `fontFamily` → string or array joined with `, `.
933
- * - `fontWeight` → primitive.
934
- * - `cubicBezier` → `cubic-bezier(a, b, c, d)`.
935
- * - `strokeStyle` → primitive string, or `dashed · dashArray · lineCap` when it's the object shape.
936
- * - `shadow` → one or more `offsetX offsetY blur spread color` layers joined with `, `.
937
- * - `border` → `width style color`.
938
- * - `transition` → `duration easing [delay]`.
939
- * - `typography` → `family / size / line-height / weight`.
940
- * - `gradient` → `stops joined with →` — compact representation, not a CSS gradient string (those live in GradientPalette's preview).
941
- *
942
- * Unknown object shapes fall through to truncated JSON.
1352
+ * Pick the value + gamut flag to display in the single Value column based
1353
+ * on the active color-format context. We pre-compute hex/hsl/oklch for the
1354
+ * expanded sub-table regardless; the extras (`rgb`, `raw`) take a fresh
1355
+ * `formatColor` pass. Keeps the hot path fast while staying honest about
1356
+ * out-of-gamut warnings per-format.
943
1357
  */
944
- function formatTokenValue(value, $type, colorFormat) {
945
- if (value == null) return "";
946
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
947
- switch ($type) {
948
- case "color": return formatColor(value, colorFormat).value;
949
- case "dimension":
950
- case "duration": return formatDimension$1(value);
951
- case "fontFamily": return formatFontFamily$1(value);
952
- case "fontWeight":
953
- case "lineHeight":
954
- case "letterSpacing":
955
- case "opacity":
956
- case "number": return formatPrimitive$1(value);
957
- case "cubicBezier": return formatCubicBezier(value);
958
- case "strokeStyle": return formatStrokeStyle(value);
959
- case "shadow": return formatShadow(value, colorFormat);
960
- case "border": return formatBorder(value, colorFormat);
961
- case "transition": return formatTransition(value);
962
- case "typography": return formatTypography(value);
963
- case "gradient": return formatGradient(value, colorFormat);
964
- default: return formatUnknown(value);
1358
+ function pickActiveFormat(raw, colorFormat, hex, hsl, oklch) {
1359
+ switch (colorFormat) {
1360
+ case "hex": return {
1361
+ value: hex.value,
1362
+ outOfGamut: hex.outOfGamut
1363
+ };
1364
+ case "hsl": return {
1365
+ value: hsl.value,
1366
+ outOfGamut: hsl.outOfGamut
1367
+ };
1368
+ case "oklch": return {
1369
+ value: oklch.value,
1370
+ outOfGamut: oklch.outOfGamut
1371
+ };
1372
+ default: {
1373
+ const extra = formatColor(raw, colorFormat);
1374
+ return {
1375
+ value: extra.value,
1376
+ outOfGamut: extra.outOfGamut
1377
+ };
1378
+ }
965
1379
  }
966
1380
  }
967
- function formatDimension$1(v) {
968
- if (typeof v === "string" || typeof v === "number") return String(v);
969
- if (v && typeof v === "object") {
970
- const d = v;
971
- if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
1381
+ function buildVariantDefs(variants) {
1382
+ if (!variants) return {
1383
+ matchOrder: [],
1384
+ displayOrder: []
1385
+ };
1386
+ const entries = [];
1387
+ const displayOrder = [];
1388
+ for (const [label, suffix] of Object.entries(variants)) {
1389
+ if (suffix.length === 0) continue;
1390
+ entries.push({
1391
+ label,
1392
+ suffix
1393
+ });
1394
+ displayOrder.push(label);
972
1395
  }
973
- return formatUnknown(v);
974
- }
975
- function formatFontFamily$1(v) {
976
- if (typeof v === "string") return v;
977
- if (Array.isArray(v)) return v.map(String).join(", ");
978
- return formatUnknown(v);
1396
+ return {
1397
+ matchOrder: entries.toSorted((a, b) => b.suffix.length - a.suffix.length),
1398
+ displayOrder
1399
+ };
979
1400
  }
980
- function formatPrimitive$1(v) {
981
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
982
- return formatUnknown(v);
1401
+ /**
1402
+ * Position of a label within a group the `base` entry always sorts first,
1403
+ * then declared labels in the order the caller wrote them in the `variants`
1404
+ * prop. Unknown labels (shouldn't happen in practice) fall to the end.
1405
+ */
1406
+ function orderIndex(label, defs) {
1407
+ if (label === BASE_LABEL) return -1;
1408
+ const idx = defs.displayOrder.indexOf(label);
1409
+ return idx >= 0 ? idx : Number.POSITIVE_INFINITY;
983
1410
  }
984
- function formatCubicBezier(v) {
985
- if (Array.isArray(v) && v.length === 4) return `cubic-bezier(${v.map((n) => typeof n === "number" ? n : 0).join(", ")})`;
986
- return formatUnknown(v);
1411
+ /**
1412
+ * Resolve the variant label + base path for a token, if any. The leaf
1413
+ * (last dot-segment) must either equal the suffix outright (dot-segment
1414
+ * form: `hi.disabled` matches suffix `disabled`) or end in `-<suffix>`
1415
+ * (hyphen-tail form: `hi-d` matches `d`). The leading hyphen is required
1416
+ * for the tail form so suffix `0` can't hit `neutral-900` by character.
1417
+ *
1418
+ * Returned `basePath` is what gets used as the grouping key:
1419
+ * - Dot-segment match → parent path (drop the last dot-segment)
1420
+ * - Hyphen-tail match → same dot-depth, leaf with `-<suffix>` stripped
1421
+ *
1422
+ * Entries in `matchOrder` are pre-sorted longest-first, so `h-dark` wins
1423
+ * over `dark` for a path ending in `-h-dark`.
1424
+ */
1425
+ function matchVariant(path, matchOrder) {
1426
+ if (matchOrder.length === 0) return void 0;
1427
+ const segments = path.split(".");
1428
+ const leaf = segments.at(-1) ?? path;
1429
+ for (const entry of matchOrder) {
1430
+ if (leaf === entry.suffix) {
1431
+ const parent = segments.slice(0, -1).join(".");
1432
+ if (parent.length === 0) continue;
1433
+ return {
1434
+ label: entry.label,
1435
+ basePath: parent
1436
+ };
1437
+ }
1438
+ const tailMarker = `-${entry.suffix}`;
1439
+ if (leaf.endsWith(tailMarker)) {
1440
+ const trimmed = leaf.slice(0, -tailMarker.length);
1441
+ if (trimmed.length === 0) continue;
1442
+ const copy = segments.slice(0, -1);
1443
+ copy.push(trimmed);
1444
+ return {
1445
+ label: entry.label,
1446
+ basePath: copy.join(".")
1447
+ };
1448
+ }
1449
+ }
1450
+ }
1451
+ //#endregion
1452
+ //#region src/Diagnostics.tsx
1453
+ const severityLabel = {
1454
+ error: "ERROR",
1455
+ warn: "WARN",
1456
+ info: "INFO"
1457
+ };
1458
+ function summaryText(diagnostics) {
1459
+ if (diagnostics.length === 0) return "✔ OK · no diagnostics";
1460
+ const counts = {
1461
+ error: 0,
1462
+ warn: 0,
1463
+ info: 0
1464
+ };
1465
+ for (const d of diagnostics) counts[d.severity] += 1;
1466
+ const parts = [];
1467
+ if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
1468
+ if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
1469
+ if (counts.info > 0) parts.push(`${counts.info} info`);
1470
+ return parts.join(" · ");
1471
+ }
1472
+ function diagnosticKey(d, i) {
1473
+ return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
1474
+ }
1475
+ function summaryVariant(diagnostics) {
1476
+ if (diagnostics.length === 0) return "ok";
1477
+ if (diagnostics.some((d) => d.severity === "error")) return "error";
1478
+ if (diagnostics.some((d) => d.severity === "warn")) return "warn";
1479
+ return null;
1480
+ }
1481
+ /**
1482
+ * Render the project's load diagnostics — parser errors, resolver warnings,
1483
+ * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
1484
+ * when the project carries errors or warnings; stays collapsed for clean
1485
+ * loads and info-only loads.
1486
+ *
1487
+ * Replaces the diagnostics section from the addon's (now-retired) Design
1488
+ * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
1489
+ * on their own MDX pages.
1490
+ */
1491
+ function Diagnostics({ caption } = {}) {
1492
+ const { activeTheme, cssVarPrefix, diagnostics } = useProject();
1493
+ const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
1494
+ const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
1495
+ const variant = summaryVariant(diagnostics);
1496
+ return /* @__PURE__ */ jsx("div", {
1497
+ ...themeAttrs(cssVarPrefix, activeTheme),
1498
+ "data-testid": "diagnostics",
1499
+ children: /* @__PURE__ */ jsxs("details", {
1500
+ open: hasErrorsOrWarnings,
1501
+ children: [/* @__PURE__ */ jsx("summary", {
1502
+ className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
1503
+ children: headingText
1504
+ }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
1505
+ className: "sb-diagnostics__list",
1506
+ children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
1507
+ className: "sb-diagnostics__row",
1508
+ children: [/* @__PURE__ */ jsx("span", {
1509
+ className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
1510
+ children: severityLabel[d.severity]
1511
+ }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
1512
+ className: "sb-diagnostics__meta",
1513
+ children: [
1514
+ d.group,
1515
+ d.filename,
1516
+ d.line ? `:${d.line}` : ""
1517
+ ].filter(Boolean).join(" · ")
1518
+ })] })]
1519
+ }, diagnosticKey(d, i)))
1520
+ })]
1521
+ })
1522
+ });
1523
+ }
1524
+ //#endregion
1525
+ //#region src/dimension-scale/DimensionBar.tsx
1526
+ const MAX_RENDER_PX$1 = 480;
1527
+ const styles$1 = {
1528
+ bar: {
1529
+ height: 14,
1530
+ background: "var(--swatchbook-accent-bg, #3b82f6)",
1531
+ borderRadius: 2,
1532
+ minWidth: 1
1533
+ },
1534
+ radiusSample: {
1535
+ width: 56,
1536
+ height: 56,
1537
+ background: "var(--swatchbook-accent-bg, #3b82f6)",
1538
+ border: BORDER_STRONG
1539
+ },
1540
+ sizeSample: {
1541
+ background: "var(--swatchbook-accent-bg, #3b82f6)",
1542
+ border: BORDER_STRONG,
1543
+ minWidth: 1,
1544
+ minHeight: 1
1545
+ }
1546
+ };
1547
+ /**
1548
+ * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
1549
+ * purpose of deciding whether to cap the rendered bar. Returns `NaN` for
1550
+ * units we can't reasonably approximate (ex / ch / %), which the caller
1551
+ * treats as "render at cssVar but don't cap".
1552
+ */
1553
+ function toPixels$1(raw) {
1554
+ if (raw == null || typeof raw !== "object") return NaN;
1555
+ const v = raw;
1556
+ if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1557
+ switch (v.unit) {
1558
+ case "px": return v.value;
1559
+ case "rem":
1560
+ case "em": return v.value * 16;
1561
+ default: return NaN;
1562
+ }
1563
+ }
1564
+ function DimensionBar({ path, kind = "length" }) {
1565
+ const project = useProject();
1566
+ const { resolved } = project;
1567
+ const cssVar = resolveCssVar(path, project);
1568
+ const token = resolved[path];
1569
+ const pxValue = toPixels$1(token?.$value);
1570
+ const cappedValue = Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX$1 ? `${MAX_RENDER_PX$1}px` : cssVar;
1571
+ switch (kind) {
1572
+ case "radius": return /* @__PURE__ */ jsx("div", {
1573
+ style: {
1574
+ ...styles$1.radiusSample,
1575
+ borderRadius: cssVar
1576
+ },
1577
+ "aria-hidden": true
1578
+ });
1579
+ case "size": return /* @__PURE__ */ jsx("div", {
1580
+ style: {
1581
+ ...styles$1.sizeSample,
1582
+ width: cappedValue,
1583
+ height: cappedValue
1584
+ },
1585
+ "aria-hidden": true
1586
+ });
1587
+ default: return /* @__PURE__ */ jsx("div", {
1588
+ style: {
1589
+ ...styles$1.bar,
1590
+ width: cappedValue
1591
+ },
1592
+ "aria-hidden": true
1593
+ });
1594
+ }
1595
+ }
1596
+ //#endregion
1597
+ //#region src/internal/format-token-value.ts
1598
+ /**
1599
+ * Produce a single-line display string for any DTCG token `$value`,
1600
+ * respecting the active color format for color-typed tokens and the
1601
+ * color sub-values of composite types (border, shadow, gradient).
1602
+ *
1603
+ * Shape by type:
1604
+ * - `color` → `formatColor(value, colorFormat)` — e.g. `#3b82f6`, `oklch(...)`, `raw` JSON.
1605
+ * - `dimension|duration` → `value + unit` — e.g. `16px`, `200ms`.
1606
+ * - `fontFamily` → string or array joined with `, `.
1607
+ * - `fontWeight` → primitive.
1608
+ * - `cubicBezier` → `cubic-bezier(a, b, c, d)`.
1609
+ * - `strokeStyle` → primitive string, or `dashed · dashArray · lineCap` when it's the object shape.
1610
+ * - `shadow` → one or more `offsetX offsetY blur spread color` layers joined with `, `.
1611
+ * - `border` → `width style color`.
1612
+ * - `transition` → `duration easing [delay]`.
1613
+ * - `typography` → `family / size / line-height / weight`.
1614
+ * - `gradient` → `stops joined with →` — compact representation, not a CSS gradient string (those live in GradientPalette's preview).
1615
+ *
1616
+ * Unknown object shapes fall through to truncated JSON.
1617
+ */
1618
+ function formatTokenValue(value, $type, colorFormat, listingEntry) {
1619
+ if (value == null) return "";
1620
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
1621
+ const preview = listingEntry?.previewValue;
1622
+ if (preview !== void 0) {
1623
+ const previewStr = typeof preview === "string" ? preview : String(preview);
1624
+ if ($type !== "color") return previewStr;
1625
+ if (colorFormat === "hex") return previewStr;
1626
+ }
1627
+ switch ($type) {
1628
+ case "color": return formatColor(value, colorFormat).value;
1629
+ case "dimension":
1630
+ case "duration": return formatDimension$1(value);
1631
+ case "fontFamily": return formatFontFamily$1(value);
1632
+ case "fontWeight":
1633
+ case "lineHeight":
1634
+ case "letterSpacing":
1635
+ case "opacity":
1636
+ case "number": return formatPrimitive$1(value);
1637
+ case "cubicBezier": return formatCubicBezier(value);
1638
+ case "strokeStyle": return formatStrokeStyle(value);
1639
+ case "shadow": return formatShadow(value, colorFormat);
1640
+ case "border": return formatBorder(value, colorFormat);
1641
+ case "transition": return formatTransition(value);
1642
+ case "typography": return formatTypography(value);
1643
+ case "gradient": return formatGradient(value, colorFormat);
1644
+ default: return formatUnknown(value);
1645
+ }
1646
+ }
1647
+ function formatDimension$1(v) {
1648
+ if (typeof v === "string" || typeof v === "number") return String(v);
1649
+ if (v && typeof v === "object") {
1650
+ const d = v;
1651
+ if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
1652
+ }
1653
+ return formatUnknown(v);
1654
+ }
1655
+ function formatFontFamily$1(v) {
1656
+ if (typeof v === "string") return v;
1657
+ if (Array.isArray(v)) return v.map(String).join(", ");
1658
+ return formatUnknown(v);
1659
+ }
1660
+ function formatPrimitive$1(v) {
1661
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
1662
+ return formatUnknown(v);
1663
+ }
1664
+ function formatCubicBezier(v) {
1665
+ if (Array.isArray(v) && v.length === 4) return `cubic-bezier(${v.map((n) => typeof n === "number" ? n : 0).join(", ")})`;
1666
+ return formatUnknown(v);
987
1667
  }
988
1668
  function formatStrokeStyle(v) {
989
1669
  if (typeof v === "string") return v;
@@ -1059,574 +1739,332 @@ function formatUnknown(v) {
1059
1739
  }
1060
1740
  }
1061
1741
  //#endregion
1062
- //#region src/token-detail/internal.ts
1063
- function useTokenDetailData(path) {
1064
- const { activeTheme, activeAxes, axes, themes, themesResolved, resolved, cssVarPrefix } = useProject();
1065
- const typedResolved = resolved;
1066
- return {
1067
- token: typedResolved[path],
1068
- cssVar: makeCssVar(path, cssVarPrefix),
1069
- activeTheme,
1070
- activeAxes,
1071
- axes,
1072
- themes,
1073
- themesResolved,
1074
- resolved: typedResolved,
1075
- cssVarPrefix
1076
- };
1742
+ //#region src/DimensionScale.tsx
1743
+ const MAX_RENDER_PX = 480;
1744
+ function toPixels(raw) {
1745
+ if (raw == null || typeof raw !== "object") return NaN;
1746
+ const v = raw;
1747
+ if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1748
+ switch (v.unit) {
1749
+ case "px": return v.value;
1750
+ case "rem":
1751
+ case "em": return v.value * 16;
1752
+ default: return NaN;
1753
+ }
1077
1754
  }
1078
- //#endregion
1079
- //#region src/token-detail/AliasChain.tsx
1080
- function AliasChain({ path }) {
1081
- const { token } = useTokenDetailData(path);
1082
- const chain = useMemo(() => {
1083
- if (!token) return [];
1084
- if (Array.isArray(token.aliasChain) && token.aliasChain.length > 0) return [path, ...token.aliasChain];
1085
- if (typeof token.aliasOf === "string") return [path, token.aliasOf];
1086
- return [path];
1087
- }, [token, path]);
1088
- if (chain.length <= 1) return null;
1089
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
1090
- className: "sb-token-detail__section-header",
1091
- children: "Alias chain"
1092
- }), /* @__PURE__ */ jsx("div", {
1093
- className: "sb-token-detail__chain",
1094
- children: chain.map((step, i) => /* @__PURE__ */ jsxs("span", {
1095
- className: "sb-token-detail__chain",
1096
- children: [/* @__PURE__ */ jsx("span", {
1097
- className: "sb-token-detail__chain-node",
1098
- children: step
1099
- }), i < chain.length - 1 && /* @__PURE__ */ jsx("span", {
1100
- className: "sb-token-detail__arrow",
1101
- children: "→"
1102
- })]
1103
- }, step))
1104
- })] });
1105
- }
1106
- //#endregion
1107
- //#region src/token-detail/AliasedBy.tsx
1108
- const ALIASED_BY_DEPTH_CAP = 6;
1109
- const GROUP_RANK = {
1110
- ref: 0,
1111
- sys: 1
1112
- };
1113
- function AliasedBy({ path }) {
1114
- const { resolved } = useTokenDetailData(path);
1115
- const tree = useMemo(() => buildAliasedByTree(path, resolved), [path, resolved]);
1116
- const truncated = useMemo(() => treeHasTruncation(tree), [tree]);
1117
- if (tree.length === 0) return null;
1118
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1119
- /* @__PURE__ */ jsx("div", {
1120
- className: "sb-token-detail__section-header",
1121
- children: "Aliased by"
1122
- }),
1123
- /* @__PURE__ */ jsx("ul", {
1124
- className: "sb-token-detail__aliased-by-list",
1125
- children: tree.map((node) => /* @__PURE__ */ jsx(AliasedByRow, {
1126
- node,
1127
- depth: 0
1128
- }, node.path))
1129
- }),
1130
- truncated && /* @__PURE__ */ jsxs("div", {
1131
- className: "sb-token-detail__aliased-by-truncated",
1755
+ function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
1756
+ const project = useProject();
1757
+ const { resolved, activeTheme, cssVarPrefix } = project;
1758
+ const rows = useMemo(() => {
1759
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1760
+ if (token.$type !== "dimension") return false;
1761
+ return globMatch(path, filter);
1762
+ }), {
1763
+ by: sortBy,
1764
+ dir: sortDir
1765
+ }).map(([path, token]) => {
1766
+ const pxValue = toPixels(token.$value);
1767
+ return {
1768
+ path,
1769
+ cssVar: resolveCssVar(path, project),
1770
+ displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
1771
+ pxValue,
1772
+ capped: Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX
1773
+ };
1774
+ });
1775
+ }, [
1776
+ resolved,
1777
+ filter,
1778
+ project,
1779
+ sortBy,
1780
+ sortDir
1781
+ ]);
1782
+ const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1783
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1784
+ ...themeAttrs(cssVarPrefix, activeTheme),
1785
+ children: /* @__PURE__ */ jsx("div", {
1786
+ className: "sb-block__empty",
1787
+ children: "No dimension tokens match this filter."
1788
+ })
1789
+ });
1790
+ return /* @__PURE__ */ jsxs("div", {
1791
+ ...themeAttrs(cssVarPrefix, activeTheme),
1792
+ children: [/* @__PURE__ */ jsx("div", {
1793
+ className: "sb-block__caption",
1794
+ children: captionText
1795
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1796
+ className: "sb-dimension-scale__row",
1132
1797
  children: [
1133
- "Further descendants truncated at depth ",
1134
- ALIASED_BY_DEPTH_CAP,
1135
- "."
1798
+ /* @__PURE__ */ jsxs("div", {
1799
+ className: "sb-dimension-scale__meta",
1800
+ children: [/* @__PURE__ */ jsx("span", {
1801
+ className: "sb-dimension-scale__path",
1802
+ children: row.path
1803
+ }), /* @__PURE__ */ jsx("span", {
1804
+ className: "sb-dimension-scale__specs",
1805
+ children: row.displayValue
1806
+ })]
1807
+ }),
1808
+ /* @__PURE__ */ jsxs("div", {
1809
+ className: "sb-dimension-scale__visual-cell",
1810
+ children: [/* @__PURE__ */ jsx(DimensionBar, {
1811
+ path: row.path,
1812
+ kind
1813
+ }), row.capped && /* @__PURE__ */ jsxs("span", {
1814
+ className: "sb-dimension-scale__cap",
1815
+ children: [
1816
+ "capped at ",
1817
+ MAX_RENDER_PX,
1818
+ "px"
1819
+ ]
1820
+ })]
1821
+ }),
1822
+ /* @__PURE__ */ jsx("span", {
1823
+ className: "sb-dimension-scale__css-var",
1824
+ children: row.cssVar
1825
+ })
1136
1826
  ]
1137
- })
1138
- ] });
1139
- }
1140
- function AliasedByRow({ node, depth }) {
1141
- return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("div", {
1142
- className: "sb-token-detail__aliased-by-row",
1143
- style: { paddingLeft: depth * 16 },
1144
- children: /* @__PURE__ */ jsx("span", {
1145
- className: "sb-token-detail__chain-node",
1146
- children: node.path
1147
- })
1148
- }), node.children.length > 0 && /* @__PURE__ */ jsx("ul", {
1149
- className: "sb-token-detail__aliased-by-list",
1150
- children: node.children.map((child) => /* @__PURE__ */ jsx(AliasedByRow, {
1151
- node: child,
1152
- depth: depth + 1
1153
- }, child.path))
1154
- })] });
1155
- }
1156
- function buildAliasedByTree(rootPath, resolved) {
1157
- const direct = resolved[rootPath]?.aliasedBy;
1158
- if (!direct || direct.length === 0) return [];
1159
- const visited = new Set([rootPath]);
1160
- return sortPaths(direct).map((p) => walk(p, resolved, visited, 1));
1827
+ }, row.path))]
1828
+ });
1161
1829
  }
1162
- function walk(path, resolved, visited, depth) {
1163
- if (visited.has(path)) return {
1164
- path,
1165
- children: []
1166
- };
1167
- visited.add(path);
1168
- const parents = resolved[path]?.aliasedBy;
1169
- if (!parents || parents.length === 0) return {
1170
- path,
1171
- children: []
1172
- };
1173
- if (depth >= ALIASED_BY_DEPTH_CAP) return {
1174
- path,
1175
- children: [],
1176
- truncated: true
1177
- };
1178
- return {
1179
- path,
1180
- children: sortPaths(parents).map((p) => walk(p, resolved, visited, depth + 1))
1181
- };
1830
+ //#endregion
1831
+ //#region src/FontFamilySample.tsx
1832
+ function stackString(raw) {
1833
+ if (typeof raw === "string") return raw;
1834
+ if (Array.isArray(raw)) return raw.map(String).join(", ");
1835
+ return "";
1182
1836
  }
1183
- function sortPaths(paths) {
1184
- return paths.toSorted((a, b) => {
1185
- const ra = GROUP_RANK[a.split(".")[0] ?? ""] ?? 2;
1186
- const rb = GROUP_RANK[b.split(".")[0] ?? ""] ?? 2;
1187
- return ra !== rb ? ra - rb : a.localeCompare(b, void 0, { numeric: true });
1837
+ function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1838
+ const project = useProject();
1839
+ const { resolved, activeTheme, cssVarPrefix } = project;
1840
+ const rows = useMemo(() => {
1841
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1842
+ if (token.$type !== "fontFamily") return false;
1843
+ return globMatch(path, filter);
1844
+ }), {
1845
+ by: sortBy,
1846
+ dir: sortDir
1847
+ }).map(([path, token]) => ({
1848
+ path,
1849
+ cssVar: resolveCssVar(path, project),
1850
+ stack: stackString(token.$value)
1851
+ }));
1852
+ }, [
1853
+ resolved,
1854
+ filter,
1855
+ project,
1856
+ sortBy,
1857
+ sortDir
1858
+ ]);
1859
+ const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1860
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1861
+ ...themeAttrs(cssVarPrefix, activeTheme),
1862
+ children: /* @__PURE__ */ jsx("div", {
1863
+ className: "sb-block__empty",
1864
+ children: "No fontFamily tokens match this filter."
1865
+ })
1866
+ });
1867
+ return /* @__PURE__ */ jsxs("div", {
1868
+ ...themeAttrs(cssVarPrefix, activeTheme),
1869
+ children: [/* @__PURE__ */ jsx("div", {
1870
+ className: "sb-block__caption",
1871
+ children: captionText
1872
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1873
+ className: "sb-font-family-sample__row",
1874
+ children: [
1875
+ /* @__PURE__ */ jsxs("div", {
1876
+ className: "sb-font-family-sample__meta",
1877
+ children: [/* @__PURE__ */ jsx("span", {
1878
+ className: "sb-font-family-sample__path",
1879
+ children: row.path
1880
+ }), /* @__PURE__ */ jsx("span", {
1881
+ className: "sb-font-family-sample__stack",
1882
+ children: row.stack
1883
+ })]
1884
+ }),
1885
+ /* @__PURE__ */ jsx("div", {
1886
+ className: "sb-font-family-sample__sample",
1887
+ style: { fontFamily: row.cssVar },
1888
+ children: sample
1889
+ }),
1890
+ /* @__PURE__ */ jsx("span", {
1891
+ className: "sb-font-family-sample__css-var",
1892
+ children: row.cssVar
1893
+ })
1894
+ ]
1895
+ }, row.path))]
1188
1896
  });
1189
1897
  }
1190
- function treeHasTruncation(nodes) {
1191
- for (const n of nodes) {
1192
- if (n.truncated) return true;
1193
- if (treeHasTruncation(n.children)) return true;
1898
+ //#endregion
1899
+ //#region src/FontWeightScale.tsx
1900
+ function toWeight(raw) {
1901
+ if (typeof raw === "number") return raw;
1902
+ if (typeof raw === "string") {
1903
+ const n = Number.parseInt(raw, 10);
1904
+ return Number.isFinite(n) ? n : NaN;
1194
1905
  }
1195
- return false;
1906
+ return NaN;
1196
1907
  }
1197
- //#endregion
1198
- //#region src/token-detail/AxisVariance.tsx
1199
- function AxisVariance({ path }) {
1200
- const { token, cssVar, axes, themes, themesResolved, activeAxes, cssVarPrefix } = useTokenDetailData(path);
1201
- const colorFormat = useColorFormat();
1202
- const tokenType = token?.$type;
1203
- const isColor = tokenType === "color";
1204
- const formatFn = (t) => valueFor(t, tokenType, colorFormat);
1205
- const variance = useMemo(() => {
1206
- const result = analyzeAxisVariance(path, axes, themes, themesResolved);
1207
- return {
1208
- kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
1209
- varyingAxes: result.varyingAxes
1210
- };
1908
+ function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
1909
+ const project = useProject();
1910
+ const { resolved, activeTheme, cssVarPrefix } = project;
1911
+ const rows = useMemo(() => {
1912
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1913
+ if (token.$type !== "fontWeight") return false;
1914
+ return globMatch(path, filter);
1915
+ }), {
1916
+ by: sortBy,
1917
+ dir: sortDir
1918
+ }).map(([path, token]) => ({
1919
+ path,
1920
+ cssVar: resolveCssVar(path, project),
1921
+ display: token.$value == null ? "" : String(token.$value),
1922
+ weight: toWeight(token.$value)
1923
+ }));
1211
1924
  }, [
1212
- path,
1213
- axes,
1214
- themes,
1215
- themesResolved
1216
- ]);
1217
- if (themes.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
1218
- if (variance.kind === "constant") {
1219
- const anyTheme = themes[0];
1220
- const value = anyTheme ? formatFn(themesResolved[anyTheme.name]?.[path]) : "—";
1221
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
1222
- className: "sb-token-detail__section-header",
1223
- children: "Values across axes"
1224
- }), /* @__PURE__ */ jsx("table", {
1225
- className: "sb-token-detail__theme-table",
1226
- "data-testid": "token-detail-values",
1227
- children: /* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx("tr", {
1228
- className: "sb-token-detail__theme-row",
1229
- children: /* @__PURE__ */ jsxs("td", {
1230
- className: "sb-token-detail__theme-cell",
1231
- "data-testid": "token-detail-constant",
1232
- children: [
1233
- isColor && /* @__PURE__ */ jsx("span", {
1234
- className: "sb-token-detail__swatch",
1235
- style: { background: cssVar },
1236
- "aria-hidden": true
1237
- }),
1238
- value,
1239
- /* @__PURE__ */ jsxs("span", {
1240
- style: {
1241
- opacity: .6,
1242
- marginLeft: 8
1243
- },
1244
- children: [
1245
- "same across all ",
1246
- themes.length,
1247
- " tuples"
1248
- ]
1249
- })
1250
- ]
1251
- })
1252
- }) })
1253
- })] });
1254
- }
1255
- if (variance.kind === "one-axis") {
1256
- const axisName = variance.varyingAxes[0];
1257
- if (!axisName) return /* @__PURE__ */ jsx(Fragment, {});
1258
- const axis = axes.find((a) => a.name === axisName);
1259
- if (!axis) return /* @__PURE__ */ jsx(Fragment, {});
1260
- const contextValues = axis.contexts.map((ctx) => {
1261
- const target = {
1262
- ...activeAxes,
1263
- [axisName]: ctx
1264
- };
1265
- const name = themes.find((t) => {
1266
- const input = t.input;
1267
- return Object.keys(input).every((k) => input[k] === target[k]);
1268
- })?.name ?? "";
1269
- return {
1270
- ctx,
1271
- themeName: name,
1272
- value: name ? formatFn(themesResolved[name]?.[path]) : "—"
1273
- };
1274
- });
1275
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1276
- className: "sb-token-detail__section-header",
1277
- children: ["Varies with ", axisName]
1278
- }), /* @__PURE__ */ jsx("table", {
1279
- className: "sb-token-detail__theme-table",
1280
- "data-testid": "token-detail-values",
1281
- children: /* @__PURE__ */ jsx("tbody", { children: contextValues.map((row) => /* @__PURE__ */ jsxs("tr", {
1282
- className: "sb-token-detail__theme-row",
1283
- "data-axis": axisName,
1284
- "data-context": row.ctx,
1285
- children: [/* @__PURE__ */ jsx("td", {
1286
- className: "sb-token-detail__theme-cell",
1287
- style: { width: "30%" },
1288
- children: row.ctx
1289
- }), /* @__PURE__ */ jsxs("td", {
1290
- className: "sb-token-detail__theme-cell",
1291
- children: [isColor && row.themeName && /* @__PURE__ */ jsx("span", {
1292
- className: "sb-token-detail__swatch",
1293
- style: { background: cssVar },
1294
- [dataAttr(cssVarPrefix, "theme")]: row.themeName,
1295
- "aria-hidden": true
1296
- }), row.value]
1297
- })]
1298
- }, row.ctx)) })
1299
- })] });
1300
- }
1301
- const [rowAxis, colAxis, ...extra] = variance.varyingAxes.map((name) => axes.find((a) => a.name === name)).filter((a) => Boolean(a)).toSorted((a, b) => b.contexts.length - a.contexts.length);
1302
- if (!rowAxis || !colAxis) return /* @__PURE__ */ jsx(Fragment, {});
1303
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1304
- /* @__PURE__ */ jsxs("div", {
1305
- className: "sb-token-detail__section-header",
1306
- children: ["Varies with ", variance.varyingAxes.join(" × ")]
1307
- }),
1308
- /* @__PURE__ */ jsxs("table", {
1309
- className: "sb-token-detail__theme-table",
1310
- "data-testid": "token-detail-values",
1311
- children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
1312
- className: "sb-token-detail__theme-row",
1313
- children: [/* @__PURE__ */ jsxs("th", {
1314
- className: "sb-token-detail__theme-cell",
1315
- style: {
1316
- textAlign: "left",
1317
- opacity: .7
1318
- },
1319
- children: [
1320
- rowAxis.name,
1321
- " \\ ",
1322
- colAxis.name
1323
- ]
1324
- }), colAxis.contexts.map((col) => /* @__PURE__ */ jsx("th", {
1325
- className: "sb-token-detail__theme-cell",
1326
- style: {
1327
- textAlign: "left",
1328
- opacity: .7
1329
- },
1330
- children: col
1331
- }, col))]
1332
- }) }), /* @__PURE__ */ jsx("tbody", { children: rowAxis.contexts.map((row) => /* @__PURE__ */ jsxs("tr", {
1333
- className: "sb-token-detail__theme-row",
1334
- children: [/* @__PURE__ */ jsx("td", {
1335
- className: "sb-token-detail__theme-cell",
1336
- children: row
1337
- }), colAxis.contexts.map((col) => {
1338
- const name = tupleName(themes, {
1339
- ...activeAxes,
1340
- [rowAxis.name]: row,
1341
- [colAxis.name]: col
1342
- });
1343
- const value = name ? formatFn(themesResolved[name]?.[path]) : "—";
1344
- return /* @__PURE__ */ jsxs("td", {
1345
- className: "sb-token-detail__theme-cell",
1346
- "data-row": row,
1347
- "data-col": col,
1348
- children: [isColor && name && /* @__PURE__ */ jsx("span", {
1349
- className: "sb-token-detail__swatch",
1350
- style: { background: cssVar },
1351
- [dataAttr(cssVarPrefix, "theme")]: name,
1352
- "aria-hidden": true
1353
- }), value]
1354
- }, col);
1355
- })]
1356
- }, row)) })]
1357
- }),
1358
- extra.length > 0 && /* @__PURE__ */ jsxs("div", {
1359
- className: "sb-token-detail__aliased-by-truncated",
1360
- style: { marginTop: 6 },
1925
+ resolved,
1926
+ filter,
1927
+ project,
1928
+ sortBy,
1929
+ sortDir
1930
+ ]);
1931
+ const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1932
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1933
+ ...themeAttrs(cssVarPrefix, activeTheme),
1934
+ children: /* @__PURE__ */ jsx("div", {
1935
+ className: "sb-block__empty",
1936
+ children: "No fontWeight tokens match this filter."
1937
+ })
1938
+ });
1939
+ return /* @__PURE__ */ jsxs("div", {
1940
+ ...themeAttrs(cssVarPrefix, activeTheme),
1941
+ children: [/* @__PURE__ */ jsx("div", {
1942
+ className: "sb-block__caption",
1943
+ children: captionText
1944
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1945
+ className: "sb-font-weight-scale__row",
1361
1946
  children: [
1362
- "Values also vary with ",
1363
- extra.map((a) => a.name).join(", "),
1364
- "; matrix shows the slice for the active selection."
1947
+ /* @__PURE__ */ jsxs("div", {
1948
+ className: "sb-font-weight-scale__meta",
1949
+ children: [/* @__PURE__ */ jsx("span", {
1950
+ className: "sb-font-weight-scale__path",
1951
+ children: row.path
1952
+ }), /* @__PURE__ */ jsx("span", {
1953
+ className: "sb-font-weight-scale__value",
1954
+ children: row.display
1955
+ })]
1956
+ }),
1957
+ /* @__PURE__ */ jsx("div", {
1958
+ className: "sb-font-weight-scale__sample",
1959
+ style: { fontWeight: row.cssVar },
1960
+ children: sample
1961
+ }),
1962
+ /* @__PURE__ */ jsx("span", {
1963
+ className: "sb-font-weight-scale__css-var",
1964
+ children: row.cssVar
1965
+ })
1365
1966
  ]
1366
- })
1367
- ] });
1967
+ }, row.path))]
1968
+ });
1368
1969
  }
1369
- function valueFor(token, $type, format) {
1370
- if (!token) return "—";
1371
- return formatTokenValue(token.$value, $type, format);
1970
+ //#endregion
1971
+ //#region src/GradientPalette.tsx
1972
+ function asStops(raw) {
1973
+ if (!Array.isArray(raw)) return [];
1974
+ return raw;
1372
1975
  }
1373
- function tupleName(themes, tuple) {
1374
- return themes.find((t) => {
1375
- const input = t.input;
1376
- return Object.keys(input).every((k) => input[k] === tuple[k]);
1377
- })?.name;
1976
+ const pct = (n) => `${(n * 100).toFixed(3)}%`;
1977
+ function stopCssColor(stop) {
1978
+ const color = stop.color;
1979
+ if (!color || !Array.isArray(color.components) || color.components.length < 3) return "transparent";
1980
+ const [r, g, b] = color.components;
1981
+ if (r === void 0 || g === void 0 || b === void 0) return "transparent";
1982
+ const alpha = color.alpha ?? 1;
1983
+ return alpha === 1 ? `rgb(${pct(r)} ${pct(g)} ${pct(b)})` : `rgb(${pct(r)} ${pct(g)} ${pct(b)} / ${alpha})`;
1378
1984
  }
1379
- //#endregion
1380
- //#region src/token-detail/CompositeBreakdown.tsx
1381
- function CompositeBreakdown({ path }) {
1382
- const { token, resolved } = useTokenDetailData(path);
1985
+ function stopKey(path, stop, fallback) {
1986
+ return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
1987
+ }
1988
+ function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1989
+ const project = useProject();
1990
+ const { resolved, activeTheme, cssVarPrefix } = project;
1383
1991
  const colorFormat = useColorFormat();
1384
- if (!token) return null;
1385
- return /* @__PURE__ */ jsx(CompositeBreakdownContent, {
1386
- type: token.$type,
1387
- rawValue: token.$value,
1388
- partialAliasOf: token.partialAliasOf,
1992
+ const rows = useMemo(() => {
1993
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1994
+ if (token.$type !== "gradient") return false;
1995
+ return globMatch(path, filter);
1996
+ }), {
1997
+ by: sortBy,
1998
+ dir: sortDir
1999
+ }).map(([path, token]) => ({
2000
+ path,
2001
+ cssVar: resolveCssVar(path, project),
2002
+ stops: asStops(token.$value)
2003
+ }));
2004
+ }, [
1389
2005
  resolved,
1390
- colorFormat
2006
+ filter,
2007
+ project,
2008
+ sortBy,
2009
+ sortDir
2010
+ ]);
2011
+ const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2012
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2013
+ ...themeAttrs(cssVarPrefix, activeTheme),
2014
+ children: /* @__PURE__ */ jsx("div", {
2015
+ className: "sb-block__empty",
2016
+ children: "No gradient tokens match this filter."
2017
+ })
1391
2018
  });
1392
- }
1393
- function CompositeBreakdownContent({ type, rawValue, partialAliasOf, resolved, colorFormat }) {
1394
- if (!rawValue || typeof rawValue !== "object") return null;
1395
- const objectAliases = pickObjectAliases(partialAliasOf);
1396
- const arrayAliases = pickArrayAliases(partialAliasOf);
1397
- const aliasFor = (key) => subValueChain(objectAliases?.[key], resolved);
1398
- if (type === "typography") {
1399
- const v = rawValue;
1400
- return renderKeyValueList([
1401
- [
1402
- "fontFamily",
1403
- formatFontFamily(v["fontFamily"]),
1404
- aliasFor("fontFamily")
1405
- ],
1406
- [
1407
- "fontSize",
1408
- formatDimensionValue(v["fontSize"]),
1409
- aliasFor("fontSize")
1410
- ],
1411
- [
1412
- "fontWeight",
1413
- formatPrimitive(v["fontWeight"]),
1414
- aliasFor("fontWeight")
1415
- ],
1416
- [
1417
- "lineHeight",
1418
- formatPrimitive(v["lineHeight"]),
1419
- aliasFor("lineHeight")
1420
- ],
1421
- [
1422
- "letterSpacing",
1423
- formatDimensionValue(v["letterSpacing"]),
1424
- aliasFor("letterSpacing")
1425
- ]
1426
- ]);
1427
- }
1428
- if (type === "border") {
1429
- const v = rawValue;
1430
- return renderKeyValueList([
1431
- [
1432
- "color",
1433
- formatColorSubValue(v["color"], colorFormat),
1434
- aliasFor("color")
1435
- ],
1436
- [
1437
- "width",
1438
- formatDimensionValue(v["width"]),
1439
- aliasFor("width")
1440
- ],
1441
- [
1442
- "style",
1443
- formatPrimitive(v["style"]),
1444
- aliasFor("style")
1445
- ]
1446
- ]);
1447
- }
1448
- if (type === "transition") {
1449
- const v = rawValue;
1450
- return renderKeyValueList([
1451
- [
1452
- "duration",
1453
- formatDimensionValue(v["duration"]),
1454
- aliasFor("duration")
1455
- ],
1456
- [
1457
- "timingFunction",
1458
- formatPrimitive(v["timingFunction"]),
1459
- aliasFor("timingFunction")
1460
- ],
1461
- [
1462
- "delay",
1463
- formatDimensionValue(v["delay"]),
1464
- aliasFor("delay")
2019
+ return /* @__PURE__ */ jsxs("div", {
2020
+ ...themeAttrs(cssVarPrefix, activeTheme),
2021
+ children: [/* @__PURE__ */ jsx("div", {
2022
+ className: "sb-block__caption",
2023
+ children: captionText
2024
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2025
+ className: "sb-gradient-palette__row",
2026
+ children: [
2027
+ /* @__PURE__ */ jsxs("div", {
2028
+ className: "sb-gradient-palette__meta",
2029
+ children: [/* @__PURE__ */ jsx("span", {
2030
+ className: "sb-gradient-palette__path",
2031
+ children: row.path
2032
+ }), /* @__PURE__ */ jsx("span", {
2033
+ className: "sb-gradient-palette__css-var",
2034
+ children: row.cssVar
2035
+ })]
2036
+ }),
2037
+ /* @__PURE__ */ jsx("div", {
2038
+ className: "sb-gradient-palette__sample",
2039
+ style: { background: `linear-gradient(to right, ${row.cssVar})` },
2040
+ "aria-hidden": true
2041
+ }),
2042
+ /* @__PURE__ */ jsx("div", {
2043
+ className: "sb-gradient-palette__stops",
2044
+ children: row.stops.map((stop, i) => /* @__PURE__ */ jsxs("div", {
2045
+ className: "sb-gradient-palette__stop-row",
2046
+ children: [
2047
+ /* @__PURE__ */ jsx("span", {
2048
+ className: "sb-gradient-palette__stop-swatch",
2049
+ style: { background: stopCssColor(stop) },
2050
+ "aria-hidden": true
2051
+ }),
2052
+ /* @__PURE__ */ jsx("span", { children: formatColor(stop.color, colorFormat).value }),
2053
+ /* @__PURE__ */ jsxs("span", {
2054
+ className: "sb-gradient-palette__stop-position",
2055
+ children: [
2056
+ "@ ",
2057
+ ((stop.position ?? 0) * 100).toFixed(0),
2058
+ "%"
2059
+ ]
2060
+ })
2061
+ ]
2062
+ }, stopKey(row.path, stop, i)))
2063
+ })
1465
2064
  ]
1466
- ]);
1467
- }
1468
- if (type === "shadow") {
1469
- const layers = Array.isArray(rawValue) ? rawValue : [rawValue];
1470
- const multi = layers.length > 1;
1471
- const layerAliasFor = (i, key) => subValueChain(arrayAliases?.[i]?.[key], resolved);
1472
- return /* @__PURE__ */ jsx("div", {
1473
- className: "sb-token-detail__breakdown-section",
1474
- children: layers.map((layer, i) => {
1475
- const v = layer;
1476
- return /* @__PURE__ */ jsxs("div", {
1477
- style: { display: "contents" },
1478
- children: [
1479
- multi && /* @__PURE__ */ jsxs("div", {
1480
- className: "sb-token-detail__breakdown-layer-header",
1481
- children: ["Layer ", i + 1]
1482
- }),
1483
- /* @__PURE__ */ jsx(KeyValueRow, {
1484
- label: "color",
1485
- value: formatColorSubValue(v["color"], colorFormat),
1486
- alias: layerAliasFor(i, "color")
1487
- }),
1488
- /* @__PURE__ */ jsx(KeyValueRow, {
1489
- label: "offsetX",
1490
- value: formatDimensionValue(v["offsetX"]),
1491
- alias: layerAliasFor(i, "offsetX")
1492
- }),
1493
- /* @__PURE__ */ jsx(KeyValueRow, {
1494
- label: "offsetY",
1495
- value: formatDimensionValue(v["offsetY"]),
1496
- alias: layerAliasFor(i, "offsetY")
1497
- }),
1498
- /* @__PURE__ */ jsx(KeyValueRow, {
1499
- label: "blur",
1500
- value: formatDimensionValue(v["blur"]),
1501
- alias: layerAliasFor(i, "blur")
1502
- }),
1503
- /* @__PURE__ */ jsx(KeyValueRow, {
1504
- label: "spread",
1505
- value: formatDimensionValue(v["spread"]),
1506
- alias: layerAliasFor(i, "spread")
1507
- }),
1508
- "inset" in v && /* @__PURE__ */ jsx(KeyValueRow, {
1509
- label: "inset",
1510
- value: formatPrimitive(v["inset"]),
1511
- alias: void 0
1512
- })
1513
- ]
1514
- }, shadowLayerKey(v, i));
1515
- })
1516
- });
1517
- }
1518
- if (type === "gradient") {
1519
- const stops = Array.isArray(rawValue) ? rawValue : [];
1520
- if (stops.length === 0) return null;
1521
- const stopAliasFor = (i) => subValueChain(arrayAliases?.[i]?.["color"], resolved);
1522
- return /* @__PURE__ */ jsx("div", {
1523
- className: "sb-token-detail__breakdown-section",
1524
- children: stops.map((stop, i) => {
1525
- const v = stop;
1526
- return /* @__PURE__ */ jsx(KeyValueRow, {
1527
- label: `${((typeof v["position"] === "number" ? v["position"] : 0) * 100).toFixed(0)}%`,
1528
- value: formatColorSubValue(v["color"], colorFormat),
1529
- alias: stopAliasFor(i)
1530
- }, gradientStopKey(v, i));
1531
- })
1532
- });
1533
- }
1534
- return null;
1535
- }
1536
- function renderKeyValueList(rows) {
1537
- return /* @__PURE__ */ jsx("div", {
1538
- className: "sb-token-detail__breakdown-section",
1539
- children: rows.filter(([, v, alias]) => v !== null || alias && alias.length > 0).map(([k, v, alias]) => /* @__PURE__ */ jsx(KeyValueRow, {
1540
- label: k,
1541
- value: v ?? "",
1542
- alias
1543
- }, k))
2065
+ }, row.path))]
1544
2066
  });
1545
2067
  }
1546
- function KeyValueRow({ label, value, alias }) {
1547
- const hasAlias = alias && alias.length > 0;
1548
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1549
- className: "sb-token-detail__breakdown-key",
1550
- children: label
1551
- }), /* @__PURE__ */ jsxs("span", {
1552
- className: "sb-token-detail__breakdown-value",
1553
- children: [/* @__PURE__ */ jsx("span", { children: value ?? "—" }), hasAlias && /* @__PURE__ */ jsx("span", {
1554
- className: "sb-token-detail__breakdown-alias",
1555
- "data-testid": "breakdown-alias",
1556
- children: alias.map((p, i) => /* @__PURE__ */ jsxs("span", {
1557
- className: "sb-token-detail__breakdown-alias-step",
1558
- children: [i > 0 && /* @__PURE__ */ jsx("span", {
1559
- className: "sb-token-detail__arrow",
1560
- children: "→"
1561
- }), /* @__PURE__ */ jsx("span", {
1562
- className: "sb-token-detail__chain-node",
1563
- children: p
1564
- })]
1565
- }, p))
1566
- })]
1567
- })] });
1568
- }
1569
- function formatPrimitive(v) {
1570
- if (v == null) return null;
1571
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
1572
- return JSON.stringify(v);
1573
- }
1574
- function formatFontFamily(v) {
1575
- if (v == null) return null;
1576
- if (typeof v === "string") return v;
1577
- if (Array.isArray(v)) return v.map(String).join(", ");
1578
- return JSON.stringify(v);
1579
- }
1580
- function formatDimensionValue(v) {
1581
- if (v == null) return null;
1582
- if (typeof v === "string" || typeof v === "number") return String(v);
1583
- if (typeof v === "object") {
1584
- const d = v;
1585
- if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
1586
- }
1587
- return JSON.stringify(v);
1588
- }
1589
- /**
1590
- * Route sub-value colors through `formatColor` so they honor the active
1591
- * color-format dropdown, just like the standalone `<ColorPalette />` and
1592
- * `<TokenDetail />` top-line do. Returns `null` for a missing field so
1593
- * the key/value row drops out entirely.
1594
- */
1595
- function formatColorSubValue(v, format) {
1596
- if (v == null) return null;
1597
- return formatColor(v, format).value;
1598
- }
1599
- function pickObjectAliases(v) {
1600
- if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
1601
- return v;
1602
- }
1603
- function pickArrayAliases(v) {
1604
- if (!Array.isArray(v)) return void 0;
1605
- return v;
1606
- }
1607
- /**
1608
- * Walk the alias chain starting from an immediate sub-value alias target.
1609
- * `aliasTarget` is the path the sub-value directly references; the target
1610
- * token's own `aliasChain` continues the walk to the primitive.
1611
- */
1612
- function subValueChain(aliasTarget, resolved) {
1613
- if (!aliasTarget) return void 0;
1614
- const tail = (resolved?.[aliasTarget])?.aliasChain;
1615
- return tail && tail.length > 0 ? [aliasTarget, ...tail] : [aliasTarget];
1616
- }
1617
- function shadowLayerKey(layer, fallback) {
1618
- return `shadow|${[
1619
- layer["color"],
1620
- layer["offsetX"],
1621
- layer["offsetY"],
1622
- layer["blur"],
1623
- layer["spread"],
1624
- layer["inset"]
1625
- ].map((p) => p === void 0 ? "" : JSON.stringify(p)).join("|")}|${fallback}`;
1626
- }
1627
- function gradientStopKey(stop, fallback) {
1628
- return `stop|${stop["position"] ?? fallback}|${JSON.stringify(stop["color"])}`;
1629
- }
1630
2068
  //#endregion
1631
2069
  //#region src/internal/prefers-reduced-motion.ts
1632
2070
  /**
@@ -1646,1619 +2084,1466 @@ function usePrefersReducedMotion() {
1646
2084
  return reduced;
1647
2085
  }
1648
2086
  //#endregion
1649
- //#region src/token-detail/CompositePreview.tsx
1650
- const PANGRAM = "Sphinx of black quartz, judge my vow.";
1651
- const STROKE_STYLE_STRINGS = new Set([
1652
- "solid",
1653
- "dashed",
1654
- "dotted",
1655
- "double",
1656
- "groove",
1657
- "ridge",
1658
- "outset",
1659
- "inset"
1660
- ]);
1661
- function CompositePreview({ path }) {
1662
- const { token, cssVar } = useTokenDetailData(path);
1663
- if (!token) return null;
1664
- return /* @__PURE__ */ jsx(CompositePreviewContent, {
1665
- type: token.$type,
1666
- cssVar,
1667
- rawValue: token.$value
1668
- });
1669
- }
1670
- function CompositePreviewContent({ type, cssVar, rawValue }) {
1671
- if (type === "typography") {
1672
- const base = cssVar.replace(/^var\(/, "").replace(/\)$/, "");
1673
- return /* @__PURE__ */ jsx("div", {
1674
- className: "sb-token-detail__typography-sample",
1675
- style: {
1676
- fontFamily: `var(${base}-font-family)`,
1677
- fontSize: `var(${base}-font-size)`,
1678
- fontWeight: `var(${base}-font-weight)`,
1679
- lineHeight: `var(${base}-line-height)`,
1680
- letterSpacing: `var(${base}-letter-spacing)`
1681
- },
1682
- children: PANGRAM
1683
- });
1684
- }
1685
- if (type === "shadow") return /* @__PURE__ */ jsx("div", {
1686
- className: "sb-token-detail__shadow-sample",
1687
- style: { boxShadow: cssVar },
1688
- "aria-hidden": true
1689
- });
1690
- if (type === "border") return /* @__PURE__ */ jsx("div", {
1691
- className: "sb-token-detail__border-sample",
1692
- style: { border: cssVar },
1693
- "aria-hidden": true
1694
- });
1695
- if (type === "transition") return /* @__PURE__ */ jsx(TransitionSample, { transition: cssVar });
1696
- if (type === "dimension") return /* @__PURE__ */ jsx("div", {
1697
- className: "sb-token-detail__dimension-track",
1698
- children: /* @__PURE__ */ jsx("div", {
1699
- className: "sb-token-detail__dimension-bar",
1700
- style: { width: cssVar },
1701
- "aria-hidden": true
1702
- })
1703
- });
1704
- if (type === "duration") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left ${cssVar} ease` });
1705
- if (type === "fontFamily") return /* @__PURE__ */ jsx("div", {
1706
- className: "sb-token-detail__font-family-sample",
1707
- style: { fontFamily: cssVar },
1708
- children: PANGRAM
1709
- });
1710
- if (type === "fontWeight") return /* @__PURE__ */ jsx("div", {
1711
- className: "sb-token-detail__font-weight-sample",
1712
- style: { fontWeight: cssVar },
1713
- children: "Aa"
1714
- });
1715
- if (type === "cubicBezier") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left 800ms ${cssVar}` });
1716
- if (type === "gradient") return /* @__PURE__ */ jsx("div", {
1717
- className: "sb-token-detail__gradient-sample",
1718
- style: { background: `linear-gradient(to right, ${cssVar})` },
1719
- "aria-hidden": true
1720
- });
1721
- if (type === "strokeStyle") return /* @__PURE__ */ jsx(StrokeStylePreview, { value: rawValue });
1722
- if (type === "color") return /* @__PURE__ */ jsxs("div", {
1723
- className: "sb-token-detail__color-swatch-row",
1724
- "aria-hidden": true,
1725
- children: [/* @__PURE__ */ jsx("div", {
1726
- className: "sb-token-detail__color-swatch-light",
1727
- style: { background: cssVar }
1728
- }), /* @__PURE__ */ jsx("div", {
1729
- className: "sb-token-detail__color-swatch-dark",
1730
- style: { background: cssVar }
1731
- })]
1732
- });
2087
+ //#region src/motion-preview/MotionSample.tsx
2088
+ const DEFAULT_DURATION_MS = 300;
2089
+ const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2090
+ const styles = {
2091
+ track: {
2092
+ position: "relative",
2093
+ height: 36,
2094
+ background: SURFACE_MUTED,
2095
+ borderRadius: 18,
2096
+ overflow: "hidden"
2097
+ },
2098
+ ball: {
2099
+ position: "absolute",
2100
+ top: "50%",
2101
+ width: 28,
2102
+ height: 28,
2103
+ marginTop: -14,
2104
+ borderRadius: "50%",
2105
+ background: "var(--swatchbook-accent-bg, #3b82f6)"
2106
+ },
2107
+ reducedMotion: {
2108
+ fontSize: 11,
2109
+ color: TEXT_MUTED,
2110
+ fontStyle: "italic"
2111
+ }
2112
+ };
2113
+ function extractDurationMs(raw) {
2114
+ if (raw == null) return NaN;
2115
+ if (typeof raw === "object") {
2116
+ const v = raw;
2117
+ if (typeof v.value === "number" && typeof v.unit === "string") {
2118
+ if (v.unit === "ms") return v.value;
2119
+ if (v.unit === "s") return v.value * 1e3;
2120
+ }
2121
+ }
2122
+ return NaN;
2123
+ }
2124
+ function extractCubicBezier(raw) {
2125
+ if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
1733
2126
  return null;
1734
2127
  }
1735
- function StrokeStylePreview({ value }) {
1736
- if (typeof value === "string" && STROKE_STYLE_STRINGS.has(value)) return /* @__PURE__ */ jsx("div", {
1737
- className: "sb-token-detail__stroke-style-line",
1738
- style: { borderTopStyle: value },
1739
- "aria-hidden": true
1740
- });
1741
- if (value && typeof value === "object" && "dashArray" in value) {
1742
- const v = value;
1743
- const lengths = asDashLengths(v.dashArray);
1744
- if (lengths.length === 0) return /* @__PURE__ */ jsx("div", {
1745
- className: "sb-token-detail__stroke-style-fallback",
1746
- children: "Object-form strokeStyle with no resolvable dashArray."
1747
- });
1748
- const cap = typeof v.lineCap === "string" ? v.lineCap : "butt";
1749
- return /* @__PURE__ */ jsx("svg", {
1750
- className: "sb-token-detail__stroke-style-svg",
1751
- viewBox: "0 0 220 24",
1752
- preserveAspectRatio: "none",
1753
- "aria-hidden": true,
1754
- children: /* @__PURE__ */ jsx("line", {
1755
- x1: "4",
1756
- y1: "12",
1757
- x2: "216",
1758
- y2: "12",
1759
- stroke: "currentColor",
1760
- strokeWidth: "4",
1761
- strokeDasharray: lengths.join(" "),
1762
- strokeLinecap: cap
1763
- })
1764
- });
2128
+ function asDuration(raw, themeTokens, fallback) {
2129
+ const direct = extractDurationMs(raw);
2130
+ if (Number.isFinite(direct)) return direct;
2131
+ if (typeof raw === "string") {
2132
+ const match = raw.match(/^\{([^}]+)\}$/);
2133
+ if (match && match[1]) {
2134
+ const referenced = themeTokens[match[1]];
2135
+ const resolved = extractDurationMs(referenced?.$value);
2136
+ if (Number.isFinite(resolved)) return resolved;
2137
+ }
1765
2138
  }
1766
- return /* @__PURE__ */ jsx("div", {
1767
- className: "sb-token-detail__stroke-style-fallback",
1768
- children: "strokeStyle value could not be previewed."
1769
- });
2139
+ return fallback;
1770
2140
  }
1771
- function asDashLengths(raw) {
1772
- if (!Array.isArray(raw)) return [];
1773
- const out = [];
1774
- for (const entry of raw) {
1775
- if (typeof entry === "number") {
1776
- out.push(entry);
1777
- continue;
1778
- }
1779
- if (entry && typeof entry === "object") {
1780
- const e = entry;
1781
- if (typeof e.value === "number") out.push(e.value);
2141
+ function asEasing(raw, themeTokens, fallback) {
2142
+ const direct = extractCubicBezier(raw);
2143
+ if (direct) return direct;
2144
+ if (typeof raw === "string") {
2145
+ const match = raw.match(/^\{([^}]+)\}$/);
2146
+ if (match && match[1]) {
2147
+ const referenced = themeTokens[match[1]];
2148
+ const resolved = extractCubicBezier(referenced?.$value);
2149
+ if (resolved) return resolved;
1782
2150
  }
1783
2151
  }
1784
- return out;
2152
+ return fallback;
1785
2153
  }
1786
- function TransitionSample({ transition }) {
1787
- const reduced = usePrefersReducedMotion();
2154
+ function resolveMotionSpec(token, themeTokens) {
2155
+ if (!token) return null;
2156
+ const type = token.$type;
2157
+ if (type === "transition") {
2158
+ const v = token.$value ?? {};
2159
+ return {
2160
+ durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
2161
+ easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
2162
+ };
2163
+ }
2164
+ if (type === "duration") {
2165
+ const durationMs = extractDurationMs(token.$value);
2166
+ if (!Number.isFinite(durationMs)) return null;
2167
+ return {
2168
+ durationMs,
2169
+ easing: DEFAULT_EASING
2170
+ };
2171
+ }
2172
+ if (type === "cubicBezier") {
2173
+ const easing = extractCubicBezier(token.$value);
2174
+ if (!easing) return null;
2175
+ return {
2176
+ durationMs: DEFAULT_DURATION_MS,
2177
+ easing
2178
+ };
2179
+ }
2180
+ return null;
2181
+ }
2182
+ function MotionSample({ path, speed = 1, runKey = 0 }) {
2183
+ const { resolved } = useProject();
2184
+ const reducedMotion = usePrefersReducedMotion();
2185
+ const spec = useMemo(() => resolveMotionSpec(resolved[path], resolved), [resolved, path]);
2186
+ const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
2187
+ const easing = spec?.easing ?? DEFAULT_EASING;
2188
+ const scaledDuration = Math.max(1, durationMs / speed);
1788
2189
  const [phase, setPhase] = useState(0);
1789
2190
  useEffect(() => {
1790
- if (reduced) return;
2191
+ if (reducedMotion) return;
2192
+ setPhase(0);
1791
2193
  const id = requestAnimationFrame(() => setPhase(1));
1792
2194
  const loop = window.setInterval(() => {
1793
2195
  setPhase((p) => p === 0 ? 1 : 0);
1794
- }, 1200);
2196
+ }, scaledDuration * 2);
1795
2197
  return () => {
1796
2198
  cancelAnimationFrame(id);
1797
2199
  window.clearInterval(loop);
1798
2200
  };
1799
- }, [reduced]);
1800
- if (reduced) return /* @__PURE__ */ jsx("div", {
1801
- className: "sb-token-detail__reduced-motion",
2201
+ }, [
2202
+ scaledDuration,
2203
+ runKey,
2204
+ reducedMotion
2205
+ ]);
2206
+ if (reducedMotion) return /* @__PURE__ */ jsx("div", {
2207
+ style: styles.reducedMotion,
1802
2208
  children: "Animation suppressed by `prefers-reduced-motion: reduce`."
1803
2209
  });
1804
2210
  return /* @__PURE__ */ jsx("div", {
1805
- className: "sb-token-detail__motion-track",
2211
+ style: styles.track,
1806
2212
  children: /* @__PURE__ */ jsx("div", {
1807
- className: "sb-token-detail__motion-ball",
1808
2213
  style: {
1809
- left: phase === 1 ? "calc(100% - 28px)" : "4px",
1810
- transition
2214
+ ...styles.ball,
2215
+ left: phase === 1 ? "calc(100% - 32px)" : "4px",
2216
+ transition: `left ${scaledDuration}ms ${easing}`
1811
2217
  },
1812
2218
  "aria-hidden": true
1813
2219
  })
1814
2220
  });
1815
2221
  }
1816
2222
  //#endregion
1817
- //#region src/token-detail/ConsumerOutput.tsx
1818
- function ConsumerOutput({ path }) {
1819
- const { token, cssVar, activeAxes } = useTokenDetailData(path);
1820
- if (!token) return null;
1821
- const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
1822
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1823
- /* @__PURE__ */ jsx("div", {
1824
- className: "sb-token-detail__section-header",
1825
- children: "Consumer output"
1826
- }),
1827
- tupleLabel && /* @__PURE__ */ jsxs("div", {
1828
- className: "sb-token-detail__tuple-indicator",
1829
- children: ["Active tuple: ", /* @__PURE__ */ jsx("strong", { children: tupleLabel })]
1830
- }),
1831
- /* @__PURE__ */ jsx(OutputRow, {
1832
- label: "Path",
1833
- value: path,
1834
- testId: "consumer-output-path"
1835
- }),
1836
- /* @__PURE__ */ jsx(OutputRow, {
1837
- label: "CSS",
1838
- value: cssVar,
1839
- testId: "consumer-output-css"
1840
- })
1841
- ] });
2223
+ //#region src/MotionPreview.tsx
2224
+ const SPEEDS = [
2225
+ .25,
2226
+ .5,
2227
+ 1,
2228
+ 2
2229
+ ];
2230
+ function formatSpec(row) {
2231
+ switch (row.kind) {
2232
+ case "transition": return `transition · ${Math.round(row.durationMs)}ms · ${row.easing}`;
2233
+ case "duration": return `duration · ${Math.round(row.durationMs)}ms`;
2234
+ case "cubicBezier": return `cubicBezier · ${row.easing}`;
2235
+ }
1842
2236
  }
1843
- function OutputRow({ label, value, testId }) {
2237
+ function MotionPreview({ filter, caption }) {
2238
+ const project = useProject();
2239
+ const { resolved, activeTheme, cssVarPrefix } = project;
2240
+ const [speed, setSpeed] = useState(1);
2241
+ const [run, setRun] = useState(0);
2242
+ const reducedMotion = usePrefersReducedMotion();
2243
+ const rows = useMemo(() => {
2244
+ const collected = [];
2245
+ for (const [path, token] of Object.entries(resolved)) {
2246
+ if (filter && !globMatch(path, filter)) continue;
2247
+ if (!filter && ![
2248
+ "transition",
2249
+ "duration",
2250
+ "cubicBezier"
2251
+ ].includes(token.$type ?? "")) continue;
2252
+ const kind = token.$type;
2253
+ if (!kind) continue;
2254
+ const spec = resolveMotionSpec(token, resolved);
2255
+ if (!spec) continue;
2256
+ collected.push({
2257
+ path,
2258
+ cssVar: resolveCssVar(path, project),
2259
+ durationMs: spec.durationMs,
2260
+ easing: spec.easing,
2261
+ kind
2262
+ });
2263
+ }
2264
+ collected.sort((a, b) => {
2265
+ if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
2266
+ return a.path.localeCompare(b.path, void 0, { numeric: true });
2267
+ });
2268
+ return collected;
2269
+ }, [
2270
+ resolved,
2271
+ filter,
2272
+ project
2273
+ ]);
2274
+ const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2275
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2276
+ ...themeAttrs(cssVarPrefix, activeTheme),
2277
+ children: /* @__PURE__ */ jsx("div", {
2278
+ className: "sb-block__empty",
2279
+ children: "No motion tokens match this filter."
2280
+ })
2281
+ });
1844
2282
  return /* @__PURE__ */ jsxs("div", {
1845
- className: "sb-token-detail__consumer-row",
2283
+ ...themeAttrs(cssVarPrefix, activeTheme),
1846
2284
  children: [
1847
- /* @__PURE__ */ jsx("span", {
1848
- className: "sb-token-detail__consumer-row-label",
1849
- children: label
2285
+ /* @__PURE__ */ jsx("div", {
2286
+ className: "sb-block__caption",
2287
+ children: captionText
1850
2288
  }),
1851
- /* @__PURE__ */ jsx("code", {
1852
- className: "sb-token-detail__consumer-row-value",
1853
- "data-testid": testId,
1854
- children: value
2289
+ /* @__PURE__ */ jsxs("div", {
2290
+ className: "sb-motion-preview__controls",
2291
+ children: [
2292
+ /* @__PURE__ */ jsx("span", {
2293
+ className: "sb-motion-preview__control-label",
2294
+ children: "Speed"
2295
+ }),
2296
+ SPEEDS.map((s) => /* @__PURE__ */ jsxs("button", {
2297
+ type: "button",
2298
+ className: cx("sb-motion-preview__speed-btn", { "sb-motion-preview__speed-btn--active": s === speed }),
2299
+ onClick: () => setSpeed(s),
2300
+ children: [s, "×"]
2301
+ }, s)),
2302
+ /* @__PURE__ */ jsx("button", {
2303
+ type: "button",
2304
+ className: "sb-motion-preview__replay-btn",
2305
+ onClick: () => setRun((n) => n + 1),
2306
+ disabled: reducedMotion,
2307
+ title: reducedMotion ? "Disabled by prefers-reduced-motion" : "Replay all",
2308
+ children: "↻ Replay"
2309
+ })
2310
+ ]
1855
2311
  }),
1856
- /* @__PURE__ */ jsx(CopyButton, {
1857
- text: value,
1858
- testId: `${testId}-copy`
1859
- })
2312
+ rows.map((row) => /* @__PURE__ */ jsxs("div", {
2313
+ className: "sb-motion-preview__row",
2314
+ children: [
2315
+ /* @__PURE__ */ jsxs("div", {
2316
+ className: "sb-motion-preview__meta",
2317
+ children: [/* @__PURE__ */ jsx("span", {
2318
+ className: "sb-motion-preview__path",
2319
+ children: row.path
2320
+ }), /* @__PURE__ */ jsx("span", {
2321
+ className: "sb-motion-preview__specs",
2322
+ children: formatSpec(row)
2323
+ })]
2324
+ }),
2325
+ /* @__PURE__ */ jsx(MotionSample, {
2326
+ path: row.path,
2327
+ speed,
2328
+ runKey: run
2329
+ }),
2330
+ /* @__PURE__ */ jsx("span", {
2331
+ className: "sb-motion-preview__css-var",
2332
+ children: row.cssVar
2333
+ })
2334
+ ]
2335
+ }, row.path))
1860
2336
  ]
1861
2337
  });
1862
2338
  }
1863
- function CopyButton({ text, testId }) {
1864
- const [copied, setCopied] = useState(false);
1865
- return /* @__PURE__ */ jsx("button", {
1866
- type: "button",
1867
- className: "sb-token-detail__consumer-row-copy",
1868
- "data-testid": testId,
1869
- onClick: () => {
1870
- copyToClipboard(text).then((ok) => {
1871
- if (!ok) return;
1872
- setCopied(true);
1873
- window.setTimeout(() => setCopied(false), 1200);
1874
- });
1875
- },
1876
- children: copied ? "Copied" : "Copy"
2339
+ //#endregion
2340
+ //#region src/provider.tsx
2341
+ /**
2342
+ * Wraps a tree of blocks with the token data they need to render.
2343
+ *
2344
+ * The Storybook addon's preview decorator mounts this automatically, so
2345
+ * story/MDX authors typically never see it. Outside Storybook — unit
2346
+ * tests, custom React apps, non-Storybook doc sites — consumers construct
2347
+ * a {@link ProjectSnapshot} (often imported from a JSON file) and wrap
2348
+ * their blocks in this provider.
2349
+ */
2350
+ function SwatchbookProvider({ value, children }) {
2351
+ return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
2352
+ value,
2353
+ children
1877
2354
  });
1878
2355
  }
1879
- async function copyToClipboard(text) {
1880
- if (typeof navigator === "undefined" || !navigator.clipboard) return false;
1881
- try {
1882
- await navigator.clipboard.writeText(text);
1883
- return true;
1884
- } catch {
1885
- return false;
1886
- }
2356
+ /**
2357
+ * Read the current {@link ProjectSnapshot}. Throws if called outside a
2358
+ * {@link SwatchbookProvider}; blocks that need to fall back to the
2359
+ * virtual module go through the internal `useProject()` hook instead.
2360
+ */
2361
+ function useSwatchbookData() {
2362
+ const value = useOptionalSwatchbookData();
2363
+ if (!value) throw new Error("[swatchbook-blocks] useSwatchbookData() called outside <SwatchbookProvider>. Wrap your tree in <SwatchbookProvider value={snapshot}> or render inside a Storybook story.");
2364
+ return value;
1887
2365
  }
1888
2366
  //#endregion
1889
- //#region src/token-detail/TokenHeader.tsx
1890
- function TokenHeader({ path, heading }) {
1891
- const { token, cssVar, activeTheme } = useTokenDetailData(path);
1892
- if (!token) return /* @__PURE__ */ jsxs("div", {
1893
- className: "sb-token-detail__missing",
1894
- children: [
1895
- "Token ",
1896
- /* @__PURE__ */ jsx("code", { children: path }),
1897
- " not found in theme ",
1898
- /* @__PURE__ */ jsx("strong", { children: activeTheme }),
1899
- "."
1900
- ]
2367
+ //#region src/shadow-preview/ShadowSample.tsx
2368
+ const sampleStyle = {
2369
+ width: 120,
2370
+ height: 56,
2371
+ background: SURFACE_RAISED,
2372
+ border: BORDER_FAINT,
2373
+ borderRadius: 6
2374
+ };
2375
+ function ShadowSample({ path }) {
2376
+ const cssVar = resolveCssVar(path, useProject());
2377
+ return /* @__PURE__ */ jsx("div", {
2378
+ style: {
2379
+ ...sampleStyle,
2380
+ boxShadow: cssVar
2381
+ },
2382
+ "aria-hidden": true
1901
2383
  });
1902
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1903
- /* @__PURE__ */ jsx("h3", {
1904
- className: "sb-token-detail__heading",
1905
- children: heading ?? path
1906
- }),
1907
- /* @__PURE__ */ jsxs("div", {
1908
- className: "sb-token-detail__subline",
1909
- children: [token.$type && /* @__PURE__ */ jsx("span", {
1910
- className: "sb-token-detail__type-pill",
1911
- children: token.$type
1912
- }), /* @__PURE__ */ jsx("span", { children: cssVar })]
1913
- }),
1914
- token.$description && /* @__PURE__ */ jsx("p", {
1915
- className: "sb-token-detail__description",
1916
- children: token.$description
1917
- })
1918
- ] });
1919
2384
  }
1920
2385
  //#endregion
1921
- //#region src/token-detail/TokenUsageSnippet.tsx
1922
- function TokenUsageSnippet({ path }) {
1923
- const { token, cssVar } = useTokenDetailData(path);
1924
- if (!token) return null;
1925
- const snippet = `color: ${cssVar};`;
1926
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
1927
- className: "sb-token-detail__section-header",
1928
- children: "Usage"
1929
- }), /* @__PURE__ */ jsxs("div", {
1930
- className: "sb-token-detail__snippet-row",
1931
- children: [/* @__PURE__ */ jsx("code", {
1932
- className: "sb-token-detail__snippet",
1933
- children: snippet
1934
- }), /* @__PURE__ */ jsx(CopyButton$1, {
1935
- value: snippet,
1936
- label: `Copy usage snippet ${snippet}`
1937
- })]
1938
- })] });
2386
+ //#region src/ShadowPreview.tsx
2387
+ function formatDimension(raw) {
2388
+ if (raw == null) return "—";
2389
+ if (typeof raw === "number") return String(raw);
2390
+ if (typeof raw === "string") return raw;
2391
+ if (typeof raw === "object") {
2392
+ const v = raw;
2393
+ if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
2394
+ }
2395
+ return JSON.stringify(raw);
1939
2396
  }
1940
- //#endregion
1941
- //#region src/TokenDetail.tsx
1942
- function TokenDetail({ path, heading }) {
1943
- const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
2397
+ function formatSubColor(raw, format) {
2398
+ if (raw == null) return "—";
2399
+ return formatColor(raw, format).value;
2400
+ }
2401
+ function asLayers(raw) {
2402
+ if (Array.isArray(raw)) return raw;
2403
+ if (raw && typeof raw === "object") return [raw];
2404
+ return [];
2405
+ }
2406
+ function layerKey(path, layer, fallback) {
2407
+ return `${path}|${`${formatDimension(layer.offsetX)},${formatDimension(layer.offsetY)}`}|${formatDimension(layer.blur)}|${formatDimension(layer.spread)}|${fallback}`;
2408
+ }
2409
+ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2410
+ const project = useProject();
2411
+ const { resolved, activeTheme, cssVarPrefix } = project;
1944
2412
  const colorFormat = useColorFormat();
1945
- const theme = themeAttrs(cssVarPrefix, activeTheme);
1946
- if (!token) return /* @__PURE__ */ jsx("div", {
1947
- ...theme,
1948
- className: cx(theme["className"], "sb-token-detail"),
1949
- children: /* @__PURE__ */ jsxs("div", {
1950
- className: "sb-token-detail__missing",
2413
+ const rows = useMemo(() => {
2414
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2415
+ if (token.$type !== "shadow") return false;
2416
+ return globMatch(path, filter);
2417
+ }), {
2418
+ by: sortBy,
2419
+ dir: sortDir
2420
+ }).map(([path, token]) => ({
2421
+ path,
2422
+ cssVar: resolveCssVar(path, project),
2423
+ layers: asLayers(token.$value)
2424
+ }));
2425
+ }, [
2426
+ resolved,
2427
+ filter,
2428
+ project,
2429
+ sortBy,
2430
+ sortDir
2431
+ ]);
2432
+ const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2433
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2434
+ ...themeAttrs(cssVarPrefix, activeTheme),
2435
+ children: /* @__PURE__ */ jsx("div", {
2436
+ className: "sb-block__empty",
2437
+ children: "No shadow tokens match this filter."
2438
+ })
2439
+ });
2440
+ return /* @__PURE__ */ jsxs("div", {
2441
+ ...themeAttrs(cssVarPrefix, activeTheme),
2442
+ children: [/* @__PURE__ */ jsx("div", {
2443
+ className: "sb-block__caption",
2444
+ children: captionText
2445
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2446
+ className: "sb-shadow-preview__row",
2447
+ children: [
2448
+ /* @__PURE__ */ jsxs("div", {
2449
+ className: "sb-shadow-preview__meta",
2450
+ children: [/* @__PURE__ */ jsx("span", {
2451
+ className: "sb-shadow-preview__path",
2452
+ children: row.path
2453
+ }), /* @__PURE__ */ jsx("span", {
2454
+ className: "sb-shadow-preview__css-var",
2455
+ children: row.cssVar
2456
+ })]
2457
+ }),
2458
+ /* @__PURE__ */ jsx("div", {
2459
+ className: "sb-shadow-preview__sample-cell",
2460
+ children: /* @__PURE__ */ jsx(ShadowSample, { path: row.path })
2461
+ }),
2462
+ /* @__PURE__ */ jsx("div", {
2463
+ className: "sb-shadow-preview__breakdown",
2464
+ children: row.layers.length === 1 ? renderLayer(row.layers[0], colorFormat) : row.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, {
2465
+ layer,
2466
+ index: i,
2467
+ total: row.layers.length,
2468
+ colorFormat
2469
+ }, layerKey(row.path, layer, i)))
2470
+ })
2471
+ ]
2472
+ }, row.path))]
2473
+ });
2474
+ }
2475
+ function renderLayer(layer, format) {
2476
+ if (!layer) return [];
2477
+ const entries = [
2478
+ ["offset", `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`],
2479
+ ["blur", formatDimension(layer.blur)],
2480
+ ["spread", formatDimension(layer.spread)],
2481
+ ["color", formatSubColor(layer.color, format)]
2482
+ ];
2483
+ if (layer.inset) entries.push(["inset", String(layer.inset)]);
2484
+ return entries.flatMap(([k, v]) => [/* @__PURE__ */ jsx("span", {
2485
+ className: "sb-shadow-preview__breakdown-key",
2486
+ children: k
2487
+ }, `k-${k}`), /* @__PURE__ */ jsx("span", { children: v }, `v-${k}`)]);
2488
+ }
2489
+ function Layer({ layer, index, total, colorFormat }) {
2490
+ return /* @__PURE__ */ jsxs("div", {
2491
+ className: "sb-shadow-preview__layer",
2492
+ children: [/* @__PURE__ */ jsxs("div", {
2493
+ className: "sb-shadow-preview__layer-header",
1951
2494
  children: [
1952
- "Token ",
1953
- /* @__PURE__ */ jsx("code", { children: path }),
1954
- " not found in theme ",
1955
- /* @__PURE__ */ jsx("strong", { children: activeTheme }),
1956
- "."
2495
+ "layer ",
2496
+ index + 1,
2497
+ " of ",
2498
+ total
1957
2499
  ]
1958
- })
1959
- });
1960
- const isColor = token.$type === "color";
1961
- const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
1962
- const value = formatTokenValue(token.$value, token.$type, colorFormat);
1963
- const outOfGamut = gamut?.outOfGamut ?? false;
1964
- return /* @__PURE__ */ jsxs("div", {
1965
- ...theme,
1966
- className: cx(theme["className"], "sb-token-detail"),
1967
- children: [
1968
- /* @__PURE__ */ jsx(TokenHeader, {
1969
- path,
1970
- ...heading !== void 0 && { heading }
1971
- }),
1972
- /* @__PURE__ */ jsxs("div", {
1973
- className: "sb-token-detail__section-header",
1974
- children: ["Resolved value · ", activeTheme]
1975
- }),
1976
- /* @__PURE__ */ jsx(CompositePreview, { path }),
1977
- /* @__PURE__ */ jsx(CompositeBreakdown, { path }),
1978
- /* @__PURE__ */ jsxs("div", {
1979
- className: "sb-token-detail__chain",
1980
- children: [
1981
- isColor && /* @__PURE__ */ jsx("span", {
1982
- className: "sb-token-detail__swatch",
1983
- style: { background: cssVar },
1984
- "aria-hidden": true
1985
- }),
1986
- /* @__PURE__ */ jsx("span", { children: value }),
1987
- outOfGamut && /* @__PURE__ */ jsx("span", {
1988
- title: "Out of sRGB gamut for this format",
1989
- "aria-label": "out of gamut",
1990
- style: { marginLeft: 6 },
1991
- children: "⚠"
1992
- }),
1993
- /* @__PURE__ */ jsx(CopyButton$1, {
1994
- value,
1995
- label: `Copy value ${value}`
1996
- })
1997
- ]
1998
- }),
1999
- /* @__PURE__ */ jsx(AliasChain, { path }),
2000
- /* @__PURE__ */ jsx(AliasedBy, { path }),
2001
- /* @__PURE__ */ jsx(TokenUsageSnippet, { path }),
2002
- /* @__PURE__ */ jsx(ConsumerOutput, { path }),
2003
- /* @__PURE__ */ jsx(AxisVariance, { path })
2004
- ]
2500
+ }), /* @__PURE__ */ jsx("div", {
2501
+ className: cx("sb-shadow-preview__breakdown", "sb-shadow-preview__layer-breakdown"),
2502
+ children: renderLayer(layer, colorFormat)
2503
+ })]
2005
2504
  });
2006
2505
  }
2007
2506
  //#endregion
2008
- //#region src/internal/DetailOverlay.tsx
2009
- function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
2010
- useEffect(() => {
2011
- const onKey = (e) => {
2012
- if (e.key === "Escape") onClose();
2013
- };
2014
- window.addEventListener("keydown", onKey);
2015
- return () => window.removeEventListener("keydown", onKey);
2016
- }, [onClose]);
2017
- return /* @__PURE__ */ jsx("div", {
2018
- className: "sb-detail-overlay__backdrop",
2019
- onClick: onClose,
2020
- role: "presentation",
2021
- "data-testid": testId,
2022
- children: /* @__PURE__ */ jsxs("div", {
2023
- className: "sb-detail-overlay__panel",
2024
- onClick: (e) => e.stopPropagation(),
2025
- role: "dialog",
2026
- "aria-modal": "true",
2027
- "aria-label": `Token detail for ${path}`,
2028
- children: [/* @__PURE__ */ jsx("button", {
2029
- type: "button",
2030
- className: "sb-detail-overlay__close",
2031
- onClick: onClose,
2032
- "aria-label": "Close",
2033
- "data-testid": `${testId}-close`,
2034
- children: "×"
2035
- }), /* @__PURE__ */ jsx(TokenDetail, { path })]
2036
- })
2037
- });
2507
+ //#region src/StrokeStyleSample.tsx
2508
+ const STRING_STYLES = new Set([
2509
+ "solid",
2510
+ "dashed",
2511
+ "dotted",
2512
+ "double",
2513
+ "groove",
2514
+ "ridge",
2515
+ "outset",
2516
+ "inset"
2517
+ ]);
2518
+ function extractCssStyle(value) {
2519
+ if (typeof value === "string" && STRING_STYLES.has(value)) return value;
2520
+ return null;
2038
2521
  }
2039
- //#endregion
2040
- //#region src/ColorTable.tsx
2041
- function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
2042
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2043
- const [selectedPath, setSelectedPath] = useState(null);
2044
- const [query, setQuery] = useState("");
2045
- const variantIndex = useMemo(() => buildVariantIndex(variants), [variants]);
2522
+ function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2523
+ const project = useProject();
2524
+ const { resolved, activeTheme, cssVarPrefix } = project;
2046
2525
  const rows = useMemo(() => {
2047
2526
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2048
- if (token.$type !== "color") return false;
2527
+ if (token.$type !== "strokeStyle") return false;
2049
2528
  return globMatch(path, filter);
2050
2529
  }), {
2051
2530
  by: sortBy,
2052
2531
  dir: sortDir
2053
- }).map(([path, token]) => {
2054
- const raw = token.$value;
2055
- const hex = formatColor(raw, "hex");
2056
- const hsl = formatColor(raw, "hsl");
2057
- const oklch = formatColor(raw, "oklch");
2058
- const variant = matchVariant(path, variantIndex);
2059
- return {
2060
- path,
2061
- cssVar: makeCssVar(path, cssVarPrefix),
2062
- hex: hex.value,
2063
- hsl: hsl.value,
2064
- oklch: oklch.value,
2065
- hexOutOfGamut: hex.outOfGamut,
2066
- ...token.aliasOf !== void 0 && { aliasOf: token.aliasOf },
2067
- ...variant !== void 0 && { variant }
2068
- };
2069
- });
2532
+ }).map(([path, token]) => ({
2533
+ path,
2534
+ cssVar: resolveCssVar(path, project),
2535
+ displayValue: formatTokenValue(token.$value, token.$type, "raw", project.listing[path]),
2536
+ cssStyle: extractCssStyle(token.$value)
2537
+ }));
2070
2538
  }, [
2071
2539
  resolved,
2072
2540
  filter,
2073
- cssVarPrefix,
2541
+ project,
2074
2542
  sortBy,
2075
- sortDir,
2076
- variantIndex
2077
- ]);
2078
- const visibleRows = useMemo(() => {
2079
- if (!searchable || query.trim() === "") return rows;
2080
- return fuzzyFilter(rows, query, (r) => `${r.path} ${r.hex} ${r.hsl} ${r.oklch}`);
2081
- }, [
2082
- rows,
2083
- query,
2084
- searchable
2543
+ sortDir
2085
2544
  ]);
2086
- const handleRowClick = useCallback((path) => {
2087
- if (onSelect) onSelect(path);
2088
- else setSelectedPath(path);
2089
- }, [onSelect]);
2090
- const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
2091
- const captionText = caption ?? `${rows.length} color${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
2545
+ const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2092
2546
  if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2093
2547
  ...themeAttrs(cssVarPrefix, activeTheme),
2094
2548
  children: /* @__PURE__ */ jsx("div", {
2095
2549
  className: "sb-block__empty",
2096
- children: "No color tokens match this filter."
2550
+ children: "No strokeStyle tokens match this filter."
2097
2551
  })
2098
2552
  });
2099
2553
  return /* @__PURE__ */ jsxs("div", {
2100
2554
  ...themeAttrs(cssVarPrefix, activeTheme),
2101
- children: [
2102
- searchable && /* @__PURE__ */ jsx("div", {
2103
- className: "sb-color-table__search",
2104
- children: /* @__PURE__ */ jsx("input", {
2105
- type: "search",
2106
- className: "sb-color-table__search-input",
2107
- placeholder: "Search colors…",
2108
- value: query,
2109
- onChange: (e) => setQuery(e.target.value),
2110
- "aria-label": "Fuzzy-search colors by path or value",
2111
- "data-testid": "color-table-search"
2112
- })
2113
- }),
2114
- /* @__PURE__ */ jsxs("table", {
2115
- className: "sb-color-table__table",
2116
- children: [
2117
- /* @__PURE__ */ jsx("caption", {
2118
- className: "sb-color-table__caption",
2119
- children: captionText
2120
- }),
2121
- /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2122
- /* @__PURE__ */ jsx("th", {
2123
- className: "sb-color-table__th sb-color-table__th--swatch",
2124
- children: /* @__PURE__ */ jsx("span", {
2125
- className: "sb-color-table__sr-only",
2126
- children: "Swatch"
2127
- })
2128
- }),
2129
- /* @__PURE__ */ jsx("th", {
2130
- className: "sb-color-table__th sb-color-table__th--path",
2131
- children: "Name"
2132
- }),
2133
- /* @__PURE__ */ jsx("th", {
2134
- className: "sb-color-table__th",
2135
- children: "HEX"
2136
- }),
2137
- /* @__PURE__ */ jsx("th", {
2138
- className: "sb-color-table__th",
2139
- children: "HSL"
2140
- }),
2141
- /* @__PURE__ */ jsx("th", {
2142
- className: "sb-color-table__th",
2143
- children: "OKLCH"
2144
- }),
2145
- /* @__PURE__ */ jsx("th", {
2146
- className: "sb-color-table__th",
2147
- children: "CSS var"
2148
- }),
2149
- /* @__PURE__ */ jsx("th", {
2150
- className: "sb-color-table__th",
2151
- children: "Alias"
2152
- })
2153
- ] }) }),
2154
- /* @__PURE__ */ jsxs("tbody", { children: [visibleRows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
2155
- colSpan: 7,
2156
- className: "sb-color-table__td sb-color-table__empty-row",
2157
- children: [
2158
- "No colors match \"",
2159
- query.trim(),
2160
- "\"."
2161
- ]
2162
- }) }), visibleRows.map((row) => /* @__PURE__ */ jsxs("tr", {
2163
- className: "sb-color-table__row",
2164
- onClick: () => handleRowClick(row.path),
2165
- onKeyDown: (e) => {
2166
- if (e.key === "Enter" || e.key === " ") {
2167
- e.preventDefault();
2168
- handleRowClick(row.path);
2169
- }
2170
- },
2171
- tabIndex: 0,
2172
- "aria-label": `Inspect ${row.path}`,
2173
- "data-testid": "color-table-row",
2174
- "data-path": row.path,
2175
- children: [
2176
- /* @__PURE__ */ jsx("td", {
2177
- className: "sb-color-table__td sb-color-table__swatch-cell",
2178
- children: /* @__PURE__ */ jsx("span", {
2179
- className: "sb-color-table__swatch",
2180
- style: { background: row.cssVar },
2181
- "aria-hidden": true
2182
- })
2183
- }),
2184
- /* @__PURE__ */ jsxs("td", {
2185
- className: cx("sb-color-table__td", "sb-color-table__path"),
2186
- children: [/* @__PURE__ */ jsx("span", {
2187
- className: "sb-color-table__path-text",
2188
- children: row.path
2189
- }), row.variant !== void 0 && /* @__PURE__ */ jsx("span", {
2190
- className: "sb-color-table__variant-pill",
2191
- "data-variant": row.variant,
2192
- "data-testid": "color-table-variant",
2193
- children: row.variant
2194
- })]
2195
- }),
2196
- /* @__PURE__ */ jsx(ValueCell, {
2197
- value: row.hex,
2198
- label: `Copy HEX ${row.hex}`,
2199
- children: row.hexOutOfGamut && /* @__PURE__ */ jsx("span", {
2200
- title: "Out of sRGB gamut",
2201
- "aria-label": "out of gamut",
2202
- className: "sb-color-table__gamut-warn",
2203
- children: "⚠"
2204
- })
2205
- }),
2206
- /* @__PURE__ */ jsx(ValueCell, {
2207
- value: row.hsl,
2208
- label: `Copy HSL ${row.hsl}`
2209
- }),
2210
- /* @__PURE__ */ jsx(ValueCell, {
2211
- value: row.oklch,
2212
- label: `Copy OKLCH ${row.oklch}`
2213
- }),
2214
- /* @__PURE__ */ jsx(ValueCell, {
2215
- value: row.cssVar,
2216
- label: `Copy CSS var ${row.cssVar}`
2217
- }),
2218
- /* @__PURE__ */ jsx("td", {
2219
- className: "sb-color-table__td sb-color-table__alias",
2220
- children: row.aliasOf ? /* @__PURE__ */ jsx("span", {
2221
- className: "sb-color-table__alias-text",
2222
- children: row.aliasOf
2223
- }) : /* @__PURE__ */ jsx("span", {
2224
- className: "sb-color-table__alias-empty",
2225
- "aria-hidden": true,
2226
- children: "—"
2227
- })
2228
- })
2229
- ]
2230
- }, row.path))] })
2231
- ]
2232
- }),
2233
- selectedPath !== null && /* @__PURE__ */ jsx(DetailOverlay, {
2234
- path: selectedPath,
2235
- onClose: () => setSelectedPath(null),
2236
- testId: "color-table-overlay"
2237
- })
2238
- ]
2239
- });
2240
- }
2241
- function ValueCell({ value, label, children }) {
2242
- return /* @__PURE__ */ jsxs("td", {
2243
- className: "sb-color-table__td sb-color-table__value-cell",
2244
- children: [
2245
- /* @__PURE__ */ jsx("span", {
2246
- className: "sb-color-table__value-text",
2247
- title: value,
2248
- children: value
2249
- }),
2250
- children,
2251
- /* @__PURE__ */ jsx("span", {
2252
- className: "sb-color-table__copy-wrap",
2253
- onClick: (e) => e.stopPropagation(),
2254
- onKeyDown: (e) => e.stopPropagation(),
2255
- role: "presentation",
2256
- children: /* @__PURE__ */ jsx(CopyButton$1, {
2257
- value,
2258
- label,
2259
- className: "sb-color-table__copy"
2555
+ children: [/* @__PURE__ */ jsx("div", {
2556
+ className: "sb-block__caption",
2557
+ children: captionText
2558
+ }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2559
+ className: "sb-stroke-style-sample__row",
2560
+ children: [
2561
+ /* @__PURE__ */ jsxs("div", {
2562
+ className: "sb-stroke-style-sample__meta",
2563
+ children: [/* @__PURE__ */ jsx("span", {
2564
+ className: "sb-stroke-style-sample__path",
2565
+ children: row.path
2566
+ }), /* @__PURE__ */ jsx("span", {
2567
+ className: "sb-stroke-style-sample__value",
2568
+ children: row.displayValue
2569
+ })]
2570
+ }),
2571
+ row.cssStyle ? /* @__PURE__ */ jsx("div", {
2572
+ className: "sb-stroke-style-sample__line",
2573
+ style: { borderTopStyle: row.cssStyle },
2574
+ "aria-hidden": true
2575
+ }) : /* @__PURE__ */ jsx("span", {
2576
+ className: "sb-stroke-style-sample__object-fallback",
2577
+ children: "Object-form (dashArray + lineCap) — no pure CSS `border-style` equivalent."
2578
+ }),
2579
+ /* @__PURE__ */ jsx("span", {
2580
+ className: "sb-stroke-style-sample__css-var",
2581
+ children: row.cssVar
2260
2582
  })
2261
- })
2262
- ]
2583
+ ]
2584
+ }, row.path))]
2263
2585
  });
2264
2586
  }
2265
- /**
2266
- * Pre-sort the variants map by descending suffix length so the first
2267
- * `endsWith` hit during matching is always the longest. Empty suffixes are
2268
- * dropped — they'd match every path and make the feature meaningless.
2269
- */
2270
- function buildVariantIndex(variants) {
2271
- if (!variants) return [];
2272
- const entries = [];
2273
- for (const [label, suffix] of Object.entries(variants)) {
2274
- if (suffix.length === 0) continue;
2275
- entries.push({
2276
- label,
2277
- suffix
2278
- });
2279
- }
2280
- entries.sort((a, b) => b.suffix.length - a.suffix.length);
2281
- return entries;
2282
- }
2283
- /**
2284
- * Resolve the variant label for a token path, if any. The leaf (last
2285
- * dot-segment) must either equal the suffix outright (`hi.disabled`
2286
- * matches suffix `disabled`) or end in `-<suffix>` (`hi-d` matches `d`).
2287
- * The leading hyphen is required for the tail form, so suffix `h` matches
2288
- * `hi-h` but not `highlight` or `neutral-900` — the whole trailing token
2289
- * has to be the suffix, not a character within it. Entries are tried
2290
- * longest-first, so `h-dark` wins over `dark` when both are configured
2291
- * and the path ends in `-h-dark`.
2292
- */
2293
- function matchVariant(path, variantIndex) {
2294
- if (variantIndex.length === 0) return void 0;
2295
- const leaf = path.split(".").at(-1) ?? path;
2296
- for (const entry of variantIndex) if (leaf === entry.suffix || leaf.endsWith(`-${entry.suffix}`)) return entry.label;
2297
- }
2298
2587
  //#endregion
2299
- //#region src/Diagnostics.tsx
2300
- const severityLabel = {
2301
- error: "ERROR",
2302
- warn: "WARN",
2303
- info: "INFO"
2304
- };
2305
- function summaryText(diagnostics) {
2306
- if (diagnostics.length === 0) return "✔ OK · no diagnostics";
2307
- const counts = {
2308
- error: 0,
2309
- warn: 0,
2310
- info: 0
2588
+ //#region src/token-detail/internal.ts
2589
+ function useTokenDetailData(path) {
2590
+ const project = useProject();
2591
+ const { activeTheme, activeAxes, axes, themes, themesResolved, resolved, cssVarPrefix } = project;
2592
+ const typedResolved = resolved;
2593
+ return {
2594
+ token: typedResolved[path],
2595
+ cssVar: resolveCssVar(path, project),
2596
+ activeTheme,
2597
+ activeAxes,
2598
+ axes,
2599
+ themes,
2600
+ themesResolved,
2601
+ resolved: typedResolved,
2602
+ cssVarPrefix
2311
2603
  };
2312
- for (const d of diagnostics) counts[d.severity] += 1;
2313
- const parts = [];
2314
- if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
2315
- if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
2316
- if (counts.info > 0) parts.push(`${counts.info} info`);
2317
- return parts.join(" · ");
2318
- }
2319
- function diagnosticKey(d, i) {
2320
- return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
2321
- }
2322
- function summaryVariant(diagnostics) {
2323
- if (diagnostics.length === 0) return "ok";
2324
- if (diagnostics.some((d) => d.severity === "error")) return "error";
2325
- if (diagnostics.some((d) => d.severity === "warn")) return "warn";
2326
- return null;
2327
2604
  }
2328
- /**
2329
- * Render the project's load diagnostics — parser errors, resolver warnings,
2330
- * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
2331
- * when the project carries errors or warnings; stays collapsed for clean
2332
- * loads and info-only loads.
2333
- *
2334
- * Replaces the diagnostics section from the addon's (now-retired) Design
2335
- * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
2336
- * on their own MDX pages.
2337
- */
2338
- function Diagnostics({ caption } = {}) {
2339
- const { activeTheme, cssVarPrefix, diagnostics } = useProject();
2340
- const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
2341
- const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
2342
- const variant = summaryVariant(diagnostics);
2343
- return /* @__PURE__ */ jsx("div", {
2344
- ...themeAttrs(cssVarPrefix, activeTheme),
2345
- "data-testid": "diagnostics",
2346
- children: /* @__PURE__ */ jsxs("details", {
2347
- open: hasErrorsOrWarnings,
2348
- children: [/* @__PURE__ */ jsx("summary", {
2349
- className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
2350
- children: headingText
2351
- }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
2352
- className: "sb-diagnostics__list",
2353
- children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
2354
- className: "sb-diagnostics__row",
2355
- children: [/* @__PURE__ */ jsx("span", {
2356
- className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
2357
- children: severityLabel[d.severity]
2358
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
2359
- className: "sb-diagnostics__meta",
2360
- children: [
2361
- d.group,
2362
- d.filename,
2363
- d.line ? `:${d.line}` : ""
2364
- ].filter(Boolean).join(" · ")
2365
- })] })]
2366
- }, diagnosticKey(d, i)))
2605
+ //#endregion
2606
+ //#region src/token-detail/AliasChain.tsx
2607
+ function AliasChain({ path }) {
2608
+ const { token } = useTokenDetailData(path);
2609
+ const chain = useMemo(() => {
2610
+ if (!token) return [];
2611
+ if (Array.isArray(token.aliasChain) && token.aliasChain.length > 0) return [path, ...token.aliasChain];
2612
+ if (typeof token.aliasOf === "string") return [path, token.aliasOf];
2613
+ return [path];
2614
+ }, [token, path]);
2615
+ if (chain.length <= 1) return null;
2616
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2617
+ className: "sb-token-detail__section-header",
2618
+ children: "Alias chain"
2619
+ }), /* @__PURE__ */ jsx("div", {
2620
+ className: "sb-token-detail__chain",
2621
+ children: chain.map((step, i) => /* @__PURE__ */ jsxs("span", {
2622
+ className: "sb-token-detail__chain",
2623
+ children: [/* @__PURE__ */ jsx("span", {
2624
+ className: "sb-token-detail__chain-node",
2625
+ children: step
2626
+ }), i < chain.length - 1 && /* @__PURE__ */ jsx("span", {
2627
+ className: "sb-token-detail__arrow",
2628
+ children: ""
2367
2629
  })]
2368
- })
2369
- });
2630
+ }, step))
2631
+ })] });
2370
2632
  }
2371
2633
  //#endregion
2372
- //#region src/dimension-scale/DimensionBar.tsx
2373
- const MAX_RENDER_PX$1 = 480;
2374
- const styles$1 = {
2375
- bar: {
2376
- height: 14,
2377
- background: "var(--swatchbook-accent-bg, #3b82f6)",
2378
- borderRadius: 2,
2379
- minWidth: 1
2380
- },
2381
- radiusSample: {
2382
- width: 56,
2383
- height: 56,
2384
- background: "var(--swatchbook-accent-bg, #3b82f6)",
2385
- border: BORDER_STRONG
2386
- },
2387
- sizeSample: {
2388
- background: "var(--swatchbook-accent-bg, #3b82f6)",
2389
- border: BORDER_STRONG,
2390
- minWidth: 1,
2391
- minHeight: 1
2392
- }
2634
+ //#region src/token-detail/AliasedBy.tsx
2635
+ const ALIASED_BY_DEPTH_CAP = 6;
2636
+ const GROUP_RANK = {
2637
+ ref: 0,
2638
+ sys: 1
2393
2639
  };
2394
- /**
2395
- * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
2396
- * purpose of deciding whether to cap the rendered bar. Returns `NaN` for
2397
- * units we can't reasonably approximate (ex / ch / %), which the caller
2398
- * treats as "render at cssVar but don't cap".
2399
- */
2400
- function toPixels$1(raw) {
2401
- if (raw == null || typeof raw !== "object") return NaN;
2402
- const v = raw;
2403
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
2404
- switch (v.unit) {
2405
- case "px": return v.value;
2406
- case "rem":
2407
- case "em": return v.value * 16;
2408
- default: return NaN;
2409
- }
2640
+ function AliasedBy({ path }) {
2641
+ const { resolved } = useTokenDetailData(path);
2642
+ const tree = useMemo(() => buildAliasedByTree(path, resolved), [path, resolved]);
2643
+ const truncated = useMemo(() => treeHasTruncation(tree), [tree]);
2644
+ if (tree.length === 0) return null;
2645
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2646
+ /* @__PURE__ */ jsx("div", {
2647
+ className: "sb-token-detail__section-header",
2648
+ children: "Aliased by"
2649
+ }),
2650
+ /* @__PURE__ */ jsx("ul", {
2651
+ className: "sb-token-detail__aliased-by-list",
2652
+ children: tree.map((node) => /* @__PURE__ */ jsx(AliasedByRow, {
2653
+ node,
2654
+ depth: 0
2655
+ }, node.path))
2656
+ }),
2657
+ truncated && /* @__PURE__ */ jsxs("div", {
2658
+ className: "sb-token-detail__aliased-by-truncated",
2659
+ children: [
2660
+ "Further descendants truncated at depth ",
2661
+ ALIASED_BY_DEPTH_CAP,
2662
+ "."
2663
+ ]
2664
+ })
2665
+ ] });
2410
2666
  }
2411
- function DimensionBar({ path, kind = "length" }) {
2412
- const { resolved, cssVarPrefix } = useProject();
2413
- const cssVar = makeCssVar(path, cssVarPrefix);
2414
- const token = resolved[path];
2415
- const pxValue = toPixels$1(token?.$value);
2416
- const cappedValue = Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX$1 ? `${MAX_RENDER_PX$1}px` : cssVar;
2417
- switch (kind) {
2418
- case "radius": return /* @__PURE__ */ jsx("div", {
2419
- style: {
2420
- ...styles$1.radiusSample,
2421
- borderRadius: cssVar
2422
- },
2423
- "aria-hidden": true
2424
- });
2425
- case "size": return /* @__PURE__ */ jsx("div", {
2426
- style: {
2427
- ...styles$1.sizeSample,
2428
- width: cappedValue,
2429
- height: cappedValue
2430
- },
2431
- "aria-hidden": true
2432
- });
2433
- default: return /* @__PURE__ */ jsx("div", {
2434
- style: {
2435
- ...styles$1.bar,
2436
- width: cappedValue
2437
- },
2438
- "aria-hidden": true
2439
- });
2667
+ function AliasedByRow({ node, depth }) {
2668
+ return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("div", {
2669
+ className: "sb-token-detail__aliased-by-row",
2670
+ style: { paddingLeft: depth * 16 },
2671
+ children: /* @__PURE__ */ jsx("span", {
2672
+ className: "sb-token-detail__chain-node",
2673
+ children: node.path
2674
+ })
2675
+ }), node.children.length > 0 && /* @__PURE__ */ jsx("ul", {
2676
+ className: "sb-token-detail__aliased-by-list",
2677
+ children: node.children.map((child) => /* @__PURE__ */ jsx(AliasedByRow, {
2678
+ node: child,
2679
+ depth: depth + 1
2680
+ }, child.path))
2681
+ })] });
2682
+ }
2683
+ function buildAliasedByTree(rootPath, resolved) {
2684
+ const direct = resolved[rootPath]?.aliasedBy;
2685
+ if (!direct || direct.length === 0) return [];
2686
+ const visited = new Set([rootPath]);
2687
+ return sortPaths(direct).map((p) => walk(p, resolved, visited, 1));
2688
+ }
2689
+ function walk(path, resolved, visited, depth) {
2690
+ if (visited.has(path)) return {
2691
+ path,
2692
+ children: []
2693
+ };
2694
+ visited.add(path);
2695
+ const parents = resolved[path]?.aliasedBy;
2696
+ if (!parents || parents.length === 0) return {
2697
+ path,
2698
+ children: []
2699
+ };
2700
+ if (depth >= ALIASED_BY_DEPTH_CAP) return {
2701
+ path,
2702
+ children: [],
2703
+ truncated: true
2704
+ };
2705
+ return {
2706
+ path,
2707
+ children: sortPaths(parents).map((p) => walk(p, resolved, visited, depth + 1))
2708
+ };
2709
+ }
2710
+ function sortPaths(paths) {
2711
+ return paths.toSorted((a, b) => {
2712
+ const ra = GROUP_RANK[a.split(".")[0] ?? ""] ?? 2;
2713
+ const rb = GROUP_RANK[b.split(".")[0] ?? ""] ?? 2;
2714
+ return ra !== rb ? ra - rb : a.localeCompare(b, void 0, { numeric: true });
2715
+ });
2716
+ }
2717
+ function treeHasTruncation(nodes) {
2718
+ for (const n of nodes) {
2719
+ if (n.truncated) return true;
2720
+ if (treeHasTruncation(n.children)) return true;
2440
2721
  }
2722
+ return false;
2441
2723
  }
2442
2724
  //#endregion
2443
- //#region src/DimensionScale.tsx
2444
- const MAX_RENDER_PX = 480;
2445
- function toPixels(raw) {
2446
- if (raw == null || typeof raw !== "object") return NaN;
2447
- const v = raw;
2448
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
2449
- switch (v.unit) {
2450
- case "px": return v.value;
2451
- case "rem":
2452
- case "em": return v.value * 16;
2453
- default: return NaN;
2725
+ //#region src/token-detail/AxisVariance.tsx
2726
+ function AxisVariance({ path }) {
2727
+ const { token, cssVar, axes, themes, themesResolved, activeAxes, cssVarPrefix } = useTokenDetailData(path);
2728
+ const colorFormat = useColorFormat();
2729
+ const tokenType = token?.$type;
2730
+ const isColor = tokenType === "color";
2731
+ const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2732
+ const variance = useMemo(() => {
2733
+ const result = analyzeAxisVariance(path, axes, themes, themesResolved);
2734
+ return {
2735
+ kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
2736
+ varyingAxes: result.varyingAxes
2737
+ };
2738
+ }, [
2739
+ path,
2740
+ axes,
2741
+ themes,
2742
+ themesResolved
2743
+ ]);
2744
+ if (themes.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2745
+ if (variance.kind === "constant") {
2746
+ const anyTheme = themes[0];
2747
+ const value = anyTheme ? formatFn(themesResolved[anyTheme.name]?.[path]) : "—";
2748
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2749
+ className: "sb-token-detail__section-header",
2750
+ children: "Values across axes"
2751
+ }), /* @__PURE__ */ jsx("table", {
2752
+ className: "sb-token-detail__theme-table",
2753
+ "data-testid": "token-detail-values",
2754
+ children: /* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx("tr", {
2755
+ className: "sb-token-detail__theme-row",
2756
+ children: /* @__PURE__ */ jsxs("td", {
2757
+ className: "sb-token-detail__theme-cell",
2758
+ "data-testid": "token-detail-constant",
2759
+ children: [
2760
+ isColor && /* @__PURE__ */ jsx("span", {
2761
+ className: "sb-token-detail__swatch",
2762
+ style: { background: cssVar },
2763
+ "aria-hidden": true
2764
+ }),
2765
+ value,
2766
+ /* @__PURE__ */ jsxs("span", {
2767
+ style: {
2768
+ opacity: .6,
2769
+ marginLeft: 8
2770
+ },
2771
+ children: [
2772
+ "same across all ",
2773
+ themes.length,
2774
+ " tuples"
2775
+ ]
2776
+ })
2777
+ ]
2778
+ })
2779
+ }) })
2780
+ })] });
2454
2781
  }
2455
- }
2456
- function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
2457
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2458
- const rows = useMemo(() => {
2459
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2460
- if (token.$type !== "dimension") return false;
2461
- return globMatch(path, filter);
2462
- }), {
2463
- by: sortBy,
2464
- dir: sortDir
2465
- }).map(([path, token]) => {
2466
- const pxValue = toPixels(token.$value);
2782
+ if (variance.kind === "one-axis") {
2783
+ const axisName = variance.varyingAxes[0];
2784
+ if (!axisName) return /* @__PURE__ */ jsx(Fragment$1, {});
2785
+ const axis = axes.find((a) => a.name === axisName);
2786
+ if (!axis) return /* @__PURE__ */ jsx(Fragment$1, {});
2787
+ const contextValues = axis.contexts.map((ctx) => {
2788
+ const target = {
2789
+ ...activeAxes,
2790
+ [axisName]: ctx
2791
+ };
2792
+ const name = themes.find((t) => {
2793
+ const input = t.input;
2794
+ return Object.keys(input).every((k) => input[k] === target[k]);
2795
+ })?.name ?? "";
2467
2796
  return {
2468
- path,
2469
- cssVar: makeCssVar(path, cssVarPrefix),
2470
- displayValue: formatTokenValue(token.$value, token.$type, "raw"),
2471
- pxValue,
2472
- capped: Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX
2797
+ ctx,
2798
+ themeName: name,
2799
+ value: name ? formatFn(themesResolved[name]?.[path]) : ""
2473
2800
  };
2474
2801
  });
2475
- }, [
2476
- resolved,
2477
- filter,
2478
- cssVarPrefix,
2479
- sortBy,
2480
- sortDir
2481
- ]);
2482
- const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2483
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2484
- ...themeAttrs(cssVarPrefix, activeTheme),
2485
- children: /* @__PURE__ */ jsx("div", {
2486
- className: "sb-block__empty",
2487
- children: "No dimension tokens match this filter."
2488
- })
2489
- });
2490
- return /* @__PURE__ */ jsxs("div", {
2491
- ...themeAttrs(cssVarPrefix, activeTheme),
2492
- children: [/* @__PURE__ */ jsx("div", {
2493
- className: "sb-block__caption",
2494
- children: captionText
2495
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2496
- className: "sb-dimension-scale__row",
2497
- children: [
2498
- /* @__PURE__ */ jsxs("div", {
2499
- className: "sb-dimension-scale__meta",
2500
- children: [/* @__PURE__ */ jsx("span", {
2501
- className: "sb-dimension-scale__path",
2502
- children: row.path
2503
- }), /* @__PURE__ */ jsx("span", {
2504
- className: "sb-dimension-scale__specs",
2505
- children: row.displayValue
2506
- })]
2507
- }),
2508
- /* @__PURE__ */ jsxs("div", {
2509
- className: "sb-dimension-scale__visual-cell",
2510
- children: [/* @__PURE__ */ jsx(DimensionBar, {
2511
- path: row.path,
2512
- kind
2513
- }), row.capped && /* @__PURE__ */ jsxs("span", {
2514
- className: "sb-dimension-scale__cap",
2515
- children: [
2516
- "capped at ",
2517
- MAX_RENDER_PX,
2518
- "px"
2519
- ]
2520
- })]
2521
- }),
2522
- /* @__PURE__ */ jsx("span", {
2523
- className: "sb-dimension-scale__css-var",
2524
- children: row.cssVar
2525
- })
2802
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
2803
+ className: "sb-token-detail__section-header",
2804
+ children: ["Varies with ", axisName]
2805
+ }), /* @__PURE__ */ jsx("table", {
2806
+ className: "sb-token-detail__theme-table",
2807
+ "data-testid": "token-detail-values",
2808
+ children: /* @__PURE__ */ jsx("tbody", { children: contextValues.map((row) => /* @__PURE__ */ jsxs("tr", {
2809
+ className: "sb-token-detail__theme-row",
2810
+ "data-axis": axisName,
2811
+ "data-context": row.ctx,
2812
+ children: [/* @__PURE__ */ jsx("td", {
2813
+ className: "sb-token-detail__theme-cell",
2814
+ style: { width: "30%" },
2815
+ children: row.ctx
2816
+ }), /* @__PURE__ */ jsxs("td", {
2817
+ className: "sb-token-detail__theme-cell",
2818
+ children: [isColor && row.themeName && /* @__PURE__ */ jsx("span", {
2819
+ className: "sb-token-detail__swatch",
2820
+ style: { background: cssVar },
2821
+ [dataAttr(cssVarPrefix, "theme")]: row.themeName,
2822
+ "aria-hidden": true
2823
+ }), row.value]
2824
+ })]
2825
+ }, row.ctx)) })
2826
+ })] });
2827
+ }
2828
+ const [rowAxis, colAxis, ...extra] = variance.varyingAxes.map((name) => axes.find((a) => a.name === name)).filter((a) => Boolean(a)).toSorted((a, b) => b.contexts.length - a.contexts.length);
2829
+ if (!rowAxis || !colAxis) return /* @__PURE__ */ jsx(Fragment$1, {});
2830
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2831
+ /* @__PURE__ */ jsxs("div", {
2832
+ className: "sb-token-detail__section-header",
2833
+ children: ["Varies with ", variance.varyingAxes.join(" × ")]
2834
+ }),
2835
+ /* @__PURE__ */ jsxs("table", {
2836
+ className: "sb-token-detail__theme-table",
2837
+ "data-testid": "token-detail-values",
2838
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
2839
+ className: "sb-token-detail__theme-row",
2840
+ children: [/* @__PURE__ */ jsxs("th", {
2841
+ className: "sb-token-detail__theme-cell",
2842
+ style: {
2843
+ textAlign: "left",
2844
+ opacity: .7
2845
+ },
2846
+ children: [
2847
+ rowAxis.name,
2848
+ " \\ ",
2849
+ colAxis.name
2850
+ ]
2851
+ }), colAxis.contexts.map((col) => /* @__PURE__ */ jsx("th", {
2852
+ className: "sb-token-detail__theme-cell",
2853
+ style: {
2854
+ textAlign: "left",
2855
+ opacity: .7
2856
+ },
2857
+ children: col
2858
+ }, col))]
2859
+ }) }), /* @__PURE__ */ jsx("tbody", { children: rowAxis.contexts.map((row) => /* @__PURE__ */ jsxs("tr", {
2860
+ className: "sb-token-detail__theme-row",
2861
+ children: [/* @__PURE__ */ jsx("td", {
2862
+ className: "sb-token-detail__theme-cell",
2863
+ children: row
2864
+ }), colAxis.contexts.map((col) => {
2865
+ const name = tupleName(themes, {
2866
+ ...activeAxes,
2867
+ [rowAxis.name]: row,
2868
+ [colAxis.name]: col
2869
+ });
2870
+ const value = name ? formatFn(themesResolved[name]?.[path]) : "—";
2871
+ return /* @__PURE__ */ jsxs("td", {
2872
+ className: "sb-token-detail__theme-cell",
2873
+ "data-row": row,
2874
+ "data-col": col,
2875
+ children: [isColor && name && /* @__PURE__ */ jsx("span", {
2876
+ className: "sb-token-detail__swatch",
2877
+ style: { background: cssVar },
2878
+ [dataAttr(cssVarPrefix, "theme")]: name,
2879
+ "aria-hidden": true
2880
+ }), value]
2881
+ }, col);
2882
+ })]
2883
+ }, row)) })]
2884
+ }),
2885
+ extra.length > 0 && /* @__PURE__ */ jsxs("div", {
2886
+ className: "sb-token-detail__aliased-by-truncated",
2887
+ style: { marginTop: 6 },
2888
+ children: [
2889
+ "Values also vary with ",
2890
+ extra.map((a) => a.name).join(", "),
2891
+ "; matrix shows the slice for the active selection."
2526
2892
  ]
2527
- }, row.path))]
2528
- });
2893
+ })
2894
+ ] });
2529
2895
  }
2530
- //#endregion
2531
- //#region src/FontFamilySample.tsx
2532
- function stackString(raw) {
2533
- if (typeof raw === "string") return raw;
2534
- if (Array.isArray(raw)) return raw.map(String).join(", ");
2535
- return "";
2896
+ function valueFor(token, $type, format) {
2897
+ if (!token) return "—";
2898
+ return formatTokenValue(token.$value, $type, format);
2536
2899
  }
2537
- function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
2538
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2539
- const rows = useMemo(() => {
2540
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2541
- if (token.$type !== "fontFamily") return false;
2542
- return globMatch(path, filter);
2543
- }), {
2544
- by: sortBy,
2545
- dir: sortDir
2546
- }).map(([path, token]) => ({
2547
- path,
2548
- cssVar: makeCssVar(path, cssVarPrefix),
2549
- stack: stackString(token.$value)
2550
- }));
2551
- }, [
2552
- resolved,
2553
- filter,
2554
- cssVarPrefix,
2555
- sortBy,
2556
- sortDir
2557
- ]);
2558
- const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2559
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2560
- ...themeAttrs(cssVarPrefix, activeTheme),
2561
- children: /* @__PURE__ */ jsx("div", {
2562
- className: "sb-block__empty",
2563
- children: "No fontFamily tokens match this filter."
2564
- })
2565
- });
2566
- return /* @__PURE__ */ jsxs("div", {
2567
- ...themeAttrs(cssVarPrefix, activeTheme),
2568
- children: [/* @__PURE__ */ jsx("div", {
2569
- className: "sb-block__caption",
2570
- children: captionText
2571
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2572
- className: "sb-font-family-sample__row",
2573
- children: [
2574
- /* @__PURE__ */ jsxs("div", {
2575
- className: "sb-font-family-sample__meta",
2576
- children: [/* @__PURE__ */ jsx("span", {
2577
- className: "sb-font-family-sample__path",
2578
- children: row.path
2579
- }), /* @__PURE__ */ jsx("span", {
2580
- className: "sb-font-family-sample__stack",
2581
- children: row.stack
2582
- })]
2583
- }),
2584
- /* @__PURE__ */ jsx("div", {
2585
- className: "sb-font-family-sample__sample",
2586
- style: { fontFamily: row.cssVar },
2587
- children: sample
2588
- }),
2589
- /* @__PURE__ */ jsx("span", {
2590
- className: "sb-font-family-sample__css-var",
2591
- children: row.cssVar
2592
- })
2593
- ]
2594
- }, row.path))]
2595
- });
2900
+ function tupleName(themes, tuple) {
2901
+ return themes.find((t) => {
2902
+ const input = t.input;
2903
+ return Object.keys(input).every((k) => input[k] === tuple[k]);
2904
+ })?.name;
2596
2905
  }
2597
2906
  //#endregion
2598
- //#region src/FontWeightScale.tsx
2599
- function toWeight(raw) {
2600
- if (typeof raw === "number") return raw;
2601
- if (typeof raw === "string") {
2602
- const n = Number.parseInt(raw, 10);
2603
- return Number.isFinite(n) ? n : NaN;
2604
- }
2605
- return NaN;
2606
- }
2607
- function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
2608
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2609
- const rows = useMemo(() => {
2610
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2611
- if (token.$type !== "fontWeight") return false;
2612
- return globMatch(path, filter);
2613
- }), {
2614
- by: sortBy,
2615
- dir: sortDir
2616
- }).map(([path, token]) => ({
2617
- path,
2618
- cssVar: makeCssVar(path, cssVarPrefix),
2619
- display: token.$value == null ? "" : String(token.$value),
2620
- weight: toWeight(token.$value)
2621
- }));
2622
- }, [
2907
+ //#region src/token-detail/CompositeBreakdown.tsx
2908
+ function CompositeBreakdown({ path }) {
2909
+ const { token, resolved } = useTokenDetailData(path);
2910
+ const colorFormat = useColorFormat();
2911
+ if (!token) return null;
2912
+ return /* @__PURE__ */ jsx(CompositeBreakdownContent, {
2913
+ type: token.$type,
2914
+ rawValue: token.$value,
2915
+ partialAliasOf: token.partialAliasOf,
2623
2916
  resolved,
2624
- filter,
2625
- cssVarPrefix,
2626
- sortBy,
2627
- sortDir
2628
- ]);
2629
- const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2630
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2631
- ...themeAttrs(cssVarPrefix, activeTheme),
2632
- children: /* @__PURE__ */ jsx("div", {
2633
- className: "sb-block__empty",
2634
- children: "No fontWeight tokens match this filter."
2635
- })
2917
+ colorFormat
2636
2918
  });
2637
- return /* @__PURE__ */ jsxs("div", {
2638
- ...themeAttrs(cssVarPrefix, activeTheme),
2639
- children: [/* @__PURE__ */ jsx("div", {
2640
- className: "sb-block__caption",
2641
- children: captionText
2642
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2643
- className: "sb-font-weight-scale__row",
2644
- children: [
2645
- /* @__PURE__ */ jsxs("div", {
2646
- className: "sb-font-weight-scale__meta",
2647
- children: [/* @__PURE__ */ jsx("span", {
2648
- className: "sb-font-weight-scale__path",
2649
- children: row.path
2650
- }), /* @__PURE__ */ jsx("span", {
2651
- className: "sb-font-weight-scale__value",
2652
- children: row.display
2653
- })]
2654
- }),
2655
- /* @__PURE__ */ jsx("div", {
2656
- className: "sb-font-weight-scale__sample",
2657
- style: { fontWeight: row.cssVar },
2658
- children: sample
2659
- }),
2660
- /* @__PURE__ */ jsx("span", {
2661
- className: "sb-font-weight-scale__css-var",
2662
- children: row.cssVar
2663
- })
2919
+ }
2920
+ function CompositeBreakdownContent({ type, rawValue, partialAliasOf, resolved, colorFormat }) {
2921
+ if (!rawValue || typeof rawValue !== "object") return null;
2922
+ const objectAliases = pickObjectAliases(partialAliasOf);
2923
+ const arrayAliases = pickArrayAliases(partialAliasOf);
2924
+ const aliasFor = (key) => subValueChain(objectAliases?.[key], resolved);
2925
+ if (type === "typography") {
2926
+ const v = rawValue;
2927
+ return renderKeyValueList([
2928
+ [
2929
+ "fontFamily",
2930
+ formatFontFamily(v["fontFamily"]),
2931
+ aliasFor("fontFamily")
2932
+ ],
2933
+ [
2934
+ "fontSize",
2935
+ formatDimensionValue(v["fontSize"]),
2936
+ aliasFor("fontSize")
2937
+ ],
2938
+ [
2939
+ "fontWeight",
2940
+ formatPrimitive(v["fontWeight"]),
2941
+ aliasFor("fontWeight")
2942
+ ],
2943
+ [
2944
+ "lineHeight",
2945
+ formatPrimitive(v["lineHeight"]),
2946
+ aliasFor("lineHeight")
2947
+ ],
2948
+ [
2949
+ "letterSpacing",
2950
+ formatDimensionValue(v["letterSpacing"]),
2951
+ aliasFor("letterSpacing")
2664
2952
  ]
2665
- }, row.path))]
2666
- });
2953
+ ]);
2954
+ }
2955
+ if (type === "border") {
2956
+ const v = rawValue;
2957
+ return renderKeyValueList([
2958
+ [
2959
+ "color",
2960
+ formatColorSubValue(v["color"], colorFormat),
2961
+ aliasFor("color")
2962
+ ],
2963
+ [
2964
+ "width",
2965
+ formatDimensionValue(v["width"]),
2966
+ aliasFor("width")
2967
+ ],
2968
+ [
2969
+ "style",
2970
+ formatPrimitive(v["style"]),
2971
+ aliasFor("style")
2972
+ ]
2973
+ ]);
2974
+ }
2975
+ if (type === "transition") {
2976
+ const v = rawValue;
2977
+ return renderKeyValueList([
2978
+ [
2979
+ "duration",
2980
+ formatDimensionValue(v["duration"]),
2981
+ aliasFor("duration")
2982
+ ],
2983
+ [
2984
+ "timingFunction",
2985
+ formatPrimitive(v["timingFunction"]),
2986
+ aliasFor("timingFunction")
2987
+ ],
2988
+ [
2989
+ "delay",
2990
+ formatDimensionValue(v["delay"]),
2991
+ aliasFor("delay")
2992
+ ]
2993
+ ]);
2994
+ }
2995
+ if (type === "shadow") {
2996
+ const layers = Array.isArray(rawValue) ? rawValue : [rawValue];
2997
+ const multi = layers.length > 1;
2998
+ const layerAliasFor = (i, key) => subValueChain(arrayAliases?.[i]?.[key], resolved);
2999
+ return /* @__PURE__ */ jsx("div", {
3000
+ className: "sb-token-detail__breakdown-section",
3001
+ children: layers.map((layer, i) => {
3002
+ const v = layer;
3003
+ return /* @__PURE__ */ jsxs("div", {
3004
+ style: { display: "contents" },
3005
+ children: [
3006
+ multi && /* @__PURE__ */ jsxs("div", {
3007
+ className: "sb-token-detail__breakdown-layer-header",
3008
+ children: ["Layer ", i + 1]
3009
+ }),
3010
+ /* @__PURE__ */ jsx(KeyValueRow, {
3011
+ label: "color",
3012
+ value: formatColorSubValue(v["color"], colorFormat),
3013
+ alias: layerAliasFor(i, "color")
3014
+ }),
3015
+ /* @__PURE__ */ jsx(KeyValueRow, {
3016
+ label: "offsetX",
3017
+ value: formatDimensionValue(v["offsetX"]),
3018
+ alias: layerAliasFor(i, "offsetX")
3019
+ }),
3020
+ /* @__PURE__ */ jsx(KeyValueRow, {
3021
+ label: "offsetY",
3022
+ value: formatDimensionValue(v["offsetY"]),
3023
+ alias: layerAliasFor(i, "offsetY")
3024
+ }),
3025
+ /* @__PURE__ */ jsx(KeyValueRow, {
3026
+ label: "blur",
3027
+ value: formatDimensionValue(v["blur"]),
3028
+ alias: layerAliasFor(i, "blur")
3029
+ }),
3030
+ /* @__PURE__ */ jsx(KeyValueRow, {
3031
+ label: "spread",
3032
+ value: formatDimensionValue(v["spread"]),
3033
+ alias: layerAliasFor(i, "spread")
3034
+ }),
3035
+ "inset" in v && /* @__PURE__ */ jsx(KeyValueRow, {
3036
+ label: "inset",
3037
+ value: formatPrimitive(v["inset"]),
3038
+ alias: void 0
3039
+ })
3040
+ ]
3041
+ }, shadowLayerKey(v, i));
3042
+ })
3043
+ });
3044
+ }
3045
+ if (type === "gradient") {
3046
+ const stops = Array.isArray(rawValue) ? rawValue : [];
3047
+ if (stops.length === 0) return null;
3048
+ const stopAliasFor = (i) => subValueChain(arrayAliases?.[i]?.["color"], resolved);
3049
+ return /* @__PURE__ */ jsx("div", {
3050
+ className: "sb-token-detail__breakdown-section",
3051
+ children: stops.map((stop, i) => {
3052
+ const v = stop;
3053
+ return /* @__PURE__ */ jsx(KeyValueRow, {
3054
+ label: `${((typeof v["position"] === "number" ? v["position"] : 0) * 100).toFixed(0)}%`,
3055
+ value: formatColorSubValue(v["color"], colorFormat),
3056
+ alias: stopAliasFor(i)
3057
+ }, gradientStopKey(v, i));
3058
+ })
3059
+ });
3060
+ }
3061
+ return null;
2667
3062
  }
2668
- //#endregion
2669
- //#region src/GradientPalette.tsx
2670
- function asStops(raw) {
2671
- if (!Array.isArray(raw)) return [];
2672
- return raw;
3063
+ function renderKeyValueList(rows) {
3064
+ return /* @__PURE__ */ jsx("div", {
3065
+ className: "sb-token-detail__breakdown-section",
3066
+ children: rows.filter(([, v, alias]) => v !== null || alias && alias.length > 0).map(([k, v, alias]) => /* @__PURE__ */ jsx(KeyValueRow, {
3067
+ label: k,
3068
+ value: v ?? "",
3069
+ alias
3070
+ }, k))
3071
+ });
2673
3072
  }
2674
- const pct = (n) => `${(n * 100).toFixed(3)}%`;
2675
- function stopCssColor(stop) {
2676
- const color = stop.color;
2677
- if (!color || !Array.isArray(color.components) || color.components.length < 3) return "transparent";
2678
- const [r, g, b] = color.components;
2679
- if (r === void 0 || g === void 0 || b === void 0) return "transparent";
2680
- const alpha = color.alpha ?? 1;
2681
- return alpha === 1 ? `rgb(${pct(r)} ${pct(g)} ${pct(b)})` : `rgb(${pct(r)} ${pct(g)} ${pct(b)} / ${alpha})`;
3073
+ function KeyValueRow({ label, value, alias }) {
3074
+ const hasAlias = alias && alias.length > 0;
3075
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
3076
+ className: "sb-token-detail__breakdown-key",
3077
+ children: label
3078
+ }), /* @__PURE__ */ jsxs("span", {
3079
+ className: "sb-token-detail__breakdown-value",
3080
+ children: [/* @__PURE__ */ jsx("span", { children: value ?? "—" }), hasAlias && /* @__PURE__ */ jsx("span", {
3081
+ className: "sb-token-detail__breakdown-alias",
3082
+ "data-testid": "breakdown-alias",
3083
+ children: alias.map((p, i) => /* @__PURE__ */ jsxs("span", {
3084
+ className: "sb-token-detail__breakdown-alias-step",
3085
+ children: [i > 0 && /* @__PURE__ */ jsx("span", {
3086
+ className: "sb-token-detail__arrow",
3087
+ children: "→"
3088
+ }), /* @__PURE__ */ jsx("span", {
3089
+ className: "sb-token-detail__chain-node",
3090
+ children: p
3091
+ })]
3092
+ }, p))
3093
+ })]
3094
+ })] });
2682
3095
  }
2683
- function stopKey(path, stop, fallback) {
2684
- return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
3096
+ function formatPrimitive(v) {
3097
+ if (v == null) return null;
3098
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
3099
+ return JSON.stringify(v);
2685
3100
  }
2686
- function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
2687
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2688
- const colorFormat = useColorFormat();
2689
- const rows = useMemo(() => {
2690
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2691
- if (token.$type !== "gradient") return false;
2692
- return globMatch(path, filter);
2693
- }), {
2694
- by: sortBy,
2695
- dir: sortDir
2696
- }).map(([path, token]) => ({
2697
- path,
2698
- cssVar: makeCssVar(path, cssVarPrefix),
2699
- stops: asStops(token.$value)
2700
- }));
2701
- }, [
2702
- resolved,
2703
- filter,
2704
- cssVarPrefix,
2705
- sortBy,
2706
- sortDir
2707
- ]);
2708
- const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2709
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2710
- ...themeAttrs(cssVarPrefix, activeTheme),
2711
- children: /* @__PURE__ */ jsx("div", {
2712
- className: "sb-block__empty",
2713
- children: "No gradient tokens match this filter."
2714
- })
2715
- });
2716
- return /* @__PURE__ */ jsxs("div", {
2717
- ...themeAttrs(cssVarPrefix, activeTheme),
2718
- children: [/* @__PURE__ */ jsx("div", {
2719
- className: "sb-block__caption",
2720
- children: captionText
2721
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
2722
- className: "sb-gradient-palette__row",
2723
- children: [
2724
- /* @__PURE__ */ jsxs("div", {
2725
- className: "sb-gradient-palette__meta",
2726
- children: [/* @__PURE__ */ jsx("span", {
2727
- className: "sb-gradient-palette__path",
2728
- children: row.path
2729
- }), /* @__PURE__ */ jsx("span", {
2730
- className: "sb-gradient-palette__css-var",
2731
- children: row.cssVar
2732
- })]
2733
- }),
2734
- /* @__PURE__ */ jsx("div", {
2735
- className: "sb-gradient-palette__sample",
2736
- style: { background: `linear-gradient(to right, ${row.cssVar})` },
2737
- "aria-hidden": true
2738
- }),
2739
- /* @__PURE__ */ jsx("div", {
2740
- className: "sb-gradient-palette__stops",
2741
- children: row.stops.map((stop, i) => /* @__PURE__ */ jsxs("div", {
2742
- className: "sb-gradient-palette__stop-row",
2743
- children: [
2744
- /* @__PURE__ */ jsx("span", {
2745
- className: "sb-gradient-palette__stop-swatch",
2746
- style: { background: stopCssColor(stop) },
2747
- "aria-hidden": true
2748
- }),
2749
- /* @__PURE__ */ jsx("span", { children: formatColor(stop.color, colorFormat).value }),
2750
- /* @__PURE__ */ jsxs("span", {
2751
- className: "sb-gradient-palette__stop-position",
2752
- children: [
2753
- "@ ",
2754
- ((stop.position ?? 0) * 100).toFixed(0),
2755
- "%"
2756
- ]
2757
- })
2758
- ]
2759
- }, stopKey(row.path, stop, i)))
2760
- })
2761
- ]
2762
- }, row.path))]
2763
- });
3101
+ function formatFontFamily(v) {
3102
+ if (v == null) return null;
3103
+ if (typeof v === "string") return v;
3104
+ if (Array.isArray(v)) return v.map(String).join(", ");
3105
+ return JSON.stringify(v);
2764
3106
  }
2765
- //#endregion
2766
- //#region src/motion-preview/MotionSample.tsx
2767
- const DEFAULT_DURATION_MS = 300;
2768
- const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
2769
- const styles = {
2770
- track: {
2771
- position: "relative",
2772
- height: 36,
2773
- background: SURFACE_MUTED,
2774
- borderRadius: 18,
2775
- overflow: "hidden"
2776
- },
2777
- ball: {
2778
- position: "absolute",
2779
- top: "50%",
2780
- width: 28,
2781
- height: 28,
2782
- marginTop: -14,
2783
- borderRadius: "50%",
2784
- background: "var(--swatchbook-accent-bg, #3b82f6)"
2785
- },
2786
- reducedMotion: {
2787
- fontSize: 11,
2788
- color: TEXT_MUTED,
2789
- fontStyle: "italic"
2790
- }
2791
- };
2792
- function extractDurationMs(raw) {
2793
- if (raw == null) return NaN;
2794
- if (typeof raw === "object") {
2795
- const v = raw;
2796
- if (typeof v.value === "number" && typeof v.unit === "string") {
2797
- if (v.unit === "ms") return v.value;
2798
- if (v.unit === "s") return v.value * 1e3;
2799
- }
3107
+ function formatDimensionValue(v) {
3108
+ if (v == null) return null;
3109
+ if (typeof v === "string" || typeof v === "number") return String(v);
3110
+ if (typeof v === "object") {
3111
+ const d = v;
3112
+ if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
2800
3113
  }
2801
- return NaN;
3114
+ return JSON.stringify(v);
2802
3115
  }
2803
- function extractCubicBezier(raw) {
2804
- if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
2805
- return null;
3116
+ /**
3117
+ * Route sub-value colors through `formatColor` so they honor the active
3118
+ * color-format dropdown, just like the standalone `<ColorPalette />` and
3119
+ * `<TokenDetail />` top-line do. Returns `null` for a missing field so
3120
+ * the key/value row drops out entirely.
3121
+ */
3122
+ function formatColorSubValue(v, format) {
3123
+ if (v == null) return null;
3124
+ return formatColor(v, format).value;
2806
3125
  }
2807
- function asDuration(raw, themeTokens, fallback) {
2808
- const direct = extractDurationMs(raw);
2809
- if (Number.isFinite(direct)) return direct;
2810
- if (typeof raw === "string") {
2811
- const match = raw.match(/^\{([^}]+)\}$/);
2812
- if (match && match[1]) {
2813
- const referenced = themeTokens[match[1]];
2814
- const resolved = extractDurationMs(referenced?.$value);
2815
- if (Number.isFinite(resolved)) return resolved;
2816
- }
2817
- }
2818
- return fallback;
3126
+ function pickObjectAliases(v) {
3127
+ if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
3128
+ return v;
3129
+ }
3130
+ function pickArrayAliases(v) {
3131
+ if (!Array.isArray(v)) return void 0;
3132
+ return v;
3133
+ }
3134
+ /**
3135
+ * Walk the alias chain starting from an immediate sub-value alias target.
3136
+ * `aliasTarget` is the path the sub-value directly references; the target
3137
+ * token's own `aliasChain` continues the walk to the primitive.
3138
+ */
3139
+ function subValueChain(aliasTarget, resolved) {
3140
+ if (!aliasTarget) return void 0;
3141
+ const tail = (resolved?.[aliasTarget])?.aliasChain;
3142
+ return tail && tail.length > 0 ? [aliasTarget, ...tail] : [aliasTarget];
3143
+ }
3144
+ function shadowLayerKey(layer, fallback) {
3145
+ return `shadow|${[
3146
+ layer["color"],
3147
+ layer["offsetX"],
3148
+ layer["offsetY"],
3149
+ layer["blur"],
3150
+ layer["spread"],
3151
+ layer["inset"]
3152
+ ].map((p) => p === void 0 ? "" : JSON.stringify(p)).join("|")}|${fallback}`;
2819
3153
  }
2820
- function asEasing(raw, themeTokens, fallback) {
2821
- const direct = extractCubicBezier(raw);
2822
- if (direct) return direct;
2823
- if (typeof raw === "string") {
2824
- const match = raw.match(/^\{([^}]+)\}$/);
2825
- if (match && match[1]) {
2826
- const referenced = themeTokens[match[1]];
2827
- const resolved = extractCubicBezier(referenced?.$value);
2828
- if (resolved) return resolved;
2829
- }
2830
- }
2831
- return fallback;
3154
+ function gradientStopKey(stop, fallback) {
3155
+ return `stop|${stop["position"] ?? fallback}|${JSON.stringify(stop["color"])}`;
2832
3156
  }
2833
- function resolveMotionSpec(token, themeTokens) {
3157
+ //#endregion
3158
+ //#region src/token-detail/CompositePreview.tsx
3159
+ const PANGRAM = "Sphinx of black quartz, judge my vow.";
3160
+ const STROKE_STYLE_STRINGS = new Set([
3161
+ "solid",
3162
+ "dashed",
3163
+ "dotted",
3164
+ "double",
3165
+ "groove",
3166
+ "ridge",
3167
+ "outset",
3168
+ "inset"
3169
+ ]);
3170
+ function CompositePreview({ path }) {
3171
+ const { token, cssVar } = useTokenDetailData(path);
2834
3172
  if (!token) return null;
2835
- const type = token.$type;
2836
- if (type === "transition") {
2837
- const v = token.$value ?? {};
2838
- return {
2839
- durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
2840
- easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
2841
- };
2842
- }
2843
- if (type === "duration") {
2844
- const durationMs = extractDurationMs(token.$value);
2845
- if (!Number.isFinite(durationMs)) return null;
2846
- return {
2847
- durationMs,
2848
- easing: DEFAULT_EASING
2849
- };
2850
- }
2851
- if (type === "cubicBezier") {
2852
- const easing = extractCubicBezier(token.$value);
2853
- if (!easing) return null;
2854
- return {
2855
- durationMs: DEFAULT_DURATION_MS,
2856
- easing
2857
- };
2858
- }
2859
- return null;
2860
- }
2861
- function MotionSample({ path, speed = 1, runKey = 0 }) {
2862
- const { resolved } = useProject();
2863
- const reducedMotion = usePrefersReducedMotion();
2864
- const spec = useMemo(() => resolveMotionSpec(resolved[path], resolved), [resolved, path]);
2865
- const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
2866
- const easing = spec?.easing ?? DEFAULT_EASING;
2867
- const scaledDuration = Math.max(1, durationMs / speed);
2868
- const [phase, setPhase] = useState(0);
2869
- useEffect(() => {
2870
- if (reducedMotion) return;
2871
- setPhase(0);
2872
- const id = requestAnimationFrame(() => setPhase(1));
2873
- const loop = window.setInterval(() => {
2874
- setPhase((p) => p === 0 ? 1 : 0);
2875
- }, scaledDuration * 2);
2876
- return () => {
2877
- cancelAnimationFrame(id);
2878
- window.clearInterval(loop);
2879
- };
2880
- }, [
2881
- scaledDuration,
2882
- runKey,
2883
- reducedMotion
2884
- ]);
2885
- if (reducedMotion) return /* @__PURE__ */ jsx("div", {
2886
- style: styles.reducedMotion,
2887
- children: "Animation suppressed by `prefers-reduced-motion: reduce`."
3173
+ return /* @__PURE__ */ jsx(CompositePreviewContent, {
3174
+ type: token.$type,
3175
+ cssVar,
3176
+ rawValue: token.$value
2888
3177
  });
2889
- return /* @__PURE__ */ jsx("div", {
2890
- style: styles.track,
2891
- children: /* @__PURE__ */ jsx("div", {
3178
+ }
3179
+ function CompositePreviewContent({ type, cssVar, rawValue }) {
3180
+ if (type === "typography") {
3181
+ const base = cssVar.replace(/^var\(/, "").replace(/\)$/, "");
3182
+ return /* @__PURE__ */ jsx("div", {
3183
+ className: "sb-token-detail__typography-sample",
2892
3184
  style: {
2893
- ...styles.ball,
2894
- left: phase === 1 ? "calc(100% - 32px)" : "4px",
2895
- transition: `left ${scaledDuration}ms ${easing}`
3185
+ fontFamily: `var(${base}-font-family)`,
3186
+ fontSize: `var(${base}-font-size)`,
3187
+ fontWeight: `var(${base}-font-weight)`,
3188
+ lineHeight: `var(${base}-line-height)`,
3189
+ letterSpacing: `var(${base}-letter-spacing)`
2896
3190
  },
2897
- "aria-hidden": true
2898
- })
2899
- });
2900
- }
2901
- //#endregion
2902
- //#region src/MotionPreview.tsx
2903
- const SPEEDS = [
2904
- .25,
2905
- .5,
2906
- 1,
2907
- 2
2908
- ];
2909
- function formatSpec(row) {
2910
- switch (row.kind) {
2911
- case "transition": return `transition · ${Math.round(row.durationMs)}ms · ${row.easing}`;
2912
- case "duration": return `duration · ${Math.round(row.durationMs)}ms`;
2913
- case "cubicBezier": return `cubicBezier · ${row.easing}`;
2914
- }
2915
- }
2916
- function MotionPreview({ filter, caption }) {
2917
- const { resolved, activeTheme, cssVarPrefix } = useProject();
2918
- const [speed, setSpeed] = useState(1);
2919
- const [run, setRun] = useState(0);
2920
- const reducedMotion = usePrefersReducedMotion();
2921
- const rows = useMemo(() => {
2922
- const collected = [];
2923
- for (const [path, token] of Object.entries(resolved)) {
2924
- if (filter && !globMatch(path, filter)) continue;
2925
- if (!filter && ![
2926
- "transition",
2927
- "duration",
2928
- "cubicBezier"
2929
- ].includes(token.$type ?? "")) continue;
2930
- const kind = token.$type;
2931
- if (!kind) continue;
2932
- const spec = resolveMotionSpec(token, resolved);
2933
- if (!spec) continue;
2934
- collected.push({
2935
- path,
2936
- cssVar: makeCssVar(path, cssVarPrefix),
2937
- durationMs: spec.durationMs,
2938
- easing: spec.easing,
2939
- kind
2940
- });
2941
- }
2942
- collected.sort((a, b) => {
2943
- if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
2944
- return a.path.localeCompare(b.path, void 0, { numeric: true });
3191
+ children: PANGRAM
2945
3192
  });
2946
- return collected;
2947
- }, [
2948
- resolved,
2949
- filter,
2950
- cssVarPrefix
2951
- ]);
2952
- const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
2953
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2954
- ...themeAttrs(cssVarPrefix, activeTheme),
3193
+ }
3194
+ if (type === "shadow") return /* @__PURE__ */ jsx("div", {
3195
+ className: "sb-token-detail__shadow-sample",
3196
+ style: { boxShadow: cssVar },
3197
+ "aria-hidden": true
3198
+ });
3199
+ if (type === "border") return /* @__PURE__ */ jsx("div", {
3200
+ className: "sb-token-detail__border-sample",
3201
+ style: { border: cssVar },
3202
+ "aria-hidden": true
3203
+ });
3204
+ if (type === "transition") return /* @__PURE__ */ jsx(TransitionSample, { transition: cssVar });
3205
+ if (type === "dimension") return /* @__PURE__ */ jsx("div", {
3206
+ className: "sb-token-detail__dimension-track",
2955
3207
  children: /* @__PURE__ */ jsx("div", {
2956
- className: "sb-block__empty",
2957
- children: "No motion tokens match this filter."
3208
+ className: "sb-token-detail__dimension-bar",
3209
+ style: { width: cssVar },
3210
+ "aria-hidden": true
2958
3211
  })
2959
3212
  });
2960
- return /* @__PURE__ */ jsxs("div", {
2961
- ...themeAttrs(cssVarPrefix, activeTheme),
2962
- children: [
2963
- /* @__PURE__ */ jsx("div", {
2964
- className: "sb-block__caption",
2965
- children: captionText
2966
- }),
2967
- /* @__PURE__ */ jsxs("div", {
2968
- className: "sb-motion-preview__controls",
2969
- children: [
2970
- /* @__PURE__ */ jsx("span", {
2971
- className: "sb-motion-preview__control-label",
2972
- children: "Speed"
2973
- }),
2974
- SPEEDS.map((s) => /* @__PURE__ */ jsxs("button", {
2975
- type: "button",
2976
- className: cx("sb-motion-preview__speed-btn", { "sb-motion-preview__speed-btn--active": s === speed }),
2977
- onClick: () => setSpeed(s),
2978
- children: [s, "×"]
2979
- }, s)),
2980
- /* @__PURE__ */ jsx("button", {
2981
- type: "button",
2982
- className: "sb-motion-preview__replay-btn",
2983
- onClick: () => setRun((n) => n + 1),
2984
- disabled: reducedMotion,
2985
- title: reducedMotion ? "Disabled by prefers-reduced-motion" : "Replay all",
2986
- children: "↻ Replay"
2987
- })
2988
- ]
2989
- }),
2990
- rows.map((row) => /* @__PURE__ */ jsxs("div", {
2991
- className: "sb-motion-preview__row",
2992
- children: [
2993
- /* @__PURE__ */ jsxs("div", {
2994
- className: "sb-motion-preview__meta",
2995
- children: [/* @__PURE__ */ jsx("span", {
2996
- className: "sb-motion-preview__path",
2997
- children: row.path
2998
- }), /* @__PURE__ */ jsx("span", {
2999
- className: "sb-motion-preview__specs",
3000
- children: formatSpec(row)
3001
- })]
3002
- }),
3003
- /* @__PURE__ */ jsx(MotionSample, {
3004
- path: row.path,
3005
- speed,
3006
- runKey: run
3007
- }),
3008
- /* @__PURE__ */ jsx("span", {
3009
- className: "sb-motion-preview__css-var",
3010
- children: row.cssVar
3011
- })
3012
- ]
3013
- }, row.path))
3014
- ]
3213
+ if (type === "duration") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left ${cssVar} ease` });
3214
+ if (type === "fontFamily") return /* @__PURE__ */ jsx("div", {
3215
+ className: "sb-token-detail__font-family-sample",
3216
+ style: { fontFamily: cssVar },
3217
+ children: PANGRAM
3218
+ });
3219
+ if (type === "fontWeight") return /* @__PURE__ */ jsx("div", {
3220
+ className: "sb-token-detail__font-weight-sample",
3221
+ style: { fontWeight: cssVar },
3222
+ children: "Aa"
3223
+ });
3224
+ if (type === "cubicBezier") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left 800ms ${cssVar}` });
3225
+ if (type === "gradient") return /* @__PURE__ */ jsx("div", {
3226
+ className: "sb-token-detail__gradient-sample",
3227
+ style: { background: `linear-gradient(to right, ${cssVar})` },
3228
+ "aria-hidden": true
3015
3229
  });
3016
- }
3017
- //#endregion
3018
- //#region src/provider.tsx
3019
- /**
3020
- * Wraps a tree of blocks with the token data they need to render.
3021
- *
3022
- * The Storybook addon's preview decorator mounts this automatically, so
3023
- * story/MDX authors typically never see it. Outside Storybook — unit
3024
- * tests, custom React apps, non-Storybook doc sites — consumers construct
3025
- * a {@link ProjectSnapshot} (often imported from a JSON file) and wrap
3026
- * their blocks in this provider.
3027
- */
3028
- function SwatchbookProvider({ value, children }) {
3029
- return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
3030
- value,
3031
- children
3230
+ if (type === "strokeStyle") return /* @__PURE__ */ jsx(StrokeStylePreview, { value: rawValue });
3231
+ if (type === "color") return /* @__PURE__ */ jsxs("div", {
3232
+ className: "sb-token-detail__color-swatch-row",
3233
+ "aria-hidden": true,
3234
+ children: [/* @__PURE__ */ jsx("div", {
3235
+ className: "sb-token-detail__color-swatch-light",
3236
+ style: { background: cssVar }
3237
+ }), /* @__PURE__ */ jsx("div", {
3238
+ className: "sb-token-detail__color-swatch-dark",
3239
+ style: { background: cssVar }
3240
+ })]
3032
3241
  });
3242
+ return null;
3033
3243
  }
3034
- /**
3035
- * Read the current {@link ProjectSnapshot}. Throws if called outside a
3036
- * {@link SwatchbookProvider}; blocks that need to fall back to the
3037
- * virtual module go through the internal `useProject()` hook instead.
3038
- */
3039
- function useSwatchbookData() {
3040
- const value = useOptionalSwatchbookData();
3041
- if (!value) throw new Error("[swatchbook-blocks] useSwatchbookData() called outside <SwatchbookProvider>. Wrap your tree in <SwatchbookProvider value={snapshot}> or render inside a Storybook story.");
3042
- return value;
3043
- }
3044
- //#endregion
3045
- //#region src/shadow-preview/ShadowSample.tsx
3046
- const sampleStyle = {
3047
- width: 120,
3048
- height: 56,
3049
- background: SURFACE_RAISED,
3050
- border: BORDER_FAINT,
3051
- borderRadius: 6
3052
- };
3053
- function ShadowSample({ path }) {
3054
- const { cssVarPrefix } = useProject();
3055
- const cssVar = makeCssVar(path, cssVarPrefix);
3056
- return /* @__PURE__ */ jsx("div", {
3057
- style: {
3058
- ...sampleStyle,
3059
- boxShadow: cssVar
3060
- },
3244
+ function StrokeStylePreview({ value }) {
3245
+ if (typeof value === "string" && STROKE_STYLE_STRINGS.has(value)) return /* @__PURE__ */ jsx("div", {
3246
+ className: "sb-token-detail__stroke-style-line",
3247
+ style: { borderTopStyle: value },
3061
3248
  "aria-hidden": true
3062
3249
  });
3063
- }
3064
- //#endregion
3065
- //#region src/ShadowPreview.tsx
3066
- function formatDimension(raw) {
3067
- if (raw == null) return "";
3068
- if (typeof raw === "number") return String(raw);
3069
- if (typeof raw === "string") return raw;
3070
- if (typeof raw === "object") {
3071
- const v = raw;
3072
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
3250
+ if (value && typeof value === "object" && "dashArray" in value) {
3251
+ const v = value;
3252
+ const lengths = asDashLengths(v.dashArray);
3253
+ if (lengths.length === 0) return /* @__PURE__ */ jsx("div", {
3254
+ className: "sb-token-detail__stroke-style-fallback",
3255
+ children: "Object-form strokeStyle with no resolvable dashArray."
3256
+ });
3257
+ const cap = typeof v.lineCap === "string" ? v.lineCap : "butt";
3258
+ return /* @__PURE__ */ jsx("svg", {
3259
+ className: "sb-token-detail__stroke-style-svg",
3260
+ viewBox: "0 0 220 24",
3261
+ preserveAspectRatio: "none",
3262
+ "aria-hidden": true,
3263
+ children: /* @__PURE__ */ jsx("line", {
3264
+ x1: "4",
3265
+ y1: "12",
3266
+ x2: "216",
3267
+ y2: "12",
3268
+ stroke: "currentColor",
3269
+ strokeWidth: "4",
3270
+ strokeDasharray: lengths.join(" "),
3271
+ strokeLinecap: cap
3272
+ })
3273
+ });
3073
3274
  }
3074
- return JSON.stringify(raw);
3075
- }
3076
- function formatSubColor(raw, format) {
3077
- if (raw == null) return "—";
3078
- return formatColor(raw, format).value;
3079
- }
3080
- function asLayers(raw) {
3081
- if (Array.isArray(raw)) return raw;
3082
- if (raw && typeof raw === "object") return [raw];
3083
- return [];
3275
+ return /* @__PURE__ */ jsx("div", {
3276
+ className: "sb-token-detail__stroke-style-fallback",
3277
+ children: "strokeStyle value could not be previewed."
3278
+ });
3084
3279
  }
3085
- function layerKey(path, layer, fallback) {
3086
- return `${path}|${`${formatDimension(layer.offsetX)},${formatDimension(layer.offsetY)}`}|${formatDimension(layer.blur)}|${formatDimension(layer.spread)}|${fallback}`;
3280
+ function asDashLengths(raw) {
3281
+ if (!Array.isArray(raw)) return [];
3282
+ const out = [];
3283
+ for (const entry of raw) {
3284
+ if (typeof entry === "number") {
3285
+ out.push(entry);
3286
+ continue;
3287
+ }
3288
+ if (entry && typeof entry === "object") {
3289
+ const e = entry;
3290
+ if (typeof e.value === "number") out.push(e.value);
3291
+ }
3292
+ }
3293
+ return out;
3087
3294
  }
3088
- function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3089
- const { resolved, activeTheme, cssVarPrefix } = useProject();
3090
- const colorFormat = useColorFormat();
3091
- const rows = useMemo(() => {
3092
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
3093
- if (token.$type !== "shadow") return false;
3094
- return globMatch(path, filter);
3095
- }), {
3096
- by: sortBy,
3097
- dir: sortDir
3098
- }).map(([path, token]) => ({
3099
- path,
3100
- cssVar: makeCssVar(path, cssVarPrefix),
3101
- layers: asLayers(token.$value)
3102
- }));
3103
- }, [
3104
- resolved,
3105
- filter,
3106
- cssVarPrefix,
3107
- sortBy,
3108
- sortDir
3109
- ]);
3110
- const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
3111
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
3112
- ...themeAttrs(cssVarPrefix, activeTheme),
3295
+ function TransitionSample({ transition }) {
3296
+ const reduced = usePrefersReducedMotion();
3297
+ const [phase, setPhase] = useState(0);
3298
+ useEffect(() => {
3299
+ if (reduced) return;
3300
+ const id = requestAnimationFrame(() => setPhase(1));
3301
+ const loop = window.setInterval(() => {
3302
+ setPhase((p) => p === 0 ? 1 : 0);
3303
+ }, 1200);
3304
+ return () => {
3305
+ cancelAnimationFrame(id);
3306
+ window.clearInterval(loop);
3307
+ };
3308
+ }, [reduced]);
3309
+ if (reduced) return /* @__PURE__ */ jsx("div", {
3310
+ className: "sb-token-detail__reduced-motion",
3311
+ children: "Animation suppressed by `prefers-reduced-motion: reduce`."
3312
+ });
3313
+ return /* @__PURE__ */ jsx("div", {
3314
+ className: "sb-token-detail__motion-track",
3113
3315
  children: /* @__PURE__ */ jsx("div", {
3114
- className: "sb-block__empty",
3115
- children: "No shadow tokens match this filter."
3316
+ className: "sb-token-detail__motion-ball",
3317
+ style: {
3318
+ left: phase === 1 ? "calc(100% - 28px)" : "4px",
3319
+ transition
3320
+ },
3321
+ "aria-hidden": true
3116
3322
  })
3117
3323
  });
3324
+ }
3325
+ //#endregion
3326
+ //#region src/token-detail/ConsumerOutput.tsx
3327
+ function ConsumerOutput({ path }) {
3328
+ const { token, cssVar, activeAxes } = useTokenDetailData(path);
3329
+ if (!token) return null;
3330
+ const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
3331
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3332
+ /* @__PURE__ */ jsx("div", {
3333
+ className: "sb-token-detail__section-header",
3334
+ children: "Consumer output"
3335
+ }),
3336
+ tupleLabel && /* @__PURE__ */ jsxs("div", {
3337
+ className: "sb-token-detail__tuple-indicator",
3338
+ children: ["Active tuple: ", /* @__PURE__ */ jsx("strong", { children: tupleLabel })]
3339
+ }),
3340
+ /* @__PURE__ */ jsx(OutputRow, {
3341
+ label: "Path",
3342
+ value: path,
3343
+ testId: "consumer-output-path"
3344
+ }),
3345
+ /* @__PURE__ */ jsx(OutputRow, {
3346
+ label: "CSS",
3347
+ value: cssVar,
3348
+ testId: "consumer-output-css"
3349
+ })
3350
+ ] });
3351
+ }
3352
+ function OutputRow({ label, value, testId }) {
3118
3353
  return /* @__PURE__ */ jsxs("div", {
3119
- ...themeAttrs(cssVarPrefix, activeTheme),
3120
- children: [/* @__PURE__ */ jsx("div", {
3121
- className: "sb-block__caption",
3122
- children: captionText
3123
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
3124
- className: "sb-shadow-preview__row",
3125
- children: [
3126
- /* @__PURE__ */ jsxs("div", {
3127
- className: "sb-shadow-preview__meta",
3128
- children: [/* @__PURE__ */ jsx("span", {
3129
- className: "sb-shadow-preview__path",
3130
- children: row.path
3131
- }), /* @__PURE__ */ jsx("span", {
3132
- className: "sb-shadow-preview__css-var",
3133
- children: row.cssVar
3134
- })]
3135
- }),
3136
- /* @__PURE__ */ jsx("div", {
3137
- className: "sb-shadow-preview__sample-cell",
3138
- children: /* @__PURE__ */ jsx(ShadowSample, { path: row.path })
3139
- }),
3140
- /* @__PURE__ */ jsx("div", {
3141
- className: "sb-shadow-preview__breakdown",
3142
- children: row.layers.length === 1 ? renderLayer(row.layers[0], colorFormat) : row.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, {
3143
- layer,
3144
- index: i,
3145
- total: row.layers.length,
3146
- colorFormat
3147
- }, layerKey(row.path, layer, i)))
3148
- })
3149
- ]
3150
- }, row.path))]
3354
+ className: "sb-token-detail__consumer-row",
3355
+ children: [
3356
+ /* @__PURE__ */ jsx("span", {
3357
+ className: "sb-token-detail__consumer-row-label",
3358
+ children: label
3359
+ }),
3360
+ /* @__PURE__ */ jsx("code", {
3361
+ className: "sb-token-detail__consumer-row-value",
3362
+ "data-testid": testId,
3363
+ children: value
3364
+ }),
3365
+ /* @__PURE__ */ jsx(CopyButton, {
3366
+ text: value,
3367
+ testId: `${testId}-copy`
3368
+ })
3369
+ ]
3370
+ });
3371
+ }
3372
+ function CopyButton({ text, testId }) {
3373
+ const [copied, setCopied] = useState(false);
3374
+ return /* @__PURE__ */ jsx("button", {
3375
+ type: "button",
3376
+ className: "sb-token-detail__consumer-row-copy",
3377
+ "data-testid": testId,
3378
+ onClick: () => {
3379
+ copyToClipboard(text).then((ok) => {
3380
+ if (!ok) return;
3381
+ setCopied(true);
3382
+ window.setTimeout(() => setCopied(false), 1200);
3383
+ });
3384
+ },
3385
+ children: copied ? "Copied" : "Copy"
3151
3386
  });
3152
3387
  }
3153
- function renderLayer(layer, format) {
3154
- if (!layer) return [];
3155
- const entries = [
3156
- ["offset", `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`],
3157
- ["blur", formatDimension(layer.blur)],
3158
- ["spread", formatDimension(layer.spread)],
3159
- ["color", formatSubColor(layer.color, format)]
3160
- ];
3161
- if (layer.inset) entries.push(["inset", String(layer.inset)]);
3162
- return entries.flatMap(([k, v]) => [/* @__PURE__ */ jsx("span", {
3163
- className: "sb-shadow-preview__breakdown-key",
3164
- children: k
3165
- }, `k-${k}`), /* @__PURE__ */ jsx("span", { children: v }, `v-${k}`)]);
3388
+ async function copyToClipboard(text) {
3389
+ if (typeof navigator === "undefined" || !navigator.clipboard) return false;
3390
+ try {
3391
+ await navigator.clipboard.writeText(text);
3392
+ return true;
3393
+ } catch {
3394
+ return false;
3395
+ }
3166
3396
  }
3167
- function Layer({ layer, index, total, colorFormat }) {
3168
- return /* @__PURE__ */ jsxs("div", {
3169
- className: "sb-shadow-preview__layer",
3170
- children: [/* @__PURE__ */ jsxs("div", {
3171
- className: "sb-shadow-preview__layer-header",
3172
- children: [
3173
- "layer ",
3174
- index + 1,
3175
- " of ",
3176
- total
3177
- ]
3178
- }), /* @__PURE__ */ jsx("div", {
3179
- className: cx("sb-shadow-preview__breakdown", "sb-shadow-preview__layer-breakdown"),
3180
- children: renderLayer(layer, colorFormat)
3181
- })]
3397
+ //#endregion
3398
+ //#region src/token-detail/TokenHeader.tsx
3399
+ function TokenHeader({ path, heading }) {
3400
+ const { token, cssVar, activeTheme } = useTokenDetailData(path);
3401
+ if (!token) return /* @__PURE__ */ jsxs("div", {
3402
+ className: "sb-token-detail__missing",
3403
+ children: [
3404
+ "Token ",
3405
+ /* @__PURE__ */ jsx("code", { children: path }),
3406
+ " not found in theme ",
3407
+ /* @__PURE__ */ jsx("strong", { children: activeTheme }),
3408
+ "."
3409
+ ]
3182
3410
  });
3411
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
3412
+ /* @__PURE__ */ jsx("h3", {
3413
+ className: "sb-token-detail__heading",
3414
+ children: heading ?? path
3415
+ }),
3416
+ /* @__PURE__ */ jsxs("div", {
3417
+ className: "sb-token-detail__subline",
3418
+ children: [token.$type && /* @__PURE__ */ jsx("span", {
3419
+ className: "sb-token-detail__type-pill",
3420
+ children: token.$type
3421
+ }), /* @__PURE__ */ jsx("span", { children: cssVar })]
3422
+ }),
3423
+ token.$description && /* @__PURE__ */ jsx("p", {
3424
+ className: "sb-token-detail__description",
3425
+ children: token.$description
3426
+ })
3427
+ ] });
3183
3428
  }
3184
3429
  //#endregion
3185
- //#region src/StrokeStyleSample.tsx
3186
- const STRING_STYLES = new Set([
3187
- "solid",
3188
- "dashed",
3189
- "dotted",
3190
- "double",
3191
- "groove",
3192
- "ridge",
3193
- "outset",
3194
- "inset"
3195
- ]);
3196
- function extractCssStyle(value) {
3197
- if (typeof value === "string" && STRING_STYLES.has(value)) return value;
3198
- return null;
3430
+ //#region src/token-detail/TokenUsageSnippet.tsx
3431
+ function TokenUsageSnippet({ path }) {
3432
+ const { token, cssVar } = useTokenDetailData(path);
3433
+ if (!token) return null;
3434
+ const snippet = `color: ${cssVar};`;
3435
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
3436
+ className: "sb-token-detail__section-header",
3437
+ children: "Usage"
3438
+ }), /* @__PURE__ */ jsxs("div", {
3439
+ className: "sb-token-detail__snippet-row",
3440
+ children: [/* @__PURE__ */ jsx("code", {
3441
+ className: "sb-token-detail__snippet",
3442
+ children: snippet
3443
+ }), /* @__PURE__ */ jsx(CopyButton$1, {
3444
+ value: snippet,
3445
+ label: `Copy usage snippet ${snippet}`
3446
+ })]
3447
+ })] });
3199
3448
  }
3200
- function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3201
- const { resolved, activeTheme, cssVarPrefix } = useProject();
3202
- const rows = useMemo(() => {
3203
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
3204
- if (token.$type !== "strokeStyle") return false;
3205
- return globMatch(path, filter);
3206
- }), {
3207
- by: sortBy,
3208
- dir: sortDir
3209
- }).map(([path, token]) => ({
3210
- path,
3211
- cssVar: makeCssVar(path, cssVarPrefix),
3212
- displayValue: formatTokenValue(token.$value, token.$type, "raw"),
3213
- cssStyle: extractCssStyle(token.$value)
3214
- }));
3215
- }, [
3216
- resolved,
3217
- filter,
3218
- cssVarPrefix,
3219
- sortBy,
3220
- sortDir
3221
- ]);
3222
- const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
3223
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
3224
- ...themeAttrs(cssVarPrefix, activeTheme),
3225
- children: /* @__PURE__ */ jsx("div", {
3226
- className: "sb-block__empty",
3227
- children: "No strokeStyle tokens match this filter."
3449
+ //#endregion
3450
+ //#region src/TokenDetail.tsx
3451
+ function TokenDetail({ path, heading }) {
3452
+ const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
3453
+ const colorFormat = useColorFormat();
3454
+ const theme = themeAttrs(cssVarPrefix, activeTheme);
3455
+ if (!token) return /* @__PURE__ */ jsx("div", {
3456
+ ...theme,
3457
+ className: cx(theme["className"], "sb-token-detail"),
3458
+ children: /* @__PURE__ */ jsxs("div", {
3459
+ className: "sb-token-detail__missing",
3460
+ children: [
3461
+ "Token ",
3462
+ /* @__PURE__ */ jsx("code", { children: path }),
3463
+ " not found in theme ",
3464
+ /* @__PURE__ */ jsx("strong", { children: activeTheme }),
3465
+ "."
3466
+ ]
3228
3467
  })
3229
3468
  });
3469
+ const { listing } = useProject();
3470
+ const isColor = token.$type === "color";
3471
+ const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
3472
+ const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
3473
+ const outOfGamut = gamut?.outOfGamut ?? false;
3230
3474
  return /* @__PURE__ */ jsxs("div", {
3231
- ...themeAttrs(cssVarPrefix, activeTheme),
3232
- children: [/* @__PURE__ */ jsx("div", {
3233
- className: "sb-block__caption",
3234
- children: captionText
3235
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
3236
- className: "sb-stroke-style-sample__row",
3237
- children: [
3238
- /* @__PURE__ */ jsxs("div", {
3239
- className: "sb-stroke-style-sample__meta",
3240
- children: [/* @__PURE__ */ jsx("span", {
3241
- className: "sb-stroke-style-sample__path",
3242
- children: row.path
3243
- }), /* @__PURE__ */ jsx("span", {
3244
- className: "sb-stroke-style-sample__value",
3245
- children: row.displayValue
3246
- })]
3247
- }),
3248
- row.cssStyle ? /* @__PURE__ */ jsx("div", {
3249
- className: "sb-stroke-style-sample__line",
3250
- style: { borderTopStyle: row.cssStyle },
3251
- "aria-hidden": true
3252
- }) : /* @__PURE__ */ jsx("span", {
3253
- className: "sb-stroke-style-sample__object-fallback",
3254
- children: "Object-form (dashArray + lineCap) no pure CSS `border-style` equivalent."
3255
- }),
3256
- /* @__PURE__ */ jsx("span", {
3257
- className: "sb-stroke-style-sample__css-var",
3258
- children: row.cssVar
3259
- })
3260
- ]
3261
- }, row.path))]
3475
+ ...theme,
3476
+ className: cx(theme["className"], "sb-token-detail"),
3477
+ children: [
3478
+ /* @__PURE__ */ jsx(TokenHeader, {
3479
+ path,
3480
+ ...heading !== void 0 && { heading }
3481
+ }),
3482
+ /* @__PURE__ */ jsxs("div", {
3483
+ className: "sb-token-detail__section-header",
3484
+ children: ["Resolved value · ", activeTheme]
3485
+ }),
3486
+ /* @__PURE__ */ jsx(CompositePreview, { path }),
3487
+ /* @__PURE__ */ jsx(CompositeBreakdown, { path }),
3488
+ /* @__PURE__ */ jsxs("div", {
3489
+ className: "sb-token-detail__chain",
3490
+ children: [
3491
+ isColor && /* @__PURE__ */ jsx("span", {
3492
+ className: "sb-token-detail__swatch",
3493
+ style: { background: cssVar },
3494
+ "aria-hidden": true
3495
+ }),
3496
+ /* @__PURE__ */ jsx("span", { children: value }),
3497
+ outOfGamut && /* @__PURE__ */ jsx("span", {
3498
+ title: "Out of sRGB gamut for this format",
3499
+ "aria-label": "out of gamut",
3500
+ style: { marginLeft: 6 },
3501
+ children: ""
3502
+ }),
3503
+ /* @__PURE__ */ jsx(CopyButton$1, {
3504
+ value,
3505
+ label: `Copy value ${value}`
3506
+ })
3507
+ ]
3508
+ }),
3509
+ /* @__PURE__ */ jsx(AliasChain, { path }),
3510
+ /* @__PURE__ */ jsx(AliasedBy, { path }),
3511
+ /* @__PURE__ */ jsx(TokenUsageSnippet, { path }),
3512
+ /* @__PURE__ */ jsx(ConsumerOutput, { path }),
3513
+ /* @__PURE__ */ jsx(AxisVariance, { path })
3514
+ ]
3515
+ });
3516
+ }
3517
+ //#endregion
3518
+ //#region src/internal/DetailOverlay.tsx
3519
+ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
3520
+ useEffect(() => {
3521
+ const onKey = (e) => {
3522
+ if (e.key === "Escape") onClose();
3523
+ };
3524
+ window.addEventListener("keydown", onKey);
3525
+ return () => window.removeEventListener("keydown", onKey);
3526
+ }, [onClose]);
3527
+ return /* @__PURE__ */ jsx("div", {
3528
+ className: "sb-detail-overlay__backdrop",
3529
+ onClick: onClose,
3530
+ role: "presentation",
3531
+ "data-testid": testId,
3532
+ children: /* @__PURE__ */ jsxs("div", {
3533
+ className: "sb-detail-overlay__panel",
3534
+ onClick: (e) => e.stopPropagation(),
3535
+ role: "dialog",
3536
+ "aria-modal": "true",
3537
+ "aria-label": `Token detail for ${path}`,
3538
+ children: [/* @__PURE__ */ jsx("button", {
3539
+ type: "button",
3540
+ className: "sb-detail-overlay__close",
3541
+ onClick: onClose,
3542
+ "aria-label": "Close",
3543
+ "data-testid": `${testId}-close`,
3544
+ children: "×"
3545
+ }), /* @__PURE__ */ jsx(TokenDetail, { path })]
3546
+ })
3262
3547
  });
3263
3548
  }
3264
3549
  //#endregion
@@ -3567,16 +3852,16 @@ function LeafRow({ node, onLeafClick }) {
3567
3852
  });
3568
3853
  }
3569
3854
  function LeafPreview({ path, token }) {
3570
- const { cssVarPrefix } = useProject();
3855
+ const project = useProject();
3571
3856
  const colorFormat = useColorFormat();
3572
3857
  const type = token.$type;
3573
3858
  if (type === "color") {
3574
- const cssVar = makeCssVar(path, cssVarPrefix);
3859
+ const cssVar = resolveCssVar(path, project);
3575
3860
  return /* @__PURE__ */ jsxs("span", {
3576
3861
  className: "sb-token-navigator__preview-box",
3577
3862
  children: [/* @__PURE__ */ jsx("span", {
3578
3863
  className: "sb-token-navigator__value",
3579
- children: formatTokenValue(token.$value, type, colorFormat)
3864
+ children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
3580
3865
  }), /* @__PURE__ */ jsx("span", {
3581
3866
  className: "sb-token-navigator__color-swatch",
3582
3867
  style: { background: cssVar },
@@ -3588,7 +3873,7 @@ function LeafPreview({ path, token }) {
3588
3873
  className: "sb-token-navigator__preview-box",
3589
3874
  children: [/* @__PURE__ */ jsx("span", {
3590
3875
  className: "sb-token-navigator__value",
3591
- children: formatTokenValue(token.$value, type, colorFormat)
3876
+ children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
3592
3877
  }), /* @__PURE__ */ jsx("span", {
3593
3878
  className: "sb-token-navigator__preview-dimension",
3594
3879
  children: /* @__PURE__ */ jsx(DimensionBar, {
@@ -3622,14 +3907,15 @@ function LeafPreview({ path, token }) {
3622
3907
  className: "sb-token-navigator__preview-box",
3623
3908
  children: /* @__PURE__ */ jsx("span", {
3624
3909
  className: "sb-token-navigator__value",
3625
- children: formatTokenValue(token.$value, type, colorFormat)
3910
+ children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
3626
3911
  })
3627
3912
  });
3628
3913
  }
3629
3914
  //#endregion
3630
3915
  //#region src/TokenTable.tsx
3631
3916
  function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
3632
- const { resolved, activeTheme, cssVarPrefix } = useProject();
3917
+ const project = useProject();
3918
+ const { resolved, activeTheme, cssVarPrefix } = project;
3633
3919
  const colorFormat = useColorFormat();
3634
3920
  const [selectedPath, setSelectedPath] = useState(null);
3635
3921
  const [query, setQuery] = useState("");
@@ -3643,13 +3929,13 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
3643
3929
  dir: sortDir
3644
3930
  }).map(([path, token]) => {
3645
3931
  const isColor = token.$type === "color";
3646
- const color = isColor ? formatColor(token.$value, colorFormat) : null;
3932
+ const color = isColor ? resolveColorValue(path, token.$value, colorFormat, project) : null;
3647
3933
  return {
3648
3934
  path,
3649
3935
  type: token.$type ?? "",
3650
- value: formatTokenValue(token.$value, token.$type, colorFormat),
3936
+ value: formatTokenValue(token.$value, token.$type, colorFormat, project.listing[path]),
3651
3937
  outOfGamut: color?.outOfGamut ?? false,
3652
- cssVar: makeCssVar(path, cssVarPrefix),
3938
+ cssVar: resolveCssVar(path, project),
3653
3939
  isColor
3654
3940
  };
3655
3941
  });
@@ -3657,7 +3943,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
3657
3943
  resolved,
3658
3944
  filter,
3659
3945
  type,
3660
- cssVarPrefix,
3946
+ project,
3661
3947
  colorFormat,
3662
3948
  sortBy,
3663
3949
  sortDir