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