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