@unpunnyfuns/swatchbook-blocks 0.15.0 → 0.16.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,12 +1,12 @@
1
1
  import './style.css';
2
2
  import Color from "colorjs.io";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
3
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { addons } from "storybook/preview-api";
6
6
  import { axes, css, cssVarPrefix, defaultTheme, diagnostics, presets, themes, themesResolved } from "virtual:swatchbook/tokens";
7
+ import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
7
8
  import cx from "clsx";
8
9
  import { analyzeAxisVariance } from "@unpunnyfuns/swatchbook-core/variance";
9
- import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
10
10
  //#region src/format-color.ts
11
11
  const COLOR_FORMATS = [
12
12
  "hex",
@@ -872,150 +872,54 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
872
872
  });
873
873
  }
874
874
  //#endregion
875
- //#region src/Diagnostics.tsx
876
- const severityLabel = {
877
- error: "ERROR",
878
- warn: "WARN",
879
- info: "INFO"
880
- };
881
- function summaryText(diagnostics) {
882
- if (diagnostics.length === 0) return "✔ OK · no diagnostics";
883
- const counts = {
884
- error: 0,
885
- warn: 0,
886
- info: 0
887
- };
888
- for (const d of diagnostics) counts[d.severity] += 1;
889
- const parts = [];
890
- if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
891
- if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
892
- if (counts.info > 0) parts.push(`${counts.info} info`);
893
- return parts.join(" · ");
894
- }
895
- function diagnosticKey(d, i) {
896
- return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
897
- }
898
- function summaryVariant(diagnostics) {
899
- if (diagnostics.length === 0) return "ok";
900
- if (diagnostics.some((d) => d.severity === "error")) return "error";
901
- if (diagnostics.some((d) => d.severity === "warn")) return "warn";
902
- return null;
903
- }
875
+ //#region src/internal/CopyButton.tsx
904
876
  /**
905
- * Render the project's load diagnostics parser errors, resolver warnings,
906
- * disabled-axes validation issues, etc. as a collapsible list. Auto-opens
907
- * when the project carries errors or warnings; stays collapsed for clean
908
- * loads and info-only loads.
909
- *
910
- * Replaces the diagnostics section from the addon's (now-retired) Design
911
- * Tokens panel. Consumers compose it alongside TokenNavigator / TokenTable
912
- * on their own MDX pages.
877
+ * Copy the given string to the clipboard and briefly surface a "Copied!"
878
+ * state. Falls back silently on unsupported clipboard APIs (older Safari,
879
+ * insecure origins) — the click still happens, the user just won't see the
880
+ * tick. No custom permission prompt: relies on the browser's native user-
881
+ * activation gate.
913
882
  */
914
- function Diagnostics({ caption } = {}) {
915
- const { activeTheme, cssVarPrefix, diagnostics } = useProject();
916
- const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
917
- const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
918
- const variant = summaryVariant(diagnostics);
919
- return /* @__PURE__ */ jsx("div", {
920
- ...themeAttrs(cssVarPrefix, activeTheme),
921
- "data-testid": "diagnostics",
922
- children: /* @__PURE__ */ jsxs("details", {
923
- open: hasErrorsOrWarnings,
924
- children: [/* @__PURE__ */ jsx("summary", {
925
- className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
926
- children: headingText
927
- }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
928
- className: "sb-diagnostics__list",
929
- children: diagnostics.map((d, i) => /* @__PURE__ */ jsxs("li", {
930
- className: "sb-diagnostics__row",
931
- children: [/* @__PURE__ */ jsx("span", {
932
- className: cx("sb-diagnostics__label", { [`sb-diagnostics__label--${d.severity}`]: d.severity !== "info" }),
933
- children: severityLabel[d.severity]
934
- }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", { children: d.message }), (d.group || d.filename) && /* @__PURE__ */ jsx("div", {
935
- className: "sb-diagnostics__meta",
936
- children: [
937
- d.group,
938
- d.filename,
939
- d.line ? `:${d.line}` : ""
940
- ].filter(Boolean).join(" · ")
941
- })] })]
942
- }, diagnosticKey(d, i)))
943
- })]
883
+ function CopyButton$1({ value, label, variant = "icon", className }) {
884
+ const [copied, setCopied] = useState(false);
885
+ const timerRef = useRef(null);
886
+ useEffect(() => {
887
+ return () => {
888
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
889
+ };
890
+ }, []);
891
+ const handleClick = useCallback(() => {
892
+ try {
893
+ navigator.clipboard?.writeText(value);
894
+ } catch {
895
+ return;
896
+ }
897
+ setCopied(true);
898
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
899
+ timerRef.current = setTimeout(() => setCopied(false), 1500);
900
+ }, [value]);
901
+ const ariaLabel = label ?? `Copy ${value}`;
902
+ const classes = ["sb-copy-button", `sb-copy-button--${variant}`];
903
+ if (copied) classes.push("sb-copy-button--copied");
904
+ if (className) classes.push(className);
905
+ return /* @__PURE__ */ jsx("button", {
906
+ type: "button",
907
+ className: classes.join(" "),
908
+ onClick: handleClick,
909
+ "aria-label": ariaLabel,
910
+ title: ariaLabel,
911
+ "data-copied": copied ? "true" : void 0,
912
+ children: variant === "text" ? /* @__PURE__ */ jsx("span", {
913
+ className: "sb-copy-button__text",
914
+ children: copied ? "Copied" : "Copy"
915
+ }) : /* @__PURE__ */ jsx("span", {
916
+ className: "sb-copy-button__glyph",
917
+ "aria-hidden": true,
918
+ children: copied ? "✓" : "⧉"
944
919
  })
945
920
  });
946
921
  }
947
922
  //#endregion
948
- //#region src/dimension-scale/DimensionBar.tsx
949
- const MAX_RENDER_PX$1 = 480;
950
- const styles$1 = {
951
- bar: {
952
- height: 14,
953
- background: "var(--swatchbook-accent-bg, #3b82f6)",
954
- borderRadius: 2,
955
- minWidth: 1
956
- },
957
- radiusSample: {
958
- width: 56,
959
- height: 56,
960
- background: "var(--swatchbook-accent-bg, #3b82f6)",
961
- border: BORDER_STRONG
962
- },
963
- sizeSample: {
964
- background: "var(--swatchbook-accent-bg, #3b82f6)",
965
- border: BORDER_STRONG,
966
- minWidth: 1,
967
- minHeight: 1
968
- }
969
- };
970
- /**
971
- * Convert a DTCG dimension `$value` (`{ value, unit }`) to pixels for the
972
- * purpose of deciding whether to cap the rendered bar. Returns `NaN` for
973
- * units we can't reasonably approximate (ex / ch / %), which the caller
974
- * treats as "render at cssVar but don't cap".
975
- */
976
- function toPixels$1(raw) {
977
- if (raw == null || typeof raw !== "object") return NaN;
978
- const v = raw;
979
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
980
- switch (v.unit) {
981
- case "px": return v.value;
982
- case "rem":
983
- case "em": return v.value * 16;
984
- default: return NaN;
985
- }
986
- }
987
- function DimensionBar({ path, kind = "length" }) {
988
- const { resolved, cssVarPrefix } = useProject();
989
- const cssVar = makeCssVar(path, cssVarPrefix);
990
- const token = resolved[path];
991
- const pxValue = toPixels$1(token?.$value);
992
- const cappedValue = Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX$1 ? `${MAX_RENDER_PX$1}px` : cssVar;
993
- switch (kind) {
994
- case "radius": return /* @__PURE__ */ jsx("div", {
995
- style: {
996
- ...styles$1.radiusSample,
997
- borderRadius: cssVar
998
- },
999
- "aria-hidden": true
1000
- });
1001
- case "size": return /* @__PURE__ */ jsx("div", {
1002
- style: {
1003
- ...styles$1.sizeSample,
1004
- width: cappedValue,
1005
- height: cappedValue
1006
- },
1007
- "aria-hidden": true
1008
- });
1009
- default: return /* @__PURE__ */ jsx("div", {
1010
- style: {
1011
- ...styles$1.bar,
1012
- width: cappedValue
1013
- },
1014
- "aria-hidden": true
1015
- });
1016
- }
1017
- }
1018
- //#endregion
1019
923
  //#region src/internal/format-token-value.ts
1020
924
  /**
1021
925
  * Produce a single-line display string for any DTCG token `$value`,
@@ -1155,768 +1059,596 @@ function formatUnknown(v) {
1155
1059
  }
1156
1060
  }
1157
1061
  //#endregion
1158
- //#region src/DimensionScale.tsx
1159
- const MAX_RENDER_PX = 480;
1160
- function toPixels(raw) {
1161
- if (raw == null || typeof raw !== "object") return NaN;
1162
- const v = raw;
1163
- if (typeof v.value !== "number" || typeof v.unit !== "string") return NaN;
1164
- switch (v.unit) {
1165
- case "px": return v.value;
1166
- case "rem":
1167
- case "em": return v.value * 16;
1168
- default: return NaN;
1169
- }
1170
- }
1171
- function DimensionScale({ filter, kind = "length", caption, sortBy = "value", sortDir = "asc" }) {
1172
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1173
- const rows = useMemo(() => {
1174
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1175
- if (token.$type !== "dimension") return false;
1176
- return globMatch(path, filter);
1177
- }), {
1178
- by: sortBy,
1179
- dir: sortDir
1180
- }).map(([path, token]) => {
1181
- const pxValue = toPixels(token.$value);
1182
- return {
1183
- path,
1184
- cssVar: makeCssVar(path, cssVarPrefix),
1185
- displayValue: formatTokenValue(token.$value, token.$type, "raw"),
1186
- pxValue,
1187
- capped: Number.isFinite(pxValue) && pxValue > MAX_RENDER_PX
1188
- };
1189
- });
1190
- }, [
1191
- resolved,
1192
- filter,
1193
- cssVarPrefix,
1194
- sortBy,
1195
- sortDir
1196
- ]);
1197
- const captionText = caption ?? `${rows.length} dimension${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1198
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1199
- ...themeAttrs(cssVarPrefix, activeTheme),
1200
- children: /* @__PURE__ */ jsx("div", {
1201
- className: "sb-block__empty",
1202
- children: "No dimension tokens match this filter."
1203
- })
1204
- });
1205
- return /* @__PURE__ */ jsxs("div", {
1206
- ...themeAttrs(cssVarPrefix, activeTheme),
1207
- children: [/* @__PURE__ */ jsx("div", {
1208
- className: "sb-block__caption",
1209
- children: captionText
1210
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1211
- className: "sb-dimension-scale__row",
1212
- children: [
1213
- /* @__PURE__ */ jsxs("div", {
1214
- className: "sb-dimension-scale__meta",
1215
- children: [/* @__PURE__ */ jsx("span", {
1216
- className: "sb-dimension-scale__path",
1217
- children: row.path
1218
- }), /* @__PURE__ */ jsx("span", {
1219
- className: "sb-dimension-scale__specs",
1220
- children: row.displayValue
1221
- })]
1222
- }),
1223
- /* @__PURE__ */ jsxs("div", {
1224
- className: "sb-dimension-scale__visual-cell",
1225
- children: [/* @__PURE__ */ jsx(DimensionBar, {
1226
- path: row.path,
1227
- kind
1228
- }), row.capped && /* @__PURE__ */ jsxs("span", {
1229
- className: "sb-dimension-scale__cap",
1230
- children: [
1231
- "capped at ",
1232
- MAX_RENDER_PX,
1233
- "px"
1234
- ]
1235
- })]
1236
- }),
1237
- /* @__PURE__ */ jsx("span", {
1238
- className: "sb-dimension-scale__css-var",
1239
- children: row.cssVar
1240
- })
1241
- ]
1242
- }, row.path))]
1243
- });
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
+ };
1244
1077
  }
1245
1078
  //#endregion
1246
- //#region src/FontFamilySample.tsx
1247
- function stackString(raw) {
1248
- if (typeof raw === "string") return raw;
1249
- if (Array.isArray(raw)) return raw.map(String).join(", ");
1250
- return "";
1251
- }
1252
- function FontFamilySample({ filter, sample = "The quick brown fox jumps over the lazy dog.", caption, sortBy = "path", sortDir = "asc" }) {
1253
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1254
- const rows = useMemo(() => {
1255
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1256
- if (token.$type !== "fontFamily") return false;
1257
- return globMatch(path, filter);
1258
- }), {
1259
- by: sortBy,
1260
- dir: sortDir
1261
- }).map(([path, token]) => ({
1262
- path,
1263
- cssVar: makeCssVar(path, cssVarPrefix),
1264
- stack: stackString(token.$value)
1265
- }));
1266
- }, [
1267
- resolved,
1268
- filter,
1269
- cssVarPrefix,
1270
- sortBy,
1271
- sortDir
1272
- ]);
1273
- const captionText = caption ?? `${rows.length} fontFamily token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontFamily" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1274
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1275
- ...themeAttrs(cssVarPrefix, activeTheme),
1276
- children: /* @__PURE__ */ jsx("div", {
1277
- className: "sb-block__empty",
1278
- children: "No fontFamily tokens match this filter."
1279
- })
1280
- });
1281
- return /* @__PURE__ */ jsxs("div", {
1282
- ...themeAttrs(cssVarPrefix, activeTheme),
1283
- children: [/* @__PURE__ */ jsx("div", {
1284
- className: "sb-block__caption",
1285
- children: captionText
1286
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1287
- className: "sb-font-family-sample__row",
1288
- children: [
1289
- /* @__PURE__ */ jsxs("div", {
1290
- className: "sb-font-family-sample__meta",
1291
- children: [/* @__PURE__ */ jsx("span", {
1292
- className: "sb-font-family-sample__path",
1293
- children: row.path
1294
- }), /* @__PURE__ */ jsx("span", {
1295
- className: "sb-font-family-sample__stack",
1296
- children: row.stack
1297
- })]
1298
- }),
1299
- /* @__PURE__ */ jsx("div", {
1300
- className: "sb-font-family-sample__sample",
1301
- style: { fontFamily: row.cssVar },
1302
- children: sample
1303
- }),
1304
- /* @__PURE__ */ jsx("span", {
1305
- className: "sb-font-family-sample__css-var",
1306
- children: row.cssVar
1307
- })
1308
- ]
1309
- }, row.path))]
1310
- });
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
+ })] });
1311
1105
  }
1312
1106
  //#endregion
1313
- //#region src/FontWeightScale.tsx
1314
- function toWeight(raw) {
1315
- if (typeof raw === "number") return raw;
1316
- if (typeof raw === "string") {
1317
- const n = Number.parseInt(raw, 10);
1318
- return Number.isFinite(n) ? n : NaN;
1319
- }
1320
- return NaN;
1321
- }
1322
- function FontWeightScale({ filter, sample = "Aa", caption, sortBy = "value", sortDir = "asc" }) {
1323
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1324
- const rows = useMemo(() => {
1325
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1326
- if (token.$type !== "fontWeight") return false;
1327
- return globMatch(path, filter);
1328
- }), {
1329
- by: sortBy,
1330
- dir: sortDir
1331
- }).map(([path, token]) => ({
1332
- path,
1333
- cssVar: makeCssVar(path, cssVarPrefix),
1334
- display: token.$value == null ? "" : String(token.$value),
1335
- weight: toWeight(token.$value)
1336
- }));
1337
- }, [
1338
- resolved,
1339
- filter,
1340
- cssVarPrefix,
1341
- sortBy,
1342
- sortDir
1343
- ]);
1344
- const captionText = caption ?? `${rows.length} fontWeight token${rows.length === 1 ? "" : "s"}${filter && filter !== "fontWeight" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1345
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1346
- ...themeAttrs(cssVarPrefix, activeTheme),
1347
- children: /* @__PURE__ */ jsx("div", {
1348
- className: "sb-block__empty",
1349
- children: "No fontWeight tokens match this filter."
1350
- })
1351
- });
1352
- return /* @__PURE__ */ jsxs("div", {
1353
- ...themeAttrs(cssVarPrefix, activeTheme),
1354
- children: [/* @__PURE__ */ jsx("div", {
1355
- className: "sb-block__caption",
1356
- children: captionText
1357
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1358
- className: "sb-font-weight-scale__row",
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",
1359
1132
  children: [
1360
- /* @__PURE__ */ jsxs("div", {
1361
- className: "sb-font-weight-scale__meta",
1362
- children: [/* @__PURE__ */ jsx("span", {
1363
- className: "sb-font-weight-scale__path",
1364
- children: row.path
1365
- }), /* @__PURE__ */ jsx("span", {
1366
- className: "sb-font-weight-scale__value",
1367
- children: row.display
1368
- })]
1369
- }),
1370
- /* @__PURE__ */ jsx("div", {
1371
- className: "sb-font-weight-scale__sample",
1372
- style: { fontWeight: row.cssVar },
1373
- children: sample
1374
- }),
1375
- /* @__PURE__ */ jsx("span", {
1376
- className: "sb-font-weight-scale__css-var",
1377
- children: row.cssVar
1378
- })
1133
+ "Further descendants truncated at depth ",
1134
+ ALIASED_BY_DEPTH_CAP,
1135
+ "."
1379
1136
  ]
1380
- }, row.path))]
1381
- });
1137
+ })
1138
+ ] });
1382
1139
  }
1383
- //#endregion
1384
- //#region src/GradientPalette.tsx
1385
- function asStops(raw) {
1386
- if (!Array.isArray(raw)) return [];
1387
- return raw;
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
+ })] });
1388
1155
  }
1389
- const pct = (n) => `${(n * 100).toFixed(3)}%`;
1390
- function stopCssColor(stop) {
1391
- const color = stop.color;
1392
- if (!color || !Array.isArray(color.components) || color.components.length < 3) return "transparent";
1393
- const [r, g, b] = color.components;
1394
- if (r === void 0 || g === void 0 || b === void 0) return "transparent";
1395
- const alpha = color.alpha ?? 1;
1396
- return alpha === 1 ? `rgb(${pct(r)} ${pct(g)} ${pct(b)})` : `rgb(${pct(r)} ${pct(g)} ${pct(b)} / ${alpha})`;
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));
1397
1161
  }
1398
- function stopKey(path, stop, fallback) {
1399
- return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
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
+ };
1400
1182
  }
1401
- function GradientPalette({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1402
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1403
- const colorFormat = useColorFormat();
1404
- const rows = useMemo(() => {
1405
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1406
- if (token.$type !== "gradient") return false;
1407
- return globMatch(path, filter);
1408
- }), {
1409
- by: sortBy,
1410
- dir: sortDir
1411
- }).map(([path, token]) => ({
1412
- path,
1413
- cssVar: makeCssVar(path, cssVarPrefix),
1414
- stops: asStops(token.$value)
1415
- }));
1416
- }, [
1417
- resolved,
1418
- filter,
1419
- cssVarPrefix,
1420
- sortBy,
1421
- sortDir
1422
- ]);
1423
- const captionText = caption ?? `${rows.length} gradient${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1424
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1425
- ...themeAttrs(cssVarPrefix, activeTheme),
1426
- children: /* @__PURE__ */ jsx("div", {
1427
- className: "sb-block__empty",
1428
- children: "No gradient tokens match this filter."
1429
- })
1430
- });
1431
- return /* @__PURE__ */ jsxs("div", {
1432
- ...themeAttrs(cssVarPrefix, activeTheme),
1433
- children: [/* @__PURE__ */ jsx("div", {
1434
- className: "sb-block__caption",
1435
- children: captionText
1436
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1437
- className: "sb-gradient-palette__row",
1438
- children: [
1439
- /* @__PURE__ */ jsxs("div", {
1440
- className: "sb-gradient-palette__meta",
1441
- children: [/* @__PURE__ */ jsx("span", {
1442
- className: "sb-gradient-palette__path",
1443
- children: row.path
1444
- }), /* @__PURE__ */ jsx("span", {
1445
- className: "sb-gradient-palette__css-var",
1446
- children: row.cssVar
1447
- })]
1448
- }),
1449
- /* @__PURE__ */ jsx("div", {
1450
- className: "sb-gradient-palette__sample",
1451
- style: { background: `linear-gradient(to right, ${row.cssVar})` },
1452
- "aria-hidden": true
1453
- }),
1454
- /* @__PURE__ */ jsx("div", {
1455
- className: "sb-gradient-palette__stops",
1456
- children: row.stops.map((stop, i) => /* @__PURE__ */ jsxs("div", {
1457
- className: "sb-gradient-palette__stop-row",
1458
- children: [
1459
- /* @__PURE__ */ jsx("span", {
1460
- className: "sb-gradient-palette__stop-swatch",
1461
- style: { background: stopCssColor(stop) },
1462
- "aria-hidden": true
1463
- }),
1464
- /* @__PURE__ */ jsx("span", { children: formatColor(stop.color, colorFormat).value }),
1465
- /* @__PURE__ */ jsxs("span", {
1466
- className: "sb-gradient-palette__stop-position",
1467
- children: [
1468
- "@ ",
1469
- ((stop.position ?? 0) * 100).toFixed(0),
1470
- "%"
1471
- ]
1472
- })
1473
- ]
1474
- }, stopKey(row.path, stop, i)))
1475
- })
1476
- ]
1477
- }, row.path))]
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 });
1478
1188
  });
1479
1189
  }
1480
- //#endregion
1481
- //#region src/internal/prefers-reduced-motion.ts
1482
- /**
1483
- * Reactive `prefers-reduced-motion: reduce` detector. Returns the current
1484
- * match and updates if the user toggles the OS-level preference.
1485
- */
1486
- function usePrefersReducedMotion() {
1487
- const [reduced, setReduced] = useState(false);
1488
- useEffect(() => {
1489
- if (typeof window === "undefined") return;
1490
- const query = window.matchMedia("(prefers-reduced-motion: reduce)");
1491
- setReduced(query.matches);
1492
- const onChange = (e) => setReduced(e.matches);
1493
- query.addEventListener("change", onChange);
1494
- return () => query.removeEventListener("change", onChange);
1495
- }, []);
1496
- return reduced;
1497
- }
1498
- //#endregion
1499
- //#region src/motion-preview/MotionSample.tsx
1500
- const DEFAULT_DURATION_MS = 300;
1501
- const DEFAULT_EASING = "cubic-bezier(0.2, 0, 0, 1)";
1502
- const styles = {
1503
- track: {
1504
- position: "relative",
1505
- height: 36,
1506
- background: SURFACE_MUTED,
1507
- borderRadius: 18,
1508
- overflow: "hidden"
1509
- },
1510
- ball: {
1511
- position: "absolute",
1512
- top: "50%",
1513
- width: 28,
1514
- height: 28,
1515
- marginTop: -14,
1516
- borderRadius: "50%",
1517
- background: "var(--swatchbook-accent-bg, #3b82f6)"
1518
- },
1519
- reducedMotion: {
1520
- fontSize: 11,
1521
- color: TEXT_MUTED,
1522
- fontStyle: "italic"
1523
- }
1524
- };
1525
- function extractDurationMs(raw) {
1526
- if (raw == null) return NaN;
1527
- if (typeof raw === "object") {
1528
- const v = raw;
1529
- if (typeof v.value === "number" && typeof v.unit === "string") {
1530
- if (v.unit === "ms") return v.value;
1531
- if (v.unit === "s") return v.value * 1e3;
1532
- }
1533
- }
1534
- return NaN;
1535
- }
1536
- function extractCubicBezier(raw) {
1537
- if (Array.isArray(raw) && raw.length === 4 && raw.every((n) => typeof n === "number")) return `cubic-bezier(${raw.map((n) => Number(n).toFixed(3)).join(", ")})`;
1538
- return null;
1539
- }
1540
- function asDuration(raw, themeTokens, fallback) {
1541
- const direct = extractDurationMs(raw);
1542
- if (Number.isFinite(direct)) return direct;
1543
- if (typeof raw === "string") {
1544
- const match = raw.match(/^\{([^}]+)\}$/);
1545
- if (match && match[1]) {
1546
- const referenced = themeTokens[match[1]];
1547
- const resolved = extractDurationMs(referenced?.$value);
1548
- if (Number.isFinite(resolved)) return resolved;
1549
- }
1550
- }
1551
- return fallback;
1552
- }
1553
- function asEasing(raw, themeTokens, fallback) {
1554
- const direct = extractCubicBezier(raw);
1555
- if (direct) return direct;
1556
- if (typeof raw === "string") {
1557
- const match = raw.match(/^\{([^}]+)\}$/);
1558
- if (match && match[1]) {
1559
- const referenced = themeTokens[match[1]];
1560
- const resolved = extractCubicBezier(referenced?.$value);
1561
- if (resolved) return resolved;
1562
- }
1190
+ function treeHasTruncation(nodes) {
1191
+ for (const n of nodes) {
1192
+ if (n.truncated) return true;
1193
+ if (treeHasTruncation(n.children)) return true;
1563
1194
  }
1564
- return fallback;
1195
+ return false;
1565
1196
  }
1566
- function resolveMotionSpec(token, themeTokens) {
1567
- if (!token) return null;
1568
- const type = token.$type;
1569
- if (type === "transition") {
1570
- const v = token.$value ?? {};
1571
- return {
1572
- durationMs: asDuration(v.duration, themeTokens, DEFAULT_DURATION_MS),
1573
- easing: asEasing(v.timingFunction, themeTokens, DEFAULT_EASING)
1574
- };
1575
- }
1576
- if (type === "duration") {
1577
- const durationMs = extractDurationMs(token.$value);
1578
- if (!Number.isFinite(durationMs)) return null;
1579
- return {
1580
- durationMs,
1581
- easing: DEFAULT_EASING
1582
- };
1583
- }
1584
- if (type === "cubicBezier") {
1585
- const easing = extractCubicBezier(token.$value);
1586
- if (!easing) return null;
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);
1587
1207
  return {
1588
- durationMs: DEFAULT_DURATION_MS,
1589
- easing
1590
- };
1591
- }
1592
- return null;
1593
- }
1594
- function MotionSample({ path, speed = 1, runKey = 0 }) {
1595
- const { resolved } = useProject();
1596
- const reducedMotion = usePrefersReducedMotion();
1597
- const spec = useMemo(() => resolveMotionSpec(resolved[path], resolved), [resolved, path]);
1598
- const durationMs = spec?.durationMs ?? DEFAULT_DURATION_MS;
1599
- const easing = spec?.easing ?? DEFAULT_EASING;
1600
- const scaledDuration = Math.max(1, durationMs / speed);
1601
- const [phase, setPhase] = useState(0);
1602
- useEffect(() => {
1603
- if (reducedMotion) return;
1604
- setPhase(0);
1605
- const id = requestAnimationFrame(() => setPhase(1));
1606
- const loop = window.setInterval(() => {
1607
- setPhase((p) => p === 0 ? 1 : 0);
1608
- }, scaledDuration * 2);
1609
- return () => {
1610
- cancelAnimationFrame(id);
1611
- window.clearInterval(loop);
1208
+ kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
1209
+ varyingAxes: result.varyingAxes
1612
1210
  };
1613
1211
  }, [
1614
- scaledDuration,
1615
- runKey,
1616
- reducedMotion
1212
+ path,
1213
+ axes,
1214
+ themes,
1215
+ themesResolved
1617
1216
  ]);
1618
- if (reducedMotion) return /* @__PURE__ */ jsx("div", {
1619
- style: styles.reducedMotion,
1620
- children: "Animation suppressed by `prefers-reduced-motion: reduce`."
1621
- });
1622
- return /* @__PURE__ */ jsx("div", {
1623
- style: styles.track,
1624
- children: /* @__PURE__ */ jsx("div", {
1625
- style: {
1626
- ...styles.ball,
1627
- left: phase === 1 ? "calc(100% - 32px)" : "4px",
1628
- transition: `left ${scaledDuration}ms ${easing}`
1629
- },
1630
- "aria-hidden": true
1631
- })
1632
- });
1633
- }
1634
- //#endregion
1635
- //#region src/MotionPreview.tsx
1636
- const SPEEDS = [
1637
- .25,
1638
- .5,
1639
- 1,
1640
- 2
1641
- ];
1642
- function formatSpec(row) {
1643
- switch (row.kind) {
1644
- case "transition": return `transition · ${Math.round(row.durationMs)}ms · ${row.easing}`;
1645
- case "duration": return `duration · ${Math.round(row.durationMs)}ms`;
1646
- case "cubicBezier": return `cubicBezier · ${row.easing}`;
1647
- }
1648
- }
1649
- function MotionPreview({ filter, caption }) {
1650
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1651
- const [speed, setSpeed] = useState(1);
1652
- const [run, setRun] = useState(0);
1653
- const reducedMotion = usePrefersReducedMotion();
1654
- const rows = useMemo(() => {
1655
- const collected = [];
1656
- for (const [path, token] of Object.entries(resolved)) {
1657
- if (filter && !globMatch(path, filter)) continue;
1658
- if (!filter && ![
1659
- "transition",
1660
- "duration",
1661
- "cubicBezier"
1662
- ].includes(token.$type ?? "")) continue;
1663
- const kind = token.$type;
1664
- if (!kind) continue;
1665
- const spec = resolveMotionSpec(token, resolved);
1666
- if (!spec) continue;
1667
- collected.push({
1668
- path,
1669
- cssVar: makeCssVar(path, cssVarPrefix),
1670
- durationMs: spec.durationMs,
1671
- easing: spec.easing,
1672
- kind
1673
- });
1674
- }
1675
- collected.sort((a, b) => {
1676
- if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
1677
- return a.path.localeCompare(b.path, void 0, { numeric: true });
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
+ };
1678
1274
  });
1679
- return collected;
1680
- }, [
1681
- resolved,
1682
- filter,
1683
- cssVarPrefix
1684
- ]);
1685
- const captionText = caption ?? `${rows.length} motion token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1686
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1687
- ...themeAttrs(cssVarPrefix, activeTheme),
1688
- children: /* @__PURE__ */ jsx("div", {
1689
- className: "sb-block__empty",
1690
- children: "No motion tokens match this filter."
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 },
1361
+ children: [
1362
+ "Values also vary with ",
1363
+ extra.map((a) => a.name).join(", "),
1364
+ "; matrix shows the slice for the active selection."
1365
+ ]
1691
1366
  })
1367
+ ] });
1368
+ }
1369
+ function valueFor(token, $type, format) {
1370
+ if (!token) return "—";
1371
+ return formatTokenValue(token.$value, $type, format);
1372
+ }
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;
1378
+ }
1379
+ //#endregion
1380
+ //#region src/token-detail/CompositeBreakdown.tsx
1381
+ function CompositeBreakdown({ path }) {
1382
+ const { token, resolved } = useTokenDetailData(path);
1383
+ 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,
1389
+ resolved,
1390
+ colorFormat
1692
1391
  });
1693
- return /* @__PURE__ */ jsxs("div", {
1694
- ...themeAttrs(cssVarPrefix, activeTheme),
1695
- children: [
1696
- /* @__PURE__ */ jsx("div", {
1697
- className: "sb-block__caption",
1698
- children: captionText
1699
- }),
1700
- /* @__PURE__ */ jsxs("div", {
1701
- className: "sb-motion-preview__controls",
1702
- children: [
1703
- /* @__PURE__ */ jsx("span", {
1704
- className: "sb-motion-preview__control-label",
1705
- children: "Speed"
1706
- }),
1707
- SPEEDS.map((s) => /* @__PURE__ */ jsxs("button", {
1708
- type: "button",
1709
- className: cx("sb-motion-preview__speed-btn", { "sb-motion-preview__speed-btn--active": s === speed }),
1710
- onClick: () => setSpeed(s),
1711
- children: [s, "×"]
1712
- }, s)),
1713
- /* @__PURE__ */ jsx("button", {
1714
- type: "button",
1715
- className: "sb-motion-preview__replay-btn",
1716
- onClick: () => setRun((n) => n + 1),
1717
- disabled: reducedMotion,
1718
- title: reducedMotion ? "Disabled by prefers-reduced-motion" : "Replay all",
1719
- children: "↻ Replay"
1720
- })
1721
- ]
1722
- }),
1723
- rows.map((row) => /* @__PURE__ */ jsxs("div", {
1724
- className: "sb-motion-preview__row",
1725
- children: [
1726
- /* @__PURE__ */ jsxs("div", {
1727
- className: "sb-motion-preview__meta",
1728
- children: [/* @__PURE__ */ jsx("span", {
1729
- className: "sb-motion-preview__path",
1730
- children: row.path
1731
- }), /* @__PURE__ */ jsx("span", {
1732
- className: "sb-motion-preview__specs",
1733
- children: formatSpec(row)
1734
- })]
1735
- }),
1736
- /* @__PURE__ */ jsx(MotionSample, {
1737
- path: row.path,
1738
- speed,
1739
- runKey: run
1740
- }),
1741
- /* @__PURE__ */ jsx("span", {
1742
- className: "sb-motion-preview__css-var",
1743
- children: row.cssVar
1744
- })
1745
- ]
1746
- }, row.path))
1747
- ]
1748
- });
1749
1392
  }
1750
- //#endregion
1751
- //#region src/provider.tsx
1752
- /**
1753
- * Wraps a tree of blocks with the token data they need to render.
1754
- *
1755
- * The Storybook addon's preview decorator mounts this automatically, so
1756
- * story/MDX authors typically never see it. Outside Storybook — unit
1757
- * tests, custom React apps, non-Storybook doc sites — consumers construct
1758
- * a {@link ProjectSnapshot} (often imported from a JSON file) and wrap
1759
- * their blocks in this provider.
1760
- */
1761
- function SwatchbookProvider({ value, children }) {
1762
- return /* @__PURE__ */ jsx(SwatchbookContext.Provider, {
1763
- value,
1764
- children
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")
1465
+ ]
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))
1765
1544
  });
1766
1545
  }
1767
- /**
1768
- * Read the current {@link ProjectSnapshot}. Throws if called outside a
1769
- * {@link SwatchbookProvider}; blocks that need to fall back to the
1770
- * virtual module go through the internal `useProject()` hook instead.
1771
- */
1772
- function useSwatchbookData() {
1773
- const value = useOptionalSwatchbookData();
1774
- if (!value) throw new Error("[swatchbook-blocks] useSwatchbookData() called outside <SwatchbookProvider>. Wrap your tree in <SwatchbookProvider value={snapshot}> or render inside a Storybook story.");
1775
- return value;
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
+ })] });
1776
1568
  }
1777
- //#endregion
1778
- //#region src/shadow-preview/ShadowSample.tsx
1779
- const sampleStyle = {
1780
- width: 120,
1781
- height: 56,
1782
- background: SURFACE_RAISED,
1783
- border: BORDER_FAINT,
1784
- borderRadius: 6
1785
- };
1786
- function ShadowSample({ path }) {
1787
- const { cssVarPrefix } = useProject();
1788
- const cssVar = makeCssVar(path, cssVarPrefix);
1789
- return /* @__PURE__ */ jsx("div", {
1790
- style: {
1791
- ...sampleStyle,
1792
- boxShadow: cssVar
1793
- },
1794
- "aria-hidden": true
1795
- });
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);
1796
1573
  }
1797
- //#endregion
1798
- //#region src/ShadowPreview.tsx
1799
- function formatDimension(raw) {
1800
- if (raw == null) return "";
1801
- if (typeof raw === "number") return String(raw);
1802
- if (typeof raw === "string") return raw;
1803
- if (typeof raw === "object") {
1804
- const v = raw;
1805
- if (typeof v.value === "number" && typeof v.unit === "string") return `${v.value}${v.unit}`;
1806
- }
1807
- return JSON.stringify(raw);
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);
1808
1579
  }
1809
- function formatSubColor(raw, format) {
1810
- if (raw == null) return "—";
1811
- return formatColor(raw, format).value;
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);
1812
1588
  }
1813
- function asLayers(raw) {
1814
- if (Array.isArray(raw)) return raw;
1815
- if (raw && typeof raw === "object") return [raw];
1816
- return [];
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;
1817
1598
  }
1818
- function layerKey(path, layer, fallback) {
1819
- return `${path}|${`${formatDimension(layer.offsetX)},${formatDimension(layer.offsetY)}`}|${formatDimension(layer.blur)}|${formatDimension(layer.spread)}|${fallback}`;
1599
+ function pickObjectAliases(v) {
1600
+ if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
1601
+ return v;
1820
1602
  }
1821
- function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1822
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1823
- const colorFormat = useColorFormat();
1824
- const rows = useMemo(() => {
1825
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1826
- if (token.$type !== "shadow") return false;
1827
- return globMatch(path, filter);
1828
- }), {
1829
- by: sortBy,
1830
- dir: sortDir
1831
- }).map(([path, token]) => ({
1832
- path,
1833
- cssVar: makeCssVar(path, cssVarPrefix),
1834
- layers: asLayers(token.$value)
1835
- }));
1836
- }, [
1837
- resolved,
1838
- filter,
1839
- cssVarPrefix,
1840
- sortBy,
1841
- sortDir
1842
- ]);
1843
- const captionText = caption ?? `${rows.length} shadow${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1844
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1845
- ...themeAttrs(cssVarPrefix, activeTheme),
1846
- children: /* @__PURE__ */ jsx("div", {
1847
- className: "sb-block__empty",
1848
- children: "No shadow tokens match this filter."
1849
- })
1850
- });
1851
- return /* @__PURE__ */ jsxs("div", {
1852
- ...themeAttrs(cssVarPrefix, activeTheme),
1853
- children: [/* @__PURE__ */ jsx("div", {
1854
- className: "sb-block__caption",
1855
- children: captionText
1856
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1857
- className: "sb-shadow-preview__row",
1858
- children: [
1859
- /* @__PURE__ */ jsxs("div", {
1860
- className: "sb-shadow-preview__meta",
1861
- children: [/* @__PURE__ */ jsx("span", {
1862
- className: "sb-shadow-preview__path",
1863
- children: row.path
1864
- }), /* @__PURE__ */ jsx("span", {
1865
- className: "sb-shadow-preview__css-var",
1866
- children: row.cssVar
1867
- })]
1868
- }),
1869
- /* @__PURE__ */ jsx("div", {
1870
- className: "sb-shadow-preview__sample-cell",
1871
- children: /* @__PURE__ */ jsx(ShadowSample, { path: row.path })
1872
- }),
1873
- /* @__PURE__ */ jsx("div", {
1874
- className: "sb-shadow-preview__breakdown",
1875
- children: row.layers.length === 1 ? renderLayer(row.layers[0], colorFormat) : row.layers.map((layer, i) => /* @__PURE__ */ jsx(Layer, {
1876
- layer,
1877
- index: i,
1878
- total: row.layers.length,
1879
- colorFormat
1880
- }, layerKey(row.path, layer, i)))
1881
- })
1882
- ]
1883
- }, row.path))]
1884
- });
1603
+ function pickArrayAliases(v) {
1604
+ if (!Array.isArray(v)) return void 0;
1605
+ return v;
1885
1606
  }
1886
- function renderLayer(layer, format) {
1887
- if (!layer) return [];
1888
- const entries = [
1889
- ["offset", `${formatDimension(layer.offsetX)} ${formatDimension(layer.offsetY)}`],
1890
- ["blur", formatDimension(layer.blur)],
1891
- ["spread", formatDimension(layer.spread)],
1892
- ["color", formatSubColor(layer.color, format)]
1893
- ];
1894
- if (layer.inset) entries.push(["inset", String(layer.inset)]);
1895
- return entries.flatMap(([k, v]) => [/* @__PURE__ */ jsx("span", {
1896
- className: "sb-shadow-preview__breakdown-key",
1897
- children: k
1898
- }, `k-${k}`), /* @__PURE__ */ jsx("span", { children: v }, `v-${k}`)]);
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];
1899
1616
  }
1900
- function Layer({ layer, index, total, colorFormat }) {
1901
- return /* @__PURE__ */ jsxs("div", {
1902
- className: "sb-shadow-preview__layer",
1903
- children: [/* @__PURE__ */ jsxs("div", {
1904
- className: "sb-shadow-preview__layer-header",
1905
- children: [
1906
- "layer ",
1907
- index + 1,
1908
- " of ",
1909
- total
1910
- ]
1911
- }), /* @__PURE__ */ jsx("div", {
1912
- className: cx("sb-shadow-preview__breakdown", "sb-shadow-preview__layer-breakdown"),
1913
- children: renderLayer(layer, colorFormat)
1914
- })]
1915
- });
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"])}`;
1916
1629
  }
1917
1630
  //#endregion
1918
- //#region src/StrokeStyleSample.tsx
1919
- const STRING_STYLES = new Set([
1631
+ //#region src/internal/prefers-reduced-motion.ts
1632
+ /**
1633
+ * Reactive `prefers-reduced-motion: reduce` detector. Returns the current
1634
+ * match and updates if the user toggles the OS-level preference.
1635
+ */
1636
+ function usePrefersReducedMotion() {
1637
+ const [reduced, setReduced] = useState(false);
1638
+ useEffect(() => {
1639
+ if (typeof window === "undefined") return;
1640
+ const query = window.matchMedia("(prefers-reduced-motion: reduce)");
1641
+ setReduced(query.matches);
1642
+ const onChange = (e) => setReduced(e.matches);
1643
+ query.addEventListener("change", onChange);
1644
+ return () => query.removeEventListener("change", onChange);
1645
+ }, []);
1646
+ return reduced;
1647
+ }
1648
+ //#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([
1920
1652
  "solid",
1921
1653
  "dashed",
1922
1654
  "dotted",
@@ -1926,1022 +1658,1608 @@ const STRING_STYLES = new Set([
1926
1658
  "outset",
1927
1659
  "inset"
1928
1660
  ]);
1929
- function extractCssStyle(value) {
1930
- if (typeof value === "string" && STRING_STYLES.has(value)) return value;
1931
- return null;
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
+ });
1932
1669
  }
1933
- function StrokeStyleSample({ filter, caption, sortBy = "path", sortDir = "asc" }) {
1934
- const { resolved, activeTheme, cssVarPrefix } = useProject();
1935
- const rows = useMemo(() => {
1936
- return sortTokens(Object.entries(resolved).filter(([path, token]) => {
1937
- if (token.$type !== "strokeStyle") return false;
1938
- return globMatch(path, filter);
1939
- }), {
1940
- by: sortBy,
1941
- dir: sortDir
1942
- }).map(([path, token]) => ({
1943
- path,
1944
- cssVar: makeCssVar(path, cssVarPrefix),
1945
- displayValue: formatTokenValue(token.$value, token.$type, "raw"),
1946
- cssStyle: extractCssStyle(token.$value)
1947
- }));
1948
- }, [
1949
- resolved,
1950
- filter,
1951
- cssVarPrefix,
1952
- sortBy,
1953
- sortDir
1954
- ]);
1955
- const captionText = caption ?? `${rows.length} strokeStyle token${rows.length === 1 ? "" : "s"}${filter && filter !== "strokeStyle" ? ` matching \`${filter}\`` : ""} · ${activeTheme}`;
1956
- if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
1957
- ...themeAttrs(cssVarPrefix, activeTheme),
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",
1958
1698
  children: /* @__PURE__ */ jsx("div", {
1959
- className: "sb-block__empty",
1960
- children: "No strokeStyle tokens match this filter."
1699
+ className: "sb-token-detail__dimension-bar",
1700
+ style: { width: cssVar },
1701
+ "aria-hidden": true
1961
1702
  })
1962
1703
  });
1963
- return /* @__PURE__ */ jsxs("div", {
1964
- ...themeAttrs(cssVarPrefix, activeTheme),
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,
1965
1725
  children: [/* @__PURE__ */ jsx("div", {
1966
- className: "sb-block__caption",
1967
- children: captionText
1968
- }), rows.map((row) => /* @__PURE__ */ jsxs("div", {
1969
- className: "sb-stroke-style-sample__row",
1970
- children: [
1971
- /* @__PURE__ */ jsxs("div", {
1972
- className: "sb-stroke-style-sample__meta",
1973
- children: [/* @__PURE__ */ jsx("span", {
1974
- className: "sb-stroke-style-sample__path",
1975
- children: row.path
1976
- }), /* @__PURE__ */ jsx("span", {
1977
- className: "sb-stroke-style-sample__value",
1978
- children: row.displayValue
1979
- })]
1980
- }),
1981
- row.cssStyle ? /* @__PURE__ */ jsx("div", {
1982
- className: "sb-stroke-style-sample__line",
1983
- style: { borderTopStyle: row.cssStyle },
1984
- "aria-hidden": true
1985
- }) : /* @__PURE__ */ jsx("span", {
1986
- className: "sb-stroke-style-sample__object-fallback",
1987
- children: "Object-form (dashArray + lineCap) — no pure CSS `border-style` equivalent."
1988
- }),
1989
- /* @__PURE__ */ jsx("span", {
1990
- className: "sb-stroke-style-sample__css-var",
1991
- children: row.cssVar
1992
- })
1993
- ]
1994
- }, row.path))]
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
+ })]
1995
1732
  });
1733
+ return null;
1996
1734
  }
1997
- //#endregion
1998
- //#region src/token-detail/internal.ts
1999
- function useTokenDetailData(path) {
2000
- const { activeTheme, activeAxes, axes, themes, themesResolved, resolved, cssVarPrefix } = useProject();
2001
- const typedResolved = resolved;
2002
- return {
2003
- token: typedResolved[path],
2004
- cssVar: makeCssVar(path, cssVarPrefix),
2005
- activeTheme,
2006
- activeAxes,
2007
- axes,
2008
- themes,
2009
- themesResolved,
2010
- resolved: typedResolved,
2011
- cssVarPrefix
2012
- };
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
+ });
1765
+ }
1766
+ return /* @__PURE__ */ jsx("div", {
1767
+ className: "sb-token-detail__stroke-style-fallback",
1768
+ children: "strokeStyle value could not be previewed."
1769
+ });
2013
1770
  }
2014
- //#endregion
2015
- //#region src/token-detail/AliasChain.tsx
2016
- function AliasChain({ path }) {
2017
- const { token } = useTokenDetailData(path);
2018
- const chain = useMemo(() => {
2019
- if (!token) return [];
2020
- if (Array.isArray(token.aliasChain) && token.aliasChain.length > 0) return [path, ...token.aliasChain];
2021
- if (typeof token.aliasOf === "string") return [path, token.aliasOf];
2022
- return [path];
2023
- }, [token, path]);
2024
- if (chain.length <= 1) return null;
2025
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2026
- className: "sb-token-detail__section-header",
2027
- children: "Alias chain"
2028
- }), /* @__PURE__ */ jsx("div", {
2029
- className: "sb-token-detail__chain",
2030
- children: chain.map((step, i) => /* @__PURE__ */ jsxs("span", {
2031
- className: "sb-token-detail__chain",
2032
- children: [/* @__PURE__ */ jsx("span", {
2033
- className: "sb-token-detail__chain-node",
2034
- children: step
2035
- }), i < chain.length - 1 && /* @__PURE__ */ jsx("span", {
2036
- className: "sb-token-detail__arrow",
2037
- children: "→"
2038
- })]
2039
- }, step))
2040
- })] });
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);
1782
+ }
1783
+ }
1784
+ return out;
1785
+ }
1786
+ function TransitionSample({ transition }) {
1787
+ const reduced = usePrefersReducedMotion();
1788
+ const [phase, setPhase] = useState(0);
1789
+ useEffect(() => {
1790
+ if (reduced) return;
1791
+ const id = requestAnimationFrame(() => setPhase(1));
1792
+ const loop = window.setInterval(() => {
1793
+ setPhase((p) => p === 0 ? 1 : 0);
1794
+ }, 1200);
1795
+ return () => {
1796
+ cancelAnimationFrame(id);
1797
+ window.clearInterval(loop);
1798
+ };
1799
+ }, [reduced]);
1800
+ if (reduced) return /* @__PURE__ */ jsx("div", {
1801
+ className: "sb-token-detail__reduced-motion",
1802
+ children: "Animation suppressed by `prefers-reduced-motion: reduce`."
1803
+ });
1804
+ return /* @__PURE__ */ jsx("div", {
1805
+ className: "sb-token-detail__motion-track",
1806
+ children: /* @__PURE__ */ jsx("div", {
1807
+ className: "sb-token-detail__motion-ball",
1808
+ style: {
1809
+ left: phase === 1 ? "calc(100% - 28px)" : "4px",
1810
+ transition
1811
+ },
1812
+ "aria-hidden": true
1813
+ })
1814
+ });
2041
1815
  }
2042
1816
  //#endregion
2043
- //#region src/token-detail/AliasedBy.tsx
2044
- const ALIASED_BY_DEPTH_CAP = 6;
2045
- const GROUP_RANK = {
2046
- ref: 0,
2047
- sys: 1
2048
- };
2049
- function AliasedBy({ path }) {
2050
- const { resolved } = useTokenDetailData(path);
2051
- const tree = useMemo(() => buildAliasedByTree(path, resolved), [path, resolved]);
2052
- const truncated = useMemo(() => treeHasTruncation(tree), [tree]);
2053
- if (tree.length === 0) return null;
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(", ");
2054
1822
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2055
1823
  /* @__PURE__ */ jsx("div", {
2056
1824
  className: "sb-token-detail__section-header",
2057
- children: "Aliased by"
1825
+ children: "Consumer output"
2058
1826
  }),
2059
- /* @__PURE__ */ jsx("ul", {
2060
- className: "sb-token-detail__aliased-by-list",
2061
- children: tree.map((node) => /* @__PURE__ */ jsx(AliasedByRow, {
2062
- node,
2063
- depth: 0
2064
- }, node.path))
1827
+ tupleLabel && /* @__PURE__ */ jsxs("div", {
1828
+ className: "sb-token-detail__tuple-indicator",
1829
+ children: ["Active tuple: ", /* @__PURE__ */ jsx("strong", { children: tupleLabel })]
2065
1830
  }),
2066
- truncated && /* @__PURE__ */ jsxs("div", {
2067
- className: "sb-token-detail__aliased-by-truncated",
2068
- children: [
2069
- "Further descendants truncated at depth ",
2070
- ALIASED_BY_DEPTH_CAP,
2071
- "."
2072
- ]
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"
2073
1840
  })
2074
1841
  ] });
2075
1842
  }
2076
- function AliasedByRow({ node, depth }) {
2077
- return /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("div", {
2078
- className: "sb-token-detail__aliased-by-row",
2079
- style: { paddingLeft: depth * 16 },
2080
- children: /* @__PURE__ */ jsx("span", {
2081
- className: "sb-token-detail__chain-node",
2082
- children: node.path
2083
- })
2084
- }), node.children.length > 0 && /* @__PURE__ */ jsx("ul", {
2085
- className: "sb-token-detail__aliased-by-list",
2086
- children: node.children.map((child) => /* @__PURE__ */ jsx(AliasedByRow, {
2087
- node: child,
2088
- depth: depth + 1
2089
- }, child.path))
2090
- })] });
2091
- }
2092
- function buildAliasedByTree(rootPath, resolved) {
2093
- const direct = resolved[rootPath]?.aliasedBy;
2094
- if (!direct || direct.length === 0) return [];
2095
- const visited = new Set([rootPath]);
2096
- return sortPaths(direct).map((p) => walk(p, resolved, visited, 1));
2097
- }
2098
- function walk(path, resolved, visited, depth) {
2099
- if (visited.has(path)) return {
2100
- path,
2101
- children: []
2102
- };
2103
- visited.add(path);
2104
- const parents = resolved[path]?.aliasedBy;
2105
- if (!parents || parents.length === 0) return {
2106
- path,
2107
- children: []
2108
- };
2109
- if (depth >= ALIASED_BY_DEPTH_CAP) return {
2110
- path,
2111
- children: [],
2112
- truncated: true
2113
- };
2114
- return {
2115
- path,
2116
- children: sortPaths(parents).map((p) => walk(p, resolved, visited, depth + 1))
2117
- };
2118
- }
2119
- function sortPaths(paths) {
2120
- return paths.toSorted((a, b) => {
2121
- const ra = GROUP_RANK[a.split(".")[0] ?? ""] ?? 2;
2122
- const rb = GROUP_RANK[b.split(".")[0] ?? ""] ?? 2;
2123
- return ra !== rb ? ra - rb : a.localeCompare(b, void 0, { numeric: true });
1843
+ function OutputRow({ label, value, testId }) {
1844
+ return /* @__PURE__ */ jsxs("div", {
1845
+ className: "sb-token-detail__consumer-row",
1846
+ children: [
1847
+ /* @__PURE__ */ jsx("span", {
1848
+ className: "sb-token-detail__consumer-row-label",
1849
+ children: label
1850
+ }),
1851
+ /* @__PURE__ */ jsx("code", {
1852
+ className: "sb-token-detail__consumer-row-value",
1853
+ "data-testid": testId,
1854
+ children: value
1855
+ }),
1856
+ /* @__PURE__ */ jsx(CopyButton, {
1857
+ text: value,
1858
+ testId: `${testId}-copy`
1859
+ })
1860
+ ]
2124
1861
  });
2125
1862
  }
2126
- function treeHasTruncation(nodes) {
2127
- for (const n of nodes) {
2128
- if (n.truncated) return true;
2129
- if (treeHasTruncation(n.children)) return true;
2130
- }
2131
- return false;
2132
- }
2133
- //#endregion
2134
- //#region src/token-detail/AxisVariance.tsx
2135
- function AxisVariance({ path }) {
2136
- const { token, cssVar, axes, themes, themesResolved, activeAxes, cssVarPrefix } = useTokenDetailData(path);
2137
- const colorFormat = useColorFormat();
2138
- const tokenType = token?.$type;
2139
- const isColor = tokenType === "color";
2140
- const formatFn = (t) => valueFor(t, tokenType, colorFormat);
2141
- const variance = useMemo(() => {
2142
- const result = analyzeAxisVariance(path, axes, themes, themesResolved);
2143
- return {
2144
- kind: result.kind === "constant" ? "constant" : result.kind === "single" ? "one-axis" : "multi-axis",
2145
- varyingAxes: result.varyingAxes
2146
- };
2147
- }, [
2148
- path,
2149
- axes,
2150
- themes,
2151
- themesResolved
2152
- ]);
2153
- if (themes.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
2154
- if (variance.kind === "constant") {
2155
- const anyTheme = themes[0];
2156
- const value = anyTheme ? formatFn(themesResolved[anyTheme.name]?.[path]) : "—";
2157
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2158
- className: "sb-token-detail__section-header",
2159
- children: "Values across axes"
2160
- }), /* @__PURE__ */ jsx("table", {
2161
- className: "sb-token-detail__theme-table",
2162
- "data-testid": "token-detail-values",
2163
- children: /* @__PURE__ */ jsx("tbody", { children: /* @__PURE__ */ jsx("tr", {
2164
- className: "sb-token-detail__theme-row",
2165
- children: /* @__PURE__ */ jsxs("td", {
2166
- className: "sb-token-detail__theme-cell",
2167
- "data-testid": "token-detail-constant",
2168
- children: [
2169
- isColor && /* @__PURE__ */ jsx("span", {
2170
- className: "sb-token-detail__swatch",
2171
- style: { background: cssVar },
2172
- "aria-hidden": true
2173
- }),
2174
- value,
2175
- /* @__PURE__ */ jsxs("span", {
2176
- style: {
2177
- opacity: .6,
2178
- marginLeft: 8
2179
- },
2180
- children: [
2181
- "same across all ",
2182
- themes.length,
2183
- " tuples"
2184
- ]
2185
- })
2186
- ]
2187
- })
2188
- }) })
2189
- })] });
2190
- }
2191
- if (variance.kind === "one-axis") {
2192
- const axisName = variance.varyingAxes[0];
2193
- if (!axisName) return /* @__PURE__ */ jsx(Fragment, {});
2194
- const axis = axes.find((a) => a.name === axisName);
2195
- if (!axis) return /* @__PURE__ */ jsx(Fragment, {});
2196
- const contextValues = axis.contexts.map((ctx) => {
2197
- const target = {
2198
- ...activeAxes,
2199
- [axisName]: ctx
2200
- };
2201
- const name = themes.find((t) => {
2202
- const input = t.input;
2203
- return Object.keys(input).every((k) => input[k] === target[k]);
2204
- })?.name ?? "";
2205
- return {
2206
- ctx,
2207
- themeName: name,
2208
- value: name ? formatFn(themesResolved[name]?.[path]) : "—"
2209
- };
2210
- });
2211
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
2212
- className: "sb-token-detail__section-header",
2213
- children: ["Varies with ", axisName]
2214
- }), /* @__PURE__ */ jsx("table", {
2215
- className: "sb-token-detail__theme-table",
2216
- "data-testid": "token-detail-values",
2217
- children: /* @__PURE__ */ jsx("tbody", { children: contextValues.map((row) => /* @__PURE__ */ jsxs("tr", {
2218
- className: "sb-token-detail__theme-row",
2219
- "data-axis": axisName,
2220
- "data-context": row.ctx,
2221
- children: [/* @__PURE__ */ jsx("td", {
2222
- className: "sb-token-detail__theme-cell",
2223
- style: { width: "30%" },
2224
- children: row.ctx
2225
- }), /* @__PURE__ */ jsxs("td", {
2226
- className: "sb-token-detail__theme-cell",
2227
- children: [isColor && row.themeName && /* @__PURE__ */ jsx("span", {
2228
- className: "sb-token-detail__swatch",
2229
- style: { background: cssVar },
2230
- [dataAttr(cssVarPrefix, "theme")]: row.themeName,
2231
- "aria-hidden": true
2232
- }), row.value]
2233
- })]
2234
- }, row.ctx)) })
2235
- })] });
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"
1877
+ });
1878
+ }
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;
2236
1886
  }
2237
- 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);
2238
- if (!rowAxis || !colAxis) return /* @__PURE__ */ jsx(Fragment, {});
1887
+ }
1888
+ //#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
+ ]
1901
+ });
2239
1902
  return /* @__PURE__ */ jsxs(Fragment, { children: [
2240
- /* @__PURE__ */ jsxs("div", {
2241
- className: "sb-token-detail__section-header",
2242
- children: ["Varies with ", variance.varyingAxes.join(" × ")]
1903
+ /* @__PURE__ */ jsx("h3", {
1904
+ className: "sb-token-detail__heading",
1905
+ children: heading ?? path
2243
1906
  }),
2244
- /* @__PURE__ */ jsxs("table", {
2245
- className: "sb-token-detail__theme-table",
2246
- "data-testid": "token-detail-values",
2247
- children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
2248
- className: "sb-token-detail__theme-row",
2249
- children: [/* @__PURE__ */ jsxs("th", {
2250
- className: "sb-token-detail__theme-cell",
2251
- style: {
2252
- textAlign: "left",
2253
- opacity: .7
2254
- },
2255
- children: [
2256
- rowAxis.name,
2257
- " \\ ",
2258
- colAxis.name
2259
- ]
2260
- }), colAxis.contexts.map((col) => /* @__PURE__ */ jsx("th", {
2261
- className: "sb-token-detail__theme-cell",
2262
- style: {
2263
- textAlign: "left",
2264
- opacity: .7
2265
- },
2266
- children: col
2267
- }, col))]
2268
- }) }), /* @__PURE__ */ jsx("tbody", { children: rowAxis.contexts.map((row) => /* @__PURE__ */ jsxs("tr", {
2269
- className: "sb-token-detail__theme-row",
2270
- children: [/* @__PURE__ */ jsx("td", {
2271
- className: "sb-token-detail__theme-cell",
2272
- children: row
2273
- }), colAxis.contexts.map((col) => {
2274
- const name = tupleName(themes, {
2275
- ...activeAxes,
2276
- [rowAxis.name]: row,
2277
- [colAxis.name]: col
2278
- });
2279
- const value = name ? formatFn(themesResolved[name]?.[path]) : "—";
2280
- return /* @__PURE__ */ jsxs("td", {
2281
- className: "sb-token-detail__theme-cell",
2282
- "data-row": row,
2283
- "data-col": col,
2284
- children: [isColor && name && /* @__PURE__ */ jsx("span", {
2285
- className: "sb-token-detail__swatch",
2286
- style: { background: cssVar },
2287
- [dataAttr(cssVarPrefix, "theme")]: name,
2288
- "aria-hidden": true
2289
- }), value]
2290
- }, col);
2291
- })]
2292
- }, row)) })]
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 })]
2293
1913
  }),
2294
- extra.length > 0 && /* @__PURE__ */ jsxs("div", {
2295
- className: "sb-token-detail__aliased-by-truncated",
2296
- style: { marginTop: 6 },
2297
- children: [
2298
- "Values also vary with ",
2299
- extra.map((a) => a.name).join(", "),
2300
- "; matrix shows the slice for the active selection."
2301
- ]
1914
+ token.$description && /* @__PURE__ */ jsx("p", {
1915
+ className: "sb-token-detail__description",
1916
+ children: token.$description
2302
1917
  })
2303
1918
  ] });
2304
1919
  }
2305
- function valueFor(token, $type, format) {
2306
- if (!token) return "—";
2307
- return formatTokenValue(token.$value, $type, format);
2308
- }
2309
- function tupleName(themes, tuple) {
2310
- return themes.find((t) => {
2311
- const input = t.input;
2312
- return Object.keys(input).every((k) => input[k] === tuple[k]);
2313
- })?.name;
1920
+ //#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
+ })] });
2314
1939
  }
2315
1940
  //#endregion
2316
- //#region src/token-detail/CompositeBreakdown.tsx
2317
- function CompositeBreakdown({ path }) {
2318
- const { token, resolved } = useTokenDetailData(path);
1941
+ //#region src/TokenDetail.tsx
1942
+ function TokenDetail({ path, heading }) {
1943
+ const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
2319
1944
  const colorFormat = useColorFormat();
2320
- if (!token) return null;
2321
- return /* @__PURE__ */ jsx(CompositeBreakdownContent, {
2322
- type: token.$type,
2323
- rawValue: token.$value,
2324
- partialAliasOf: token.partialAliasOf,
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",
1951
+ children: [
1952
+ "Token ",
1953
+ /* @__PURE__ */ jsx("code", { children: path }),
1954
+ " not found in theme ",
1955
+ /* @__PURE__ */ jsx("strong", { children: activeTheme }),
1956
+ "."
1957
+ ]
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
+ ]
2005
+ });
2006
+ }
2007
+ //#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
+ });
2038
+ }
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]);
2046
+ const rows = useMemo(() => {
2047
+ return sortTokens(Object.entries(resolved).filter(([path, token]) => {
2048
+ if (token.$type !== "color") return false;
2049
+ return globMatch(path, filter);
2050
+ }), {
2051
+ by: sortBy,
2052
+ 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
+ });
2070
+ }, [
2325
2071
  resolved,
2326
- colorFormat
2072
+ filter,
2073
+ cssVarPrefix,
2074
+ 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
2085
+ ]);
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}`;
2092
+ if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
2093
+ ...themeAttrs(cssVarPrefix, activeTheme),
2094
+ children: /* @__PURE__ */ jsx("div", {
2095
+ className: "sb-block__empty",
2096
+ children: "No color tokens match this filter."
2097
+ })
2327
2098
  });
2328
- }
2329
- function CompositeBreakdownContent({ type, rawValue, partialAliasOf, resolved, colorFormat }) {
2330
- if (!rawValue || typeof rawValue !== "object") return null;
2331
- const objectAliases = pickObjectAliases(partialAliasOf);
2332
- const arrayAliases = pickArrayAliases(partialAliasOf);
2333
- const aliasFor = (key) => subValueChain(objectAliases?.[key], resolved);
2334
- if (type === "typography") {
2335
- const v = rawValue;
2336
- return renderKeyValueList([
2337
- [
2338
- "fontFamily",
2339
- formatFontFamily(v["fontFamily"]),
2340
- aliasFor("fontFamily")
2341
- ],
2342
- [
2343
- "fontSize",
2344
- formatDimensionValue(v["fontSize"]),
2345
- aliasFor("fontSize")
2346
- ],
2347
- [
2348
- "fontWeight",
2349
- formatPrimitive(v["fontWeight"]),
2350
- aliasFor("fontWeight")
2351
- ],
2352
- [
2353
- "lineHeight",
2354
- formatPrimitive(v["lineHeight"]),
2355
- aliasFor("lineHeight")
2356
- ],
2357
- [
2358
- "letterSpacing",
2359
- formatDimensionValue(v["letterSpacing"]),
2360
- aliasFor("letterSpacing")
2361
- ]
2362
- ]);
2363
- }
2364
- if (type === "border") {
2365
- const v = rawValue;
2366
- return renderKeyValueList([
2367
- [
2368
- "color",
2369
- formatColorSubValue(v["color"], colorFormat),
2370
- aliasFor("color")
2371
- ],
2372
- [
2373
- "width",
2374
- formatDimensionValue(v["width"]),
2375
- aliasFor("width")
2376
- ],
2377
- [
2378
- "style",
2379
- formatPrimitive(v["style"]),
2380
- aliasFor("style")
2381
- ]
2382
- ]);
2383
- }
2384
- if (type === "transition") {
2385
- const v = rawValue;
2386
- return renderKeyValueList([
2387
- [
2388
- "duration",
2389
- formatDimensionValue(v["duration"]),
2390
- aliasFor("duration")
2391
- ],
2392
- [
2393
- "timingFunction",
2394
- formatPrimitive(v["timingFunction"]),
2395
- aliasFor("timingFunction")
2396
- ],
2397
- [
2398
- "delay",
2399
- formatDimensionValue(v["delay"]),
2400
- aliasFor("delay")
2401
- ]
2402
- ]);
2403
- }
2404
- if (type === "shadow") {
2405
- const layers = Array.isArray(rawValue) ? rawValue : [rawValue];
2406
- const multi = layers.length > 1;
2407
- const layerAliasFor = (i, key) => subValueChain(arrayAliases?.[i]?.[key], resolved);
2408
- return /* @__PURE__ */ jsx("div", {
2409
- className: "sb-token-detail__breakdown-section",
2410
- children: layers.map((layer, i) => {
2411
- const v = layer;
2412
- return /* @__PURE__ */ jsxs("div", {
2413
- style: { display: "contents" },
2414
- children: [
2415
- multi && /* @__PURE__ */ jsxs("div", {
2416
- className: "sb-token-detail__breakdown-layer-header",
2417
- children: ["Layer ", i + 1]
2099
+ return /* @__PURE__ */ jsxs("div", {
2100
+ ...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
+ })
2418
2128
  }),
2419
- /* @__PURE__ */ jsx(KeyValueRow, {
2420
- label: "color",
2421
- value: formatColorSubValue(v["color"], colorFormat),
2422
- alias: layerAliasFor(i, "color")
2129
+ /* @__PURE__ */ jsx("th", {
2130
+ className: "sb-color-table__th sb-color-table__th--path",
2131
+ children: "Name"
2423
2132
  }),
2424
- /* @__PURE__ */ jsx(KeyValueRow, {
2425
- label: "offsetX",
2426
- value: formatDimensionValue(v["offsetX"]),
2427
- alias: layerAliasFor(i, "offsetX")
2133
+ /* @__PURE__ */ jsx("th", {
2134
+ className: "sb-color-table__th",
2135
+ children: "HEX"
2428
2136
  }),
2429
- /* @__PURE__ */ jsx(KeyValueRow, {
2430
- label: "offsetY",
2431
- value: formatDimensionValue(v["offsetY"]),
2432
- alias: layerAliasFor(i, "offsetY")
2137
+ /* @__PURE__ */ jsx("th", {
2138
+ className: "sb-color-table__th",
2139
+ children: "HSL"
2433
2140
  }),
2434
- /* @__PURE__ */ jsx(KeyValueRow, {
2435
- label: "blur",
2436
- value: formatDimensionValue(v["blur"]),
2437
- alias: layerAliasFor(i, "blur")
2141
+ /* @__PURE__ */ jsx("th", {
2142
+ className: "sb-color-table__th",
2143
+ children: "OKLCH"
2438
2144
  }),
2439
- /* @__PURE__ */ jsx(KeyValueRow, {
2440
- label: "spread",
2441
- value: formatDimensionValue(v["spread"]),
2442
- alias: layerAliasFor(i, "spread")
2145
+ /* @__PURE__ */ jsx("th", {
2146
+ className: "sb-color-table__th",
2147
+ children: "CSS var"
2443
2148
  }),
2444
- "inset" in v && /* @__PURE__ */ jsx(KeyValueRow, {
2445
- label: "inset",
2446
- value: formatPrimitive(v["inset"]),
2447
- alias: void 0
2149
+ /* @__PURE__ */ jsx("th", {
2150
+ className: "sb-color-table__th",
2151
+ children: "Alias"
2448
2152
  })
2449
- ]
2450
- }, shadowLayerKey(v, i));
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"
2260
+ })
2451
2261
  })
2262
+ ]
2263
+ });
2264
+ }
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
2452
2278
  });
2453
2279
  }
2454
- if (type === "gradient") {
2455
- const stops = Array.isArray(rawValue) ? rawValue : [];
2456
- if (stops.length === 0) return null;
2457
- const stopAliasFor = (i) => subValueChain(arrayAliases?.[i]?.["color"], resolved);
2458
- return /* @__PURE__ */ jsx("div", {
2459
- className: "sb-token-detail__breakdown-section",
2460
- children: stops.map((stop, i) => {
2461
- const v = stop;
2462
- return /* @__PURE__ */ jsx(KeyValueRow, {
2463
- label: `${((typeof v["position"] === "number" ? v["position"] : 0) * 100).toFixed(0)}%`,
2464
- value: formatColorSubValue(v["color"], colorFormat),
2465
- alias: stopAliasFor(i)
2466
- }, gradientStopKey(v, i));
2467
- })
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
+ //#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
2311
+ };
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
+ }
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)))
2367
+ })]
2368
+ })
2369
+ });
2370
+ }
2371
+ //#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
+ }
2393
+ };
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
+ }
2410
+ }
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
2468
2439
  });
2469
2440
  }
2470
- return null;
2471
2441
  }
2472
- function renderKeyValueList(rows) {
2473
- return /* @__PURE__ */ jsx("div", {
2474
- className: "sb-token-detail__breakdown-section",
2475
- children: rows.filter(([, v, alias]) => v !== null || alias && alias.length > 0).map(([k, v, alias]) => /* @__PURE__ */ jsx(KeyValueRow, {
2476
- label: k,
2477
- value: v ?? "",
2478
- alias
2479
- }, k))
2442
+ //#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;
2454
+ }
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);
2467
+ 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
2473
+ };
2474
+ });
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
+ })
2526
+ ]
2527
+ }, row.path))]
2480
2528
  });
2481
2529
  }
2482
- function KeyValueRow({ label, value, alias }) {
2483
- const hasAlias = alias && alias.length > 0;
2484
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2485
- className: "sb-token-detail__breakdown-key",
2486
- children: label
2487
- }), /* @__PURE__ */ jsxs("span", {
2488
- className: "sb-token-detail__breakdown-value",
2489
- children: [/* @__PURE__ */ jsx("span", { children: value ?? "—" }), hasAlias && /* @__PURE__ */ jsx("span", {
2490
- className: "sb-token-detail__breakdown-alias",
2491
- "data-testid": "breakdown-alias",
2492
- children: alias.map((p, i) => /* @__PURE__ */ jsxs("span", {
2493
- className: "sb-token-detail__breakdown-alias-step",
2494
- children: [i > 0 && /* @__PURE__ */ jsx("span", {
2495
- className: "sb-token-detail__arrow",
2496
- children: "→"
2497
- }), /* @__PURE__ */ jsx("span", {
2498
- className: "sb-token-detail__chain-node",
2499
- children: p
2500
- })]
2501
- }, p))
2502
- })]
2503
- })] });
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 "";
2536
+ }
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
+ });
2504
2596
  }
2505
- function formatPrimitive(v) {
2506
- if (v == null) return null;
2507
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return String(v);
2508
- return JSON.stringify(v);
2597
+ //#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;
2509
2606
  }
2510
- function formatFontFamily(v) {
2511
- if (v == null) return null;
2512
- if (typeof v === "string") return v;
2513
- if (Array.isArray(v)) return v.map(String).join(", ");
2514
- return JSON.stringify(v);
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
+ }, [
2623
+ 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
+ })
2636
+ });
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
+ })
2664
+ ]
2665
+ }, row.path))]
2666
+ });
2515
2667
  }
2516
- function formatDimensionValue(v) {
2517
- if (v == null) return null;
2518
- if (typeof v === "string" || typeof v === "number") return String(v);
2519
- if (typeof v === "object") {
2520
- const d = v;
2521
- if (typeof d.value === "number" && typeof d.unit === "string") return `${d.value}${d.unit}`;
2522
- }
2523
- return JSON.stringify(v);
2668
+ //#endregion
2669
+ //#region src/GradientPalette.tsx
2670
+ function asStops(raw) {
2671
+ if (!Array.isArray(raw)) return [];
2672
+ return raw;
2524
2673
  }
2525
- /**
2526
- * Route sub-value colors through `formatColor` so they honor the active
2527
- * color-format dropdown, just like the standalone `<ColorPalette />` and
2528
- * `<TokenDetail />` top-line do. Returns `null` for a missing field so
2529
- * the key/value row drops out entirely.
2530
- */
2531
- function formatColorSubValue(v, format) {
2532
- if (v == null) return null;
2533
- return formatColor(v, format).value;
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})`;
2534
2682
  }
2535
- function pickObjectAliases(v) {
2536
- if (!v || typeof v !== "object" || Array.isArray(v)) return void 0;
2537
- return v;
2683
+ function stopKey(path, stop, fallback) {
2684
+ return `${path}|${stop.position ?? fallback}|${stopCssColor(stop)}`;
2538
2685
  }
2539
- function pickArrayAliases(v) {
2540
- if (!Array.isArray(v)) return void 0;
2541
- return v;
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
+ });
2542
2764
  }
2543
- /**
2544
- * Walk the alias chain starting from an immediate sub-value alias target.
2545
- * `aliasTarget` is the path the sub-value directly references; the target
2546
- * token's own `aliasChain` continues the walk to the primitive.
2547
- */
2548
- function subValueChain(aliasTarget, resolved) {
2549
- if (!aliasTarget) return void 0;
2550
- const tail = (resolved?.[aliasTarget])?.aliasChain;
2551
- return tail && tail.length > 0 ? [aliasTarget, ...tail] : [aliasTarget];
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
+ }
2800
+ }
2801
+ return NaN;
2802
+ }
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;
2552
2806
  }
2553
- function shadowLayerKey(layer, fallback) {
2554
- return `shadow|${[
2555
- layer["color"],
2556
- layer["offsetX"],
2557
- layer["offsetY"],
2558
- layer["blur"],
2559
- layer["spread"],
2560
- layer["inset"]
2561
- ].map((p) => p === void 0 ? "" : JSON.stringify(p)).join("|")}|${fallback}`;
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;
2562
2819
  }
2563
- function gradientStopKey(stop, fallback) {
2564
- return `stop|${stop["position"] ?? fallback}|${JSON.stringify(stop["color"])}`;
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;
2565
2832
  }
2566
- //#endregion
2567
- //#region src/token-detail/CompositePreview.tsx
2568
- const PANGRAM = "Sphinx of black quartz, judge my vow.";
2569
- const STROKE_STYLE_STRINGS = new Set([
2570
- "solid",
2571
- "dashed",
2572
- "dotted",
2573
- "double",
2574
- "groove",
2575
- "ridge",
2576
- "outset",
2577
- "inset"
2578
- ]);
2579
- function CompositePreview({ path }) {
2580
- const { token, cssVar } = useTokenDetailData(path);
2833
+ function resolveMotionSpec(token, themeTokens) {
2581
2834
  if (!token) return null;
2582
- return /* @__PURE__ */ jsx(CompositePreviewContent, {
2583
- type: token.$type,
2584
- cssVar,
2585
- rawValue: token.$value
2586
- });
2587
- }
2588
- function CompositePreviewContent({ type, cssVar, rawValue }) {
2589
- if (type === "typography") {
2590
- const base = cssVar.replace(/^var\(/, "").replace(/\)$/, "");
2591
- return /* @__PURE__ */ jsx("div", {
2592
- className: "sb-token-detail__typography-sample",
2593
- style: {
2594
- fontFamily: `var(${base}-font-family)`,
2595
- fontSize: `var(${base}-font-size)`,
2596
- fontWeight: `var(${base}-font-weight)`,
2597
- lineHeight: `var(${base}-line-height)`,
2598
- letterSpacing: `var(${base}-letter-spacing)`
2599
- },
2600
- children: PANGRAM
2601
- });
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
+ };
2602
2842
  }
2603
- if (type === "shadow") return /* @__PURE__ */ jsx("div", {
2604
- className: "sb-token-detail__shadow-sample",
2605
- style: { boxShadow: cssVar },
2606
- "aria-hidden": true
2607
- });
2608
- if (type === "border") return /* @__PURE__ */ jsx("div", {
2609
- className: "sb-token-detail__border-sample",
2610
- style: { border: cssVar },
2611
- "aria-hidden": true
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`."
2612
2888
  });
2613
- if (type === "transition") return /* @__PURE__ */ jsx(TransitionSample, { transition: cssVar });
2614
- if (type === "dimension") return /* @__PURE__ */ jsx("div", {
2615
- className: "sb-token-detail__dimension-track",
2889
+ return /* @__PURE__ */ jsx("div", {
2890
+ style: styles.track,
2616
2891
  children: /* @__PURE__ */ jsx("div", {
2617
- className: "sb-token-detail__dimension-bar",
2618
- style: { width: cssVar },
2892
+ style: {
2893
+ ...styles.ball,
2894
+ left: phase === 1 ? "calc(100% - 32px)" : "4px",
2895
+ transition: `left ${scaledDuration}ms ${easing}`
2896
+ },
2619
2897
  "aria-hidden": true
2620
2898
  })
2621
2899
  });
2622
- if (type === "duration") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left ${cssVar} ease` });
2623
- if (type === "fontFamily") return /* @__PURE__ */ jsx("div", {
2624
- className: "sb-token-detail__font-family-sample",
2625
- style: { fontFamily: cssVar },
2626
- children: PANGRAM
2627
- });
2628
- if (type === "fontWeight") return /* @__PURE__ */ jsx("div", {
2629
- className: "sb-token-detail__font-weight-sample",
2630
- style: { fontWeight: cssVar },
2631
- children: "Aa"
2632
- });
2633
- if (type === "cubicBezier") return /* @__PURE__ */ jsx(TransitionSample, { transition: `left 800ms ${cssVar}` });
2634
- if (type === "gradient") return /* @__PURE__ */ jsx("div", {
2635
- className: "sb-token-detail__gradient-sample",
2636
- style: { background: `linear-gradient(to right, ${cssVar})` },
2637
- "aria-hidden": true
2638
- });
2639
- if (type === "strokeStyle") return /* @__PURE__ */ jsx(StrokeStylePreview, { value: rawValue });
2640
- if (type === "color") return /* @__PURE__ */ jsxs("div", {
2641
- className: "sb-token-detail__color-swatch-row",
2642
- "aria-hidden": true,
2643
- children: [/* @__PURE__ */ jsx("div", {
2644
- className: "sb-token-detail__color-swatch-light",
2645
- style: { background: cssVar }
2646
- }), /* @__PURE__ */ jsx("div", {
2647
- className: "sb-token-detail__color-swatch-dark",
2648
- style: { background: cssVar }
2649
- })]
2650
- });
2651
- return null;
2652
2900
  }
2653
- function StrokeStylePreview({ value }) {
2654
- if (typeof value === "string" && STROKE_STYLE_STRINGS.has(value)) return /* @__PURE__ */ jsx("div", {
2655
- className: "sb-token-detail__stroke-style-line",
2656
- style: { borderTopStyle: value },
2657
- "aria-hidden": true
2658
- });
2659
- if (value && typeof value === "object" && "dashArray" in value) {
2660
- const v = value;
2661
- const lengths = asDashLengths(v.dashArray);
2662
- if (lengths.length === 0) return /* @__PURE__ */ jsx("div", {
2663
- className: "sb-token-detail__stroke-style-fallback",
2664
- children: "Object-form strokeStyle with no resolvable dashArray."
2665
- });
2666
- const cap = typeof v.lineCap === "string" ? v.lineCap : "butt";
2667
- return /* @__PURE__ */ jsx("svg", {
2668
- className: "sb-token-detail__stroke-style-svg",
2669
- viewBox: "0 0 220 24",
2670
- preserveAspectRatio: "none",
2671
- "aria-hidden": true,
2672
- children: /* @__PURE__ */ jsx("line", {
2673
- x1: "4",
2674
- y1: "12",
2675
- x2: "216",
2676
- y2: "12",
2677
- stroke: "currentColor",
2678
- strokeWidth: "4",
2679
- strokeDasharray: lengths.join(" "),
2680
- strokeLinecap: cap
2681
- })
2682
- });
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}`;
2683
2914
  }
2684
- return /* @__PURE__ */ jsx("div", {
2685
- className: "sb-token-detail__stroke-style-fallback",
2686
- children: "strokeStyle value could not be previewed."
2687
- });
2688
2915
  }
2689
- function asDashLengths(raw) {
2690
- if (!Array.isArray(raw)) return [];
2691
- const out = [];
2692
- for (const entry of raw) {
2693
- if (typeof entry === "number") {
2694
- out.push(entry);
2695
- continue;
2696
- }
2697
- if (entry && typeof entry === "object") {
2698
- const e = entry;
2699
- if (typeof e.value === "number") out.push(e.value);
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
+ });
2700
2941
  }
2701
- }
2702
- return out;
2703
- }
2704
- function TransitionSample({ transition }) {
2705
- const reduced = usePrefersReducedMotion();
2706
- const [phase, setPhase] = useState(0);
2707
- useEffect(() => {
2708
- if (reduced) return;
2709
- const id = requestAnimationFrame(() => setPhase(1));
2710
- const loop = window.setInterval(() => {
2711
- setPhase((p) => p === 0 ? 1 : 0);
2712
- }, 1200);
2713
- return () => {
2714
- cancelAnimationFrame(id);
2715
- window.clearInterval(loop);
2716
- };
2717
- }, [reduced]);
2718
- if (reduced) return /* @__PURE__ */ jsx("div", {
2719
- className: "sb-token-detail__reduced-motion",
2720
- children: "Animation suppressed by `prefers-reduced-motion: reduce`."
2721
- });
2722
- return /* @__PURE__ */ jsx("div", {
2723
- className: "sb-token-detail__motion-track",
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 });
2945
+ });
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),
2724
2955
  children: /* @__PURE__ */ jsx("div", {
2725
- className: "sb-token-detail__motion-ball",
2726
- style: {
2727
- left: phase === 1 ? "calc(100% - 28px)" : "4px",
2728
- transition
2729
- },
2730
- "aria-hidden": true
2956
+ className: "sb-block__empty",
2957
+ children: "No motion tokens match this filter."
2731
2958
  })
2732
2959
  });
2733
- }
2734
- //#endregion
2735
- //#region src/token-detail/ConsumerOutput.tsx
2736
- function ConsumerOutput({ path }) {
2737
- const { token, cssVar, activeAxes } = useTokenDetailData(path);
2738
- if (!token) return null;
2739
- const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
2740
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2741
- /* @__PURE__ */ jsx("div", {
2742
- className: "sb-token-detail__section-header",
2743
- children: "Consumer output"
2744
- }),
2745
- tupleLabel && /* @__PURE__ */ jsxs("div", {
2746
- className: "sb-token-detail__tuple-indicator",
2747
- children: ["Active tuple: ", /* @__PURE__ */ jsx("strong", { children: tupleLabel })]
2748
- }),
2749
- /* @__PURE__ */ jsx(OutputRow, {
2750
- label: "Path",
2751
- value: path,
2752
- testId: "consumer-output-path"
2753
- }),
2754
- /* @__PURE__ */ jsx(OutputRow, {
2755
- label: "CSS",
2756
- value: cssVar,
2757
- testId: "consumer-output-css"
2758
- })
2759
- ] });
2760
- }
2761
- function OutputRow({ label, value, testId }) {
2762
2960
  return /* @__PURE__ */ jsxs("div", {
2763
- className: "sb-token-detail__consumer-row",
2961
+ ...themeAttrs(cssVarPrefix, activeTheme),
2764
2962
  children: [
2765
- /* @__PURE__ */ jsx("span", {
2766
- className: "sb-token-detail__consumer-row-label",
2767
- children: label
2963
+ /* @__PURE__ */ jsx("div", {
2964
+ className: "sb-block__caption",
2965
+ children: captionText
2768
2966
  }),
2769
- /* @__PURE__ */ jsx("code", {
2770
- className: "sb-token-detail__consumer-row-value",
2771
- "data-testid": testId,
2772
- children: value
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
+ ]
2773
2989
  }),
2774
- /* @__PURE__ */ jsx(CopyButton, {
2775
- text: value,
2776
- testId: `${testId}-copy`
2777
- })
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))
2778
3014
  ]
2779
3015
  });
2780
3016
  }
2781
- function CopyButton({ text, testId }) {
2782
- const [copied, setCopied] = useState(false);
2783
- return /* @__PURE__ */ jsx("button", {
2784
- type: "button",
2785
- className: "sb-token-detail__consumer-row-copy",
2786
- "data-testid": testId,
2787
- onClick: () => {
2788
- copyToClipboard(text).then((ok) => {
2789
- if (!ok) return;
2790
- setCopied(true);
2791
- window.setTimeout(() => setCopied(false), 1200);
2792
- });
2793
- },
2794
- children: copied ? "Copied" : "Copy"
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
2795
3032
  });
2796
3033
  }
2797
- async function copyToClipboard(text) {
2798
- if (typeof navigator === "undefined" || !navigator.clipboard) return false;
2799
- try {
2800
- await navigator.clipboard.writeText(text);
2801
- return true;
2802
- } catch {
2803
- return false;
2804
- }
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;
2805
3043
  }
2806
3044
  //#endregion
2807
- //#region src/token-detail/TokenHeader.tsx
2808
- function TokenHeader({ path, heading }) {
2809
- const { token, cssVar, activeTheme } = useTokenDetailData(path);
2810
- if (!token) return /* @__PURE__ */ jsxs("div", {
2811
- className: "sb-token-detail__missing",
2812
- children: [
2813
- "Token ",
2814
- /* @__PURE__ */ jsx("code", { children: path }),
2815
- " not found in theme ",
2816
- /* @__PURE__ */ jsx("strong", { children: activeTheme }),
2817
- "."
2818
- ]
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
+ },
3061
+ "aria-hidden": true
2819
3062
  });
2820
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2821
- /* @__PURE__ */ jsx("h3", {
2822
- className: "sb-token-detail__heading",
2823
- children: heading ?? path
2824
- }),
2825
- /* @__PURE__ */ jsxs("div", {
2826
- className: "sb-token-detail__subline",
2827
- children: [token.$type && /* @__PURE__ */ jsx("span", {
2828
- className: "sb-token-detail__type-pill",
2829
- children: token.$type
2830
- }), /* @__PURE__ */ jsx("span", { children: cssVar })]
2831
- }),
2832
- token.$description && /* @__PURE__ */ jsx("p", {
2833
- className: "sb-token-detail__description",
2834
- children: token.$description
2835
- })
2836
- ] });
2837
3063
  }
2838
3064
  //#endregion
2839
- //#region src/token-detail/TokenUsageSnippet.tsx
2840
- function TokenUsageSnippet({ path }) {
2841
- const { token, cssVar } = useTokenDetailData(path);
2842
- if (!token) return null;
2843
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2844
- className: "sb-token-detail__section-header",
2845
- children: "Usage"
2846
- }), /* @__PURE__ */ jsx("code", {
2847
- className: "sb-token-detail__snippet",
2848
- children: `color: ${cssVar};`
2849
- })] });
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}`;
3073
+ }
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 [];
2850
3084
  }
2851
- //#endregion
2852
- //#region src/TokenDetail.tsx
2853
- function TokenDetail({ path, heading }) {
2854
- const { token, cssVar, activeTheme, cssVarPrefix } = useTokenDetailData(path);
3085
+ function layerKey(path, layer, fallback) {
3086
+ return `${path}|${`${formatDimension(layer.offsetX)},${formatDimension(layer.offsetY)}`}|${formatDimension(layer.blur)}|${formatDimension(layer.spread)}|${fallback}`;
3087
+ }
3088
+ function ShadowPreview({ filter, caption, sortBy = "path", sortDir = "asc" }) {
3089
+ const { resolved, activeTheme, cssVarPrefix } = useProject();
2855
3090
  const colorFormat = useColorFormat();
2856
- const theme = themeAttrs(cssVarPrefix, activeTheme);
2857
- if (!token) return /* @__PURE__ */ jsx("div", {
2858
- ...theme,
2859
- className: cx(theme["className"], "sb-token-detail"),
2860
- children: /* @__PURE__ */ jsxs("div", {
2861
- className: "sb-token-detail__missing",
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),
3113
+ children: /* @__PURE__ */ jsx("div", {
3114
+ className: "sb-block__empty",
3115
+ children: "No shadow tokens match this filter."
3116
+ })
3117
+ });
3118
+ 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",
2862
3125
  children: [
2863
- "Token ",
2864
- /* @__PURE__ */ jsx("code", { children: path }),
2865
- " not found in theme ",
2866
- /* @__PURE__ */ jsx("strong", { children: activeTheme }),
2867
- "."
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
+ })
2868
3149
  ]
2869
- })
3150
+ }, row.path))]
2870
3151
  });
2871
- const isColor = token.$type === "color";
2872
- const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
2873
- const value = formatTokenValue(token.$value, token.$type, colorFormat);
2874
- const outOfGamut = gamut?.outOfGamut ?? false;
3152
+ }
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}`)]);
3166
+ }
3167
+ function Layer({ layer, index, total, colorFormat }) {
2875
3168
  return /* @__PURE__ */ jsxs("div", {
2876
- ...theme,
2877
- className: cx(theme["className"], "sb-token-detail"),
2878
- children: [
2879
- /* @__PURE__ */ jsx(TokenHeader, {
2880
- path,
2881
- ...heading !== void 0 && { heading }
2882
- }),
2883
- /* @__PURE__ */ jsxs("div", {
2884
- className: "sb-token-detail__section-header",
2885
- children: ["Resolved value · ", activeTheme]
2886
- }),
2887
- /* @__PURE__ */ jsx(CompositePreview, { path }),
2888
- /* @__PURE__ */ jsx(CompositeBreakdown, { path }),
2889
- /* @__PURE__ */ jsxs("div", {
2890
- className: "sb-token-detail__chain",
2891
- children: [
2892
- isColor && /* @__PURE__ */ jsx("span", {
2893
- className: "sb-token-detail__swatch",
2894
- style: { background: cssVar },
2895
- "aria-hidden": true
2896
- }),
2897
- /* @__PURE__ */ jsx("span", { children: value }),
2898
- outOfGamut && /* @__PURE__ */ jsx("span", {
2899
- title: "Out of sRGB gamut for this format",
2900
- "aria-label": "out of gamut",
2901
- style: { marginLeft: 6 },
2902
- children: "⚠"
2903
- })
2904
- ]
2905
- }),
2906
- /* @__PURE__ */ jsx(AliasChain, { path }),
2907
- /* @__PURE__ */ jsx(AliasedBy, { path }),
2908
- /* @__PURE__ */ jsx(TokenUsageSnippet, { path }),
2909
- /* @__PURE__ */ jsx(ConsumerOutput, { path }),
2910
- /* @__PURE__ */ jsx(AxisVariance, { path })
2911
- ]
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
+ })]
2912
3182
  });
2913
3183
  }
2914
3184
  //#endregion
2915
- //#region src/internal/DetailOverlay.tsx
2916
- function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
2917
- useEffect(() => {
2918
- const onKey = (e) => {
2919
- if (e.key === "Escape") onClose();
2920
- };
2921
- window.addEventListener("keydown", onKey);
2922
- return () => window.removeEventListener("keydown", onKey);
2923
- }, [onClose]);
2924
- return /* @__PURE__ */ jsx("div", {
2925
- className: "sb-detail-overlay__backdrop",
2926
- onClick: onClose,
2927
- role: "presentation",
2928
- "data-testid": testId,
2929
- children: /* @__PURE__ */ jsxs("div", {
2930
- className: "sb-detail-overlay__panel",
2931
- onClick: (e) => e.stopPropagation(),
2932
- role: "dialog",
2933
- "aria-modal": "true",
2934
- "aria-label": `Token detail for ${path}`,
2935
- children: [/* @__PURE__ */ jsx("button", {
2936
- type: "button",
2937
- className: "sb-detail-overlay__close",
2938
- onClick: onClose,
2939
- "aria-label": "Close",
2940
- "data-testid": `${testId}-close`,
2941
- children: "×"
2942
- }), /* @__PURE__ */ jsx(TokenDetail, { path })]
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;
3199
+ }
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."
2943
3228
  })
2944
3229
  });
3230
+ 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))]
3262
+ });
2945
3263
  }
2946
3264
  //#endregion
2947
3265
  //#region src/TokenNavigator.tsx
@@ -3443,6 +3761,17 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
3443
3761
  "aria-label": "out of gamut",
3444
3762
  className: "sb-token-table__gamut-warn",
3445
3763
  children: "⚠"
3764
+ }),
3765
+ /* @__PURE__ */ jsx("span", {
3766
+ className: "sb-token-table__copy-wrap",
3767
+ onClick: (e) => e.stopPropagation(),
3768
+ onKeyDown: (e) => e.stopPropagation(),
3769
+ role: "presentation",
3770
+ children: /* @__PURE__ */ jsx(CopyButton$1, {
3771
+ value: row.value,
3772
+ label: `Copy value ${row.value}`,
3773
+ className: "sb-token-table__copy"
3774
+ })
3446
3775
  })
3447
3776
  ]
3448
3777
  })
@@ -3550,6 +3879,6 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
3550
3879
  });
3551
3880
  }
3552
3881
  //#endregion
3553
- export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilySample, FontWeightScale, GradientPalette, MotionPreview, MotionSample, ShadowPreview, ShadowSample, StrokeStyleSample, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
3882
+ export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilySample, FontWeightScale, GradientPalette, MotionPreview, MotionSample, ShadowPreview, ShadowSample, StrokeStyleSample, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
3554
3883
 
3555
3884
  //# sourceMappingURL=index.mjs.map