@unpunnyfuns/swatchbook-blocks 0.15.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,12 +1,12 @@
1
1
  import './style.css';
2
2
  import Color from "colorjs.io";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, useSyncExternalStore } from "react";
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
+ import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
7
8
  import cx from "clsx";
8
9
  import { analyzeAxisVariance } from "@unpunnyfuns/swatchbook-core/variance";
9
- import { fuzzyFilter } from "@unpunnyfuns/swatchbook-core/fuzzy";
10
10
  //#region src/format-color.ts
11
11
  const COLOR_FORMATS = [
12
12
  "hex",
@@ -872,6 +872,533 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
872
872
  });
873
873
  }
874
874
  //#endregion
875
+ //#region src/internal/CopyButton.tsx
876
+ /**
877
+ * Copy the given string to the clipboard and briefly surface a "Copied!"
878
+ * state. Falls back silently on unsupported clipboard APIs (older Safari,
879
+ * insecure origins) — the click still happens, the user just won't see the
880
+ * tick. No custom permission prompt: relies on the browser's native user-
881
+ * activation gate.
882
+ */
883
+ function CopyButton$1({ value, label, variant = "icon", className }) {
884
+ const [copied, setCopied] = useState(false);
885
+ const timerRef = useRef(null);
886
+ useEffect(() => {
887
+ return () => {
888
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
889
+ };
890
+ }, []);
891
+ const handleClick = useCallback(() => {
892
+ try {
893
+ navigator.clipboard?.writeText(value);
894
+ } catch {
895
+ return;
896
+ }
897
+ setCopied(true);
898
+ if (timerRef.current !== null) clearTimeout(timerRef.current);
899
+ timerRef.current = setTimeout(() => setCopied(false), 1500);
900
+ }, [value]);
901
+ const ariaLabel = label ?? `Copy ${value}`;
902
+ const classes = ["sb-copy-button", `sb-copy-button--${variant}`];
903
+ if (copied) classes.push("sb-copy-button--copied");
904
+ if (className) classes.push(className);
905
+ return /* @__PURE__ */ jsx("button", {
906
+ type: "button",
907
+ className: classes.join(" "),
908
+ onClick: handleClick,
909
+ "aria-label": ariaLabel,
910
+ title: ariaLabel,
911
+ "data-copied": copied ? "true" : void 0,
912
+ children: variant === "text" ? /* @__PURE__ */ jsx("span", {
913
+ className: "sb-copy-button__text",
914
+ children: copied ? "Copied" : "Copy"
915
+ }) : /* @__PURE__ */ jsx("span", {
916
+ className: "sb-copy-button__glyph",
917
+ "aria-hidden": true,
918
+ children: copied ? "✓" : "⧉"
919
+ })
920
+ });
921
+ }
922
+ //#endregion
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
+ }
1301
+ /**
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.
1307
+ */
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
+ }
1329
+ }
1330
+ }
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);
1345
+ }
1346
+ return {
1347
+ matchOrder: entries.toSorted((a, b) => b.suffix.length - a.suffix.length),
1348
+ displayOrder
1349
+ };
1350
+ }
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;
1360
+ }
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
875
1402
  //#region src/Diagnostics.tsx
876
1403
  const severityLabel = {
877
1404
  error: "ERROR",
@@ -2022,7 +2549,7 @@ function AliasChain({ path }) {
2022
2549
  return [path];
2023
2550
  }, [token, path]);
2024
2551
  if (chain.length <= 1) return null;
2025
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2552
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2026
2553
  className: "sb-token-detail__section-header",
2027
2554
  children: "Alias chain"
2028
2555
  }), /* @__PURE__ */ jsx("div", {
@@ -2051,7 +2578,7 @@ function AliasedBy({ path }) {
2051
2578
  const tree = useMemo(() => buildAliasedByTree(path, resolved), [path, resolved]);
2052
2579
  const truncated = useMemo(() => treeHasTruncation(tree), [tree]);
2053
2580
  if (tree.length === 0) return null;
2054
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2581
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2055
2582
  /* @__PURE__ */ jsx("div", {
2056
2583
  className: "sb-token-detail__section-header",
2057
2584
  children: "Aliased by"
@@ -2150,11 +2677,11 @@ function AxisVariance({ path }) {
2150
2677
  themes,
2151
2678
  themesResolved
2152
2679
  ]);
2153
- if (themes.length === 0) return /* @__PURE__ */ jsx(Fragment, {});
2680
+ if (themes.length === 0) return /* @__PURE__ */ jsx(Fragment$1, {});
2154
2681
  if (variance.kind === "constant") {
2155
2682
  const anyTheme = themes[0];
2156
2683
  const value = anyTheme ? formatFn(themesResolved[anyTheme.name]?.[path]) : "—";
2157
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
2684
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2158
2685
  className: "sb-token-detail__section-header",
2159
2686
  children: "Values across axes"
2160
2687
  }), /* @__PURE__ */ jsx("table", {
@@ -2190,9 +2717,9 @@ function AxisVariance({ path }) {
2190
2717
  }
2191
2718
  if (variance.kind === "one-axis") {
2192
2719
  const axisName = variance.varyingAxes[0];
2193
- if (!axisName) return /* @__PURE__ */ jsx(Fragment, {});
2720
+ if (!axisName) return /* @__PURE__ */ jsx(Fragment$1, {});
2194
2721
  const axis = axes.find((a) => a.name === axisName);
2195
- if (!axis) return /* @__PURE__ */ jsx(Fragment, {});
2722
+ if (!axis) return /* @__PURE__ */ jsx(Fragment$1, {});
2196
2723
  const contextValues = axis.contexts.map((ctx) => {
2197
2724
  const target = {
2198
2725
  ...activeAxes,
@@ -2208,7 +2735,7 @@ function AxisVariance({ path }) {
2208
2735
  value: name ? formatFn(themesResolved[name]?.[path]) : "—"
2209
2736
  };
2210
2737
  });
2211
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
2738
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs("div", {
2212
2739
  className: "sb-token-detail__section-header",
2213
2740
  children: ["Varies with ", axisName]
2214
2741
  }), /* @__PURE__ */ jsx("table", {
@@ -2235,8 +2762,8 @@ function AxisVariance({ path }) {
2235
2762
  })] });
2236
2763
  }
2237
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);
2238
- if (!rowAxis || !colAxis) return /* @__PURE__ */ jsx(Fragment, {});
2239
- return /* @__PURE__ */ jsxs(Fragment, { children: [
2765
+ if (!rowAxis || !colAxis) return /* @__PURE__ */ jsx(Fragment$1, {});
2766
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2240
2767
  /* @__PURE__ */ jsxs("div", {
2241
2768
  className: "sb-token-detail__section-header",
2242
2769
  children: ["Varies with ", variance.varyingAxes.join(" × ")]
@@ -2481,7 +3008,7 @@ function renderKeyValueList(rows) {
2481
3008
  }
2482
3009
  function KeyValueRow({ label, value, alias }) {
2483
3010
  const hasAlias = alias && alias.length > 0;
2484
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
3011
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
2485
3012
  className: "sb-token-detail__breakdown-key",
2486
3013
  children: label
2487
3014
  }), /* @__PURE__ */ jsxs("span", {
@@ -2737,7 +3264,7 @@ function ConsumerOutput({ path }) {
2737
3264
  const { token, cssVar, activeAxes } = useTokenDetailData(path);
2738
3265
  if (!token) return null;
2739
3266
  const tupleLabel = Object.entries(activeAxes).map(([k, v]) => `${k}=${v}`).join(", ");
2740
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3267
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2741
3268
  /* @__PURE__ */ jsx("div", {
2742
3269
  className: "sb-token-detail__section-header",
2743
3270
  children: "Consumer output"
@@ -2817,7 +3344,7 @@ function TokenHeader({ path, heading }) {
2817
3344
  "."
2818
3345
  ]
2819
3346
  });
2820
- return /* @__PURE__ */ jsxs(Fragment, { children: [
3347
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2821
3348
  /* @__PURE__ */ jsx("h3", {
2822
3349
  className: "sb-token-detail__heading",
2823
3350
  children: heading ?? path
@@ -2840,12 +3367,19 @@ function TokenHeader({ path, heading }) {
2840
3367
  function TokenUsageSnippet({ path }) {
2841
3368
  const { token, cssVar } = useTokenDetailData(path);
2842
3369
  if (!token) return null;
2843
- return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
3370
+ const snippet = `color: ${cssVar};`;
3371
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("div", {
2844
3372
  className: "sb-token-detail__section-header",
2845
3373
  children: "Usage"
2846
- }), /* @__PURE__ */ jsx("code", {
2847
- className: "sb-token-detail__snippet",
2848
- children: `color: ${cssVar};`
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
+ })]
2849
3383
  })] });
2850
3384
  }
2851
3385
  //#endregion
@@ -2900,6 +3434,10 @@ function TokenDetail({ path, heading }) {
2900
3434
  "aria-label": "out of gamut",
2901
3435
  style: { marginLeft: 6 },
2902
3436
  children: "⚠"
3437
+ }),
3438
+ /* @__PURE__ */ jsx(CopyButton$1, {
3439
+ value,
3440
+ label: `Copy value ${value}`
2903
3441
  })
2904
3442
  ]
2905
3443
  }),
@@ -3443,6 +3981,17 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
3443
3981
  "aria-label": "out of gamut",
3444
3982
  className: "sb-token-table__gamut-warn",
3445
3983
  children: "⚠"
3984
+ }),
3985
+ /* @__PURE__ */ jsx("span", {
3986
+ className: "sb-token-table__copy-wrap",
3987
+ onClick: (e) => e.stopPropagation(),
3988
+ onKeyDown: (e) => e.stopPropagation(),
3989
+ role: "presentation",
3990
+ children: /* @__PURE__ */ jsx(CopyButton$1, {
3991
+ value: row.value,
3992
+ label: `Copy value ${row.value}`,
3993
+ className: "sb-token-table__copy"
3994
+ })
3446
3995
  })
3447
3996
  ]
3448
3997
  })
@@ -3550,6 +4099,6 @@ function TypographyScale({ filter, sample = "The quick brown fox jumps over the
3550
4099
  });
3551
4100
  }
3552
4101
  //#endregion
3553
- export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilySample, FontWeightScale, GradientPalette, MotionPreview, MotionSample, ShadowPreview, ShadowSample, StrokeStyleSample, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
4102
+ export { AliasChain, AliasedBy, AxesContext, AxisVariance, BorderPreview, BorderSample, COLOR_FORMATS, ColorFormatContext, ColorPalette, ColorTable, CompositeBreakdown, CompositePreview, ConsumerOutput, Diagnostics, DimensionBar, DimensionScale, FontFamilySample, FontWeightScale, GradientPalette, MotionPreview, MotionSample, ShadowPreview, ShadowSample, StrokeStyleSample, SwatchbookContext, SwatchbookProvider, ThemeContext, TokenDetail, TokenHeader, TokenNavigator, TokenTable, TokenUsageSnippet, TypographyScale, formatColor, useActiveAxes, useActiveTheme, useColorFormat, useOptionalSwatchbookData, useSwatchbookData };
3554
4103
 
3555
4104
  //# sourceMappingURL=index.mjs.map