@unpunnyfuns/swatchbook-blocks 0.58.0 → 0.59.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.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _$react from "react";
2
2
  import { ReactElement, ReactNode } from "react";
3
- import { Axis, AxisVarianceResult, Diagnostic, Preset } from "@unpunnyfuns/swatchbook-core";
3
+ import { Axis, AxisVarianceResult, Diagnostic, Preset, TupleKey } from "@unpunnyfuns/swatchbook-core";
4
4
  import { SlimListedToken } from "@unpunnyfuns/swatchbook-core/snapshot-for-wire";
5
5
 
6
6
  //#region src/format-color.d.ts
@@ -151,7 +151,7 @@ interface ProjectSnapshot {
151
151
  * Same ascending-arity iteration order the Map carries on the
152
152
  * server side. Empty array when no joint divergences exist.
153
153
  */
154
- jointOverrides: readonly (readonly [string, VirtualJointOverrideShape])[];
154
+ jointOverrides: readonly (readonly [TupleKey, VirtualJointOverrideShape])[];
155
155
  /**
156
156
  * Cached per-path variance results. Blocks read this for O(1) axis
157
157
  * variance lookup instead of recomputing on each render.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import './style.css';
2
2
  import Color from "colorjs.io";
3
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { buildResolveAt } from "@unpunnyfuns/swatchbook-core/resolve-at";
6
6
  import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
@@ -881,10 +881,13 @@ function fixedPrefixLength(filter) {
881
881
  return fixed;
882
882
  }
883
883
  function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "asc" }) {
884
- const project = useProject();
885
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
884
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
886
885
  const colorFormat = useColorFormat();
887
886
  const groups = useMemo(() => {
887
+ const projectFields = {
888
+ listing,
889
+ cssVarPrefix
890
+ };
888
891
  const entries = sortTokens(Object.entries(resolved).filter(([path, token]) => {
889
892
  if (token.$type !== "color") return false;
890
893
  return matchPath(path, filter);
@@ -900,11 +903,11 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
900
903
  const groupKey = segments.slice(0, effectiveGroupBy).join(".");
901
904
  const leaf = segments.slice(effectiveGroupBy).join(".") || segments.at(-1) || path;
902
905
  const list = bucket.get(groupKey) ?? [];
903
- const formatted = resolveColorValue(path, token.$value, colorFormat, project);
906
+ const formatted = resolveColorValue(path, token.$value, colorFormat, projectFields);
904
907
  list.push({
905
908
  path,
906
909
  leaf,
907
- cssVar: resolveCssVar(path, project),
910
+ cssVar: resolveCssVar(path, projectFields),
908
911
  value: formatted.value,
909
912
  outOfGamut: formatted.outOfGamut
910
913
  });
@@ -913,9 +916,10 @@ function ColorPalette({ filter, groupBy, caption, sortBy = "path", sortDir = "as
913
916
  return [...bucket.entries()].toSorted(([a], [b]) => a.localeCompare(b, void 0, { numeric: true }));
914
917
  }, [
915
918
  resolved,
919
+ listing,
920
+ cssVarPrefix,
916
921
  filter,
917
922
  groupBy,
918
- project,
919
923
  colorFormat,
920
924
  sortBy,
921
925
  sortDir
@@ -1021,14 +1025,18 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
1021
1025
  const BASE_LABEL = "base";
1022
1026
  const COLUMN_COUNT = 6;
1023
1027
  function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
1024
- const project = useProject();
1025
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
1028
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
1026
1029
  const colorFormat = useColorFormat();
1027
1030
  const [query, setQuery] = useState("");
1031
+ const deferredQuery = useDeferredValue(query);
1028
1032
  const [selectedByBase, setSelectedByBase] = useState({});
1029
1033
  const [expandedByBase, setExpandedByBase] = useState(() => /* @__PURE__ */ new Set());
1030
1034
  const defs = useMemo(() => buildVariantDefs(variants), [variants]);
1031
1035
  const groups = useMemo(() => {
1036
+ const projectFields = {
1037
+ listing,
1038
+ cssVarPrefix
1039
+ };
1032
1040
  const sorted = sortTokens(Object.entries(resolved).filter(([path, token]) => {
1033
1041
  if (token.$type !== "color") return false;
1034
1042
  return matchPath(path, filter);
@@ -1039,7 +1047,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1039
1047
  const groupMap = /* @__PURE__ */ new Map();
1040
1048
  for (const [path, token] of sorted) {
1041
1049
  const raw = token.$value;
1042
- const hex = resolveColorValue(path, raw, "hex", project);
1050
+ const hex = resolveColorValue(path, raw, "hex", projectFields);
1043
1051
  const hsl = formatColor(raw, "hsl");
1044
1052
  const oklch = formatColor(raw, "oklch");
1045
1053
  const active = pickActiveFormat(raw, colorFormat, hex, hsl, oklch);
@@ -1047,7 +1055,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1047
1055
  const variant = {
1048
1056
  label: match?.label ?? BASE_LABEL,
1049
1057
  path,
1050
- cssVar: resolveCssVar(path, project),
1058
+ cssVar: resolveCssVar(path, projectFields),
1051
1059
  value: active.value,
1052
1060
  outOfGamut: active.outOfGamut,
1053
1061
  hex: hex.value,
@@ -1078,19 +1086,20 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1078
1086
  return out;
1079
1087
  }, [
1080
1088
  resolved,
1089
+ listing,
1090
+ cssVarPrefix,
1081
1091
  filter,
1082
- project,
1083
1092
  sortBy,
1084
1093
  sortDir,
1085
1094
  defs,
1086
1095
  colorFormat
1087
1096
  ]);
1088
1097
  const visibleGroups = useMemo(() => {
1089
- if (!searchable || query.trim() === "") return groups;
1090
- return fuzzyFilter(groups, query, (g) => g.searchText);
1098
+ if (!searchable || deferredQuery.trim() === "") return groups;
1099
+ return fuzzyFilter(groups, deferredQuery, (g) => g.searchText);
1091
1100
  }, [
1092
1101
  groups,
1093
- query,
1102
+ deferredQuery,
1094
1103
  searchable
1095
1104
  ]);
1096
1105
  const totalTokens = useMemo(() => groups.reduce((n, g) => n + g.variants.length, 0), [groups]);
@@ -1192,7 +1201,7 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
1192
1201
  })]
1193
1202
  });
1194
1203
  }
1195
- function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect }) {
1204
+ const GroupRow = memo(function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVariant, onSelect }) {
1196
1205
  const multi = group.variants.length > 1;
1197
1206
  const active = group.variants.find((v) => v.label === selectedLabel) ?? group.variants[0];
1198
1207
  const nameText = multi ? group.base : active.path;
@@ -1291,7 +1300,7 @@ function GroupRow({ group, selectedLabel, expanded, onToggleExpand, onSelectVari
1291
1300
  })
1292
1301
  })
1293
1302
  })] });
1294
- }
1303
+ });
1295
1304
  function ExpandedDetail({ group, active }) {
1296
1305
  const hasDescription = active.description !== void 0 && active.description.length > 0;
1297
1306
  const chain = active.aliasChain && active.aliasChain.length > 0 ? active.aliasChain : void 0;
@@ -1507,29 +1516,32 @@ const severityLabel = {
1507
1516
  warn: "WARN",
1508
1517
  info: "INFO"
1509
1518
  };
1510
- function summaryText(diagnostics) {
1511
- if (diagnostics.length === 0) return "✔ OK · no diagnostics";
1512
- const counts = {
1513
- error: 0,
1514
- warn: 0,
1515
- info: 0
1519
+ function summarize(diagnostics) {
1520
+ if (diagnostics.length === 0) return {
1521
+ text: "✔ OK · no diagnostics",
1522
+ variant: "ok",
1523
+ hasErrorsOrWarnings: false
1516
1524
  };
1517
- for (const d of diagnostics) counts[d.severity] += 1;
1525
+ let errors = 0;
1526
+ let warnings = 0;
1527
+ let infos = 0;
1528
+ for (const d of diagnostics) if (d.severity === "error") errors += 1;
1529
+ else if (d.severity === "warn") warnings += 1;
1530
+ else infos += 1;
1518
1531
  const parts = [];
1519
- if (counts.error > 0) parts.push(`✖ ${counts.error} error${counts.error === 1 ? "" : "s"}`);
1520
- if (counts.warn > 0) parts.push(`⚠ ${counts.warn} warning${counts.warn === 1 ? "" : "s"}`);
1521
- if (counts.info > 0) parts.push(`${counts.info} info`);
1522
- return parts.join(" · ");
1532
+ if (errors > 0) parts.push(`✖ ${errors} error${errors === 1 ? "" : "s"}`);
1533
+ if (warnings > 0) parts.push(`⚠ ${warnings} warning${warnings === 1 ? "" : "s"}`);
1534
+ if (infos > 0) parts.push(`${infos} info`);
1535
+ const variant = errors > 0 ? "error" : warnings > 0 ? "warn" : null;
1536
+ return {
1537
+ text: parts.join(" · "),
1538
+ variant,
1539
+ hasErrorsOrWarnings: errors > 0 || warnings > 0
1540
+ };
1523
1541
  }
1524
1542
  function diagnosticKey(d, i) {
1525
1543
  return `${d.severity}:${d.group}:${d.filename ?? ""}:${d.line ?? ""}:${d.message}:${i}`;
1526
1544
  }
1527
- function summaryVariant(diagnostics) {
1528
- if (diagnostics.length === 0) return "ok";
1529
- if (diagnostics.some((d) => d.severity === "error")) return "error";
1530
- if (diagnostics.some((d) => d.severity === "warn")) return "warn";
1531
- return null;
1532
- }
1533
1545
  /**
1534
1546
  * Render the project's load diagnostics — parser errors, resolver warnings,
1535
1547
  * disabled-axes validation issues, etc. — as a collapsible list. Auto-opens
@@ -1542,16 +1554,15 @@ function summaryVariant(diagnostics) {
1542
1554
  */
1543
1555
  function Diagnostics({ caption } = {}) {
1544
1556
  const { activeAxes, cssVarPrefix, diagnostics } = useProject();
1545
- const hasErrorsOrWarnings = diagnostics.some((d) => d.severity === "error" || d.severity === "warn");
1546
- const headingText = caption ?? `Diagnostics · ${summaryText(diagnostics)}`;
1547
- const variant = summaryVariant(diagnostics);
1557
+ const summary = useMemo(() => summarize(diagnostics), [diagnostics]);
1558
+ const headingText = caption ?? `Diagnostics · ${summary.text}`;
1548
1559
  return /* @__PURE__ */ jsx("div", {
1549
1560
  ...themeAttrs(cssVarPrefix, activeAxes),
1550
1561
  "data-testid": "diagnostics",
1551
1562
  children: /* @__PURE__ */ jsxs("details", {
1552
- open: hasErrorsOrWarnings,
1563
+ open: summary.hasErrorsOrWarnings,
1553
1564
  children: [/* @__PURE__ */ jsx("summary", {
1554
- className: cx("sb-diagnostics__summary", variant && `sb-diagnostics__summary--${variant}`),
1565
+ className: cx("sb-diagnostics__summary", summary.variant && `sb-diagnostics__summary--${summary.variant}`),
1555
1566
  children: headingText
1556
1567
  }), diagnostics.length > 0 && /* @__PURE__ */ jsx("ul", {
1557
1568
  role: "list",
@@ -3897,14 +3908,15 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3897
3908
  }, [initialExpanded]);
3898
3909
  const [selectedPath, setSelectedPath] = useState(null);
3899
3910
  const [query, setQuery] = useState("");
3911
+ const deferredQuery = useDeferredValue(query);
3900
3912
  const { visibleTree, searchExpanded } = useMemo(() => {
3901
- if (!searchable || query.trim() === "") return {
3913
+ if (!searchable || deferredQuery.trim() === "") return {
3902
3914
  visibleTree: tree,
3903
3915
  searchExpanded: null
3904
3916
  };
3905
3917
  const leafPaths = [];
3906
3918
  collectLeafPaths(tree, leafPaths);
3907
- const matches = new Set(fuzzyFilter(leafPaths, query, (p) => p));
3919
+ const matches = new Set(fuzzyFilter(leafPaths, deferredQuery, (p) => p));
3908
3920
  const expandOut = /* @__PURE__ */ new Set();
3909
3921
  return {
3910
3922
  visibleTree: matches.size === 0 ? [] : pruneTreeForMatches(tree, matches, expandOut),
@@ -3912,7 +3924,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3912
3924
  };
3913
3925
  }, [
3914
3926
  tree,
3915
- query,
3927
+ deferredQuery,
3916
3928
  searchable
3917
3929
  ]);
3918
3930
  const effectiveExpanded = useMemo(() => {
@@ -3933,7 +3945,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3933
3945
  if (onSelect) onSelect(path);
3934
3946
  else setSelectedPath(path);
3935
3947
  }, [onSelect]);
3936
- const [focusedPath, setFocusedPath] = useState(null);
3948
+ const [storedFocus, setStoredFocus] = useState(null);
3937
3949
  const treeItemRefs = useRef(/* @__PURE__ */ new Map());
3938
3950
  const registerTreeItem = useCallback((path) => (el) => {
3939
3951
  if (el) treeItemRefs.current.set(path, el);
@@ -3944,22 +3956,15 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
3944
3956
  flattenVisible(visibleTree, effectiveExpanded, null, out);
3945
3957
  return out;
3946
3958
  }, [visibleTree, effectiveExpanded]);
3947
- useEffect(() => {
3948
- if (flatVisible.length === 0) {
3949
- setFocusedPath(null);
3950
- return;
3951
- }
3952
- setFocusedPath((prev) => {
3953
- if (prev && flatVisible.some((entry) => entry.path === prev)) return prev;
3954
- return flatVisible[0]?.path ?? null;
3955
- });
3956
- }, [flatVisible]);
3959
+ const focusedPath = useMemo(() => {
3960
+ if (flatVisible.length === 0) return null;
3961
+ if (storedFocus && flatVisible.some((entry) => entry.path === storedFocus)) return storedFocus;
3962
+ return flatVisible[0]?.path ?? null;
3963
+ }, [flatVisible, storedFocus]);
3957
3964
  const focusByPath = useCallback((path) => {
3958
3965
  const node = treeItemRefs.current.get(path);
3959
- if (node) {
3960
- node.focus();
3961
- setFocusedPath(path);
3962
- } else setFocusedPath(path);
3966
+ if (node) node.focus();
3967
+ setStoredFocus(path);
3963
3968
  }, []);
3964
3969
  useEffect(() => {
3965
3970
  if (focusedPath === null) return;
@@ -4113,7 +4118,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4113
4118
  focusedPath,
4114
4119
  registerTreeItem,
4115
4120
  onToggle: toggle,
4116
- onFocusPath: setFocusedPath,
4121
+ onFocusPath: setStoredFocus,
4117
4122
  onLeafClick: handleLeafClick,
4118
4123
  level: 1,
4119
4124
  setsize: visibleTree.length,
@@ -4131,7 +4136,7 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
4131
4136
  function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, level, setsize, posinset }) {
4132
4137
  if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
4133
4138
  node,
4134
- focusedPath,
4139
+ isFocused: focusedPath === node.path,
4135
4140
  registerTreeItem,
4136
4141
  onFocusPath,
4137
4142
  onLeafClick,
@@ -4189,9 +4194,8 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
4189
4194
  })]
4190
4195
  });
4191
4196
  }
4192
- function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick, level, setsize, posinset }) {
4197
+ const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFocusPath, onLeafClick, level, setsize, posinset }) {
4193
4198
  const type = node.token.$type ?? "";
4194
- const isFocused = focusedPath === node.path;
4195
4199
  return /* @__PURE__ */ jsx("li", {
4196
4200
  ref: registerTreeItem(node.path),
4197
4201
  role: "treeitem",
@@ -4230,8 +4234,8 @@ function LeafRow({ node, focusedPath, registerTreeItem, onFocusPath, onLeafClick
4230
4234
  ]
4231
4235
  })
4232
4236
  });
4233
- }
4234
- function LeafPreview({ path, token }) {
4237
+ });
4238
+ const LeafPreview = memo(function LeafPreview({ path, token }) {
4235
4239
  const project = useProject();
4236
4240
  const colorFormat = useColorFormat();
4237
4241
  const type = token.$type;
@@ -4290,16 +4294,20 @@ function LeafPreview({ path, token }) {
4290
4294
  children: formatTokenValue(token.$value, type, colorFormat, project.listing[path])
4291
4295
  })
4292
4296
  });
4293
- }
4297
+ });
4294
4298
  //#endregion
4295
4299
  //#region src/TokenTable.tsx
4296
4300
  function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
4297
- const project = useProject();
4298
- const { resolved, activeTheme, activeAxes, cssVarPrefix } = project;
4301
+ const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
4299
4302
  const colorFormat = useColorFormat();
4300
4303
  const [selectedPath, setSelectedPath] = useState(null);
4301
4304
  const [query, setQuery] = useState("");
4305
+ const deferredQuery = useDeferredValue(query);
4302
4306
  const rows = useMemo(() => {
4307
+ const projectFields = {
4308
+ listing,
4309
+ cssVarPrefix
4310
+ };
4303
4311
  return sortTokens(Object.entries(resolved).filter(([path, token]) => {
4304
4312
  if (!matchPath(path, filter)) return false;
4305
4313
  if (type && token.$type !== type) return false;
@@ -4309,31 +4317,32 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
4309
4317
  dir: sortDir
4310
4318
  }).map(([path, token]) => {
4311
4319
  const isColor = token.$type === "color";
4312
- const color = isColor ? resolveColorValue(path, token.$value, colorFormat, project) : null;
4320
+ const color = isColor ? resolveColorValue(path, token.$value, colorFormat, projectFields) : null;
4313
4321
  return {
4314
4322
  path,
4315
4323
  type: token.$type ?? "",
4316
- value: formatTokenValue(token.$value, token.$type, colorFormat, project.listing[path]),
4324
+ value: formatTokenValue(token.$value, token.$type, colorFormat, listing[path]),
4317
4325
  outOfGamut: color?.outOfGamut ?? false,
4318
- cssVar: resolveCssVar(path, project),
4326
+ cssVar: resolveCssVar(path, projectFields),
4319
4327
  isColor
4320
4328
  };
4321
4329
  });
4322
4330
  }, [
4323
4331
  resolved,
4332
+ listing,
4333
+ cssVarPrefix,
4324
4334
  filter,
4325
4335
  type,
4326
- project,
4327
4336
  colorFormat,
4328
4337
  sortBy,
4329
4338
  sortDir
4330
4339
  ]);
4331
4340
  const visibleRows = useMemo(() => {
4332
- if (!searchable || query.trim() === "") return rows;
4333
- return fuzzyFilter(rows, query, (row) => `${row.path} ${row.type} ${row.value}`);
4341
+ if (!searchable || deferredQuery.trim() === "") return rows;
4342
+ return fuzzyFilter(rows, deferredQuery, (row) => `${row.path} ${row.type} ${row.value}`);
4334
4343
  }, [
4335
4344
  rows,
4336
- query,
4345
+ deferredQuery,
4337
4346
  searchable
4338
4347
  ]);
4339
4348
  const handleRowClick = useCallback((path) => {