@unpunnyfuns/swatchbook-blocks 0.62.3 → 0.63.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 +23 -3
- package/dist/index.mjs +376 -21
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +123 -0
- package/package.json +8 -8
package/dist/index.d.mts
CHANGED
|
@@ -29,6 +29,12 @@ interface VirtualTokenShape {
|
|
|
29
29
|
$type?: string | undefined;
|
|
30
30
|
$value?: unknown;
|
|
31
31
|
$description?: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* DTCG `$deprecated` — `true` or a message string. Mirrors core's
|
|
34
|
+
* `SwatchbookToken.$deprecated`; reaches blocks via the resolved token
|
|
35
|
+
* map reconstructed from the wire `tokenGraph`.
|
|
36
|
+
*/
|
|
37
|
+
$deprecated?: string | boolean | undefined;
|
|
32
38
|
aliasOf?: string | undefined;
|
|
33
39
|
aliasChain?: readonly string[] | undefined;
|
|
34
40
|
aliasedBy?: readonly string[] | undefined;
|
|
@@ -271,6 +277,8 @@ interface ColorTableProps {
|
|
|
271
277
|
* own row.
|
|
272
278
|
*/
|
|
273
279
|
variants?: Record<string, string>;
|
|
280
|
+
/** Disambiguates persisted UI state for two identical-prop tables on a page. */
|
|
281
|
+
id?: string;
|
|
274
282
|
}
|
|
275
283
|
declare function ColorTable({
|
|
276
284
|
filter,
|
|
@@ -279,7 +287,8 @@ declare function ColorTable({
|
|
|
279
287
|
sortDir,
|
|
280
288
|
searchable,
|
|
281
289
|
onSelect,
|
|
282
|
-
variants
|
|
290
|
+
variants,
|
|
291
|
+
id
|
|
283
292
|
}: ColorTableProps): ReactElement;
|
|
284
293
|
//#endregion
|
|
285
294
|
//#region src/Diagnostics.d.ts
|
|
@@ -789,13 +798,21 @@ interface TokenNavigatorProps {
|
|
|
789
798
|
* the follow-up UI.
|
|
790
799
|
*/
|
|
791
800
|
onSelect?(path: string): void;
|
|
801
|
+
/**
|
|
802
|
+
* Disambiguates persisted UI state (expand/collapse, selection, search)
|
|
803
|
+
* when two navigators with otherwise-identical props sit on the same docs
|
|
804
|
+
* page. Only needed in that case; the state key is derived from the other
|
|
805
|
+
* props otherwise.
|
|
806
|
+
*/
|
|
807
|
+
id?: string;
|
|
792
808
|
}
|
|
793
809
|
declare function TokenNavigator({
|
|
794
810
|
root,
|
|
795
811
|
type,
|
|
796
812
|
initiallyExpanded,
|
|
797
813
|
searchable,
|
|
798
|
-
onSelect
|
|
814
|
+
onSelect,
|
|
815
|
+
id
|
|
799
816
|
}: TokenNavigatorProps): ReactElement;
|
|
800
817
|
//#endregion
|
|
801
818
|
//#region src/TokenTable.d.ts
|
|
@@ -836,6 +853,8 @@ interface TokenTableProps {
|
|
|
836
853
|
* follow-up UI (inline panel, drill-down route, …).
|
|
837
854
|
*/
|
|
838
855
|
onSelect?(path: string): void;
|
|
856
|
+
/** Disambiguates persisted UI state for two identical-prop tables on a page. */
|
|
857
|
+
id?: string;
|
|
839
858
|
}
|
|
840
859
|
declare function TokenTable({
|
|
841
860
|
filter,
|
|
@@ -844,7 +863,8 @@ declare function TokenTable({
|
|
|
844
863
|
sortBy,
|
|
845
864
|
sortDir,
|
|
846
865
|
searchable,
|
|
847
|
-
onSelect
|
|
866
|
+
onSelect,
|
|
867
|
+
id
|
|
848
868
|
}: TokenTableProps): ReactElement;
|
|
849
869
|
//#endregion
|
|
850
870
|
//#region src/TypographyScale.d.ts
|
package/dist/index.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { COLOR_FORMATS } from "@unpunnyfuns/swatchbook-core/color-formats";
|
|
|
3
3
|
import { formatColor, parseColor } from "@unpunnyfuns/swatchbook-core/format-color";
|
|
4
4
|
import { createContext, memo, useCallback, useContext, useDeferredValue, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
5
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
-
import { getVariance, listPaths,
|
|
6
|
+
import { getVariance, listPaths, resolveAllWithProvenanceAt } from "@unpunnyfuns/swatchbook-core/graph";
|
|
7
7
|
import { makeCssVar } from "@unpunnyfuns/swatchbook-core/css-var";
|
|
8
8
|
import { SWATCHBOOK_STYLE_ELEMENT_ID, ensureStyleElement } from "@unpunnyfuns/swatchbook-core/style-element";
|
|
9
9
|
import { tupleToName } from "@unpunnyfuns/swatchbook-core/themes";
|
|
@@ -244,7 +244,7 @@ function defaultTuple(axes) {
|
|
|
244
244
|
}
|
|
245
245
|
function makeResolveAt(graph) {
|
|
246
246
|
if (!graph) return () => ({});
|
|
247
|
-
return (tuple) =>
|
|
247
|
+
return (tuple) => resolveAllWithProvenanceAt(graph, tuple);
|
|
248
248
|
}
|
|
249
249
|
function snapshotResolveAt(snapshot) {
|
|
250
250
|
if (snapshot.resolveAt) return snapshot.resolveAt;
|
|
@@ -809,16 +809,73 @@ function CopyButton$1({ value, label, variant = "icon", className }) {
|
|
|
809
809
|
})] });
|
|
810
810
|
}
|
|
811
811
|
//#endregion
|
|
812
|
+
//#region src/internal/persistent-state.ts
|
|
813
|
+
/**
|
|
814
|
+
* Block UI state that survives a docs-mode remount.
|
|
815
|
+
*
|
|
816
|
+
* In MDX docs mode Storybook re-renders the docs container on every
|
|
817
|
+
* `updateGlobals` (axis flip), which unmounts and remounts the embedded
|
|
818
|
+
* blocks — destroying any plain `useState` (expand/collapse, selection,
|
|
819
|
+
* search). This is the same problem `channel-globals` solves for the
|
|
820
|
+
* globals: lift the value out of React into module state so it persists
|
|
821
|
+
* across the remount, and re-seed component state from it on mount.
|
|
822
|
+
*
|
|
823
|
+
* `usePersistedState` is a drop-in `useState` whose value is mirrored to a
|
|
824
|
+
* module-level store under a caller-supplied key, and read back from it on
|
|
825
|
+
* (re)mount. `useBlockKey` builds a stable key scoped to the current docs
|
|
826
|
+
* page + block identity so two pages (or two distinct blocks) don't share
|
|
827
|
+
* an entry.
|
|
828
|
+
*/
|
|
829
|
+
const store = /* @__PURE__ */ new Map();
|
|
830
|
+
function pageScope() {
|
|
831
|
+
if (typeof window === "undefined") return "";
|
|
832
|
+
try {
|
|
833
|
+
return new URLSearchParams(window.location.search).get("id") ?? window.location.pathname;
|
|
834
|
+
} catch {
|
|
835
|
+
return "";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
const SEP = "";
|
|
839
|
+
/**
|
|
840
|
+
* Build a stable persistence key for a block's UI state: docs page + block
|
|
841
|
+
* type + the props that distinguish one instance from another (and an
|
|
842
|
+
* optional explicit `id` for identical-prop siblings on the same page).
|
|
843
|
+
*/
|
|
844
|
+
function useBlockKey(blockType, parts) {
|
|
845
|
+
const partsKey = parts.map((p) => p === void 0 ? "" : String(p)).join(SEP);
|
|
846
|
+
return useMemo(() => `${pageScope()}${SEP}${blockType}${SEP}${partsKey}`, [blockType, partsKey]);
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* `useState`, but the value persists across remounts under `key`. `initial`
|
|
850
|
+
* may be a value or a lazy initializer (used only on the first mount when the
|
|
851
|
+
* store has no entry yet — never an actual `T` that's a function here).
|
|
852
|
+
*/
|
|
853
|
+
function usePersistedState(key, initial) {
|
|
854
|
+
const [value, setValue] = useState(() => {
|
|
855
|
+
if (store.has(key)) return store.get(key);
|
|
856
|
+
return typeof initial === "function" ? initial() : initial;
|
|
857
|
+
});
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
store.set(key, value);
|
|
860
|
+
}, [key, value]);
|
|
861
|
+
return [value, setValue];
|
|
862
|
+
}
|
|
863
|
+
//#endregion
|
|
812
864
|
//#region src/ColorTable.tsx
|
|
813
865
|
const BASE_LABEL = "base";
|
|
814
866
|
const COLUMN_COUNT = 6;
|
|
815
|
-
function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants }) {
|
|
867
|
+
function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, variants, id }) {
|
|
816
868
|
const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
|
|
817
869
|
const colorFormat = useColorFormat();
|
|
818
|
-
const
|
|
870
|
+
const blockKey = useBlockKey("ColorTable", [
|
|
871
|
+
filter,
|
|
872
|
+
caption,
|
|
873
|
+
id
|
|
874
|
+
]);
|
|
875
|
+
const [query, setQuery] = usePersistedState(`${blockKey}::query`, "");
|
|
819
876
|
const deferredQuery = useDeferredValue(query);
|
|
820
|
-
const [selectedByBase, setSelectedByBase] =
|
|
821
|
-
const [expandedByBase, setExpandedByBase] =
|
|
877
|
+
const [selectedByBase, setSelectedByBase] = usePersistedState(`${blockKey}::selected`, {});
|
|
878
|
+
const [expandedByBase, setExpandedByBase] = usePersistedState(`${blockKey}::expanded`, () => /* @__PURE__ */ new Set());
|
|
822
879
|
const defs = useMemo(() => buildVariantDefs(variants), [variants]);
|
|
823
880
|
const groups = useMemo(() => {
|
|
824
881
|
const projectFields = {
|
|
@@ -898,13 +955,13 @@ function ColorTable({ filter, caption, sortBy = "path", sortDir = "asc", searcha
|
|
|
898
955
|
else next.add(base);
|
|
899
956
|
return next;
|
|
900
957
|
});
|
|
901
|
-
}, []);
|
|
958
|
+
}, [setExpandedByBase]);
|
|
902
959
|
const selectVariant = useCallback((base, label) => {
|
|
903
960
|
setSelectedByBase((prev) => ({
|
|
904
961
|
...prev,
|
|
905
962
|
[base]: label
|
|
906
963
|
}));
|
|
907
|
-
}, []);
|
|
964
|
+
}, [setSelectedByBase]);
|
|
908
965
|
const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleGroups.length} matching "${query.trim()}"` : "";
|
|
909
966
|
const captionText = caption ?? `${totalTokens} color${totalTokens === 1 ? "" : "s"} across ${groups.length} group${groups.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${matchSuffix} · ${activeTheme}`;
|
|
910
967
|
if (groups.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
@@ -3398,6 +3455,8 @@ function TokenDetail({ path, heading }) {
|
|
|
3398
3455
|
const gamut = isColor ? formatColor(token.$value, colorFormat) : null;
|
|
3399
3456
|
const value = formatTokenValue(token.$value, token.$type, colorFormat, listing[path]);
|
|
3400
3457
|
const outOfGamut = gamut?.outOfGamut ?? false;
|
|
3458
|
+
const dep = token.$deprecated;
|
|
3459
|
+
const isDeprecated = dep === true || typeof dep === "string" && dep.length > 0;
|
|
3401
3460
|
return /* @__PURE__ */ jsxs("div", {
|
|
3402
3461
|
...wrapperAttrs,
|
|
3403
3462
|
className: cx(wrapperAttrs["className"], "sb-token-detail"),
|
|
@@ -3406,6 +3465,19 @@ function TokenDetail({ path, heading }) {
|
|
|
3406
3465
|
path,
|
|
3407
3466
|
...heading !== void 0 && { heading }
|
|
3408
3467
|
}),
|
|
3468
|
+
isDeprecated && /* @__PURE__ */ jsxs("div", {
|
|
3469
|
+
className: "sb-token-detail__deprecated",
|
|
3470
|
+
"data-testid": "token-detail-deprecated",
|
|
3471
|
+
role: "note",
|
|
3472
|
+
children: [
|
|
3473
|
+
/* @__PURE__ */ jsx("span", {
|
|
3474
|
+
"aria-hidden": true,
|
|
3475
|
+
children: "⚠ "
|
|
3476
|
+
}),
|
|
3477
|
+
"Deprecated",
|
|
3478
|
+
typeof dep === "string" ? `: ${dep}` : ""
|
|
3479
|
+
]
|
|
3480
|
+
}),
|
|
3409
3481
|
/* @__PURE__ */ jsxs("div", {
|
|
3410
3482
|
className: "sb-token-detail__section-header",
|
|
3411
3483
|
children: ["Resolved value · ", activeTheme]
|
|
@@ -3536,6 +3608,219 @@ function findBodyChildContaining(node) {
|
|
|
3536
3608
|
return cursor;
|
|
3537
3609
|
}
|
|
3538
3610
|
//#endregion
|
|
3611
|
+
//#region src/token-navigator/navigate.ts
|
|
3612
|
+
/**
|
|
3613
|
+
* The group paths to expand so `path` becomes visible in the tree — the
|
|
3614
|
+
* cumulative dotted prefixes of `path`, excluding `path` itself and any
|
|
3615
|
+
* prefix at or above `root` (the navigator's implicit root container is not
|
|
3616
|
+
* a group node). Matches `buildTree`'s full-dotted group-path scheme.
|
|
3617
|
+
*/
|
|
3618
|
+
function ancestorGroupPaths(path, root) {
|
|
3619
|
+
const segments = path.split(".");
|
|
3620
|
+
const out = [];
|
|
3621
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
3622
|
+
const prefix = segments.slice(0, i).join(".");
|
|
3623
|
+
if (root && !prefix.startsWith(`${root}.`)) continue;
|
|
3624
|
+
out.push(prefix);
|
|
3625
|
+
}
|
|
3626
|
+
return out;
|
|
3627
|
+
}
|
|
3628
|
+
/**
|
|
3629
|
+
* Whether `path` survives the navigator's structural (`root` / `type`)
|
|
3630
|
+
* filters and exists in the resolved map — i.e. it can be selected in the
|
|
3631
|
+
* current tree. Transient search is NOT considered here: a target hidden
|
|
3632
|
+
* only by an active query is still "in view" once the query is cleared,
|
|
3633
|
+
* which the caller handles.
|
|
3634
|
+
*/
|
|
3635
|
+
function isInView(path, ctx) {
|
|
3636
|
+
const token = ctx.resolved[path];
|
|
3637
|
+
if (!token) return false;
|
|
3638
|
+
if (ctx.root && !(path === ctx.root || path.startsWith(`${ctx.root}.`))) return false;
|
|
3639
|
+
if (ctx.typeFilter && !(token.$type !== void 0 && ctx.typeFilter.has(token.$type))) return false;
|
|
3640
|
+
return true;
|
|
3641
|
+
}
|
|
3642
|
+
//#endregion
|
|
3643
|
+
//#region src/token-navigator/RowIndicators.tsx
|
|
3644
|
+
function relativeLabel(path, root) {
|
|
3645
|
+
if (root && path.startsWith(`${root}.`)) return path.slice(root.length + 1);
|
|
3646
|
+
return path;
|
|
3647
|
+
}
|
|
3648
|
+
/**
|
|
3649
|
+
* The forward alias chain for one row. Full chain in `aria-label`; visually
|
|
3650
|
+
* capped to first … last beyond two hops (no width measurement). Each shown
|
|
3651
|
+
* node navigates when in view, else renders as plain text.
|
|
3652
|
+
*/
|
|
3653
|
+
function ForwardChain({ chain, root, resolveInView, onNavigate }) {
|
|
3654
|
+
const full = chain.map((p) => relativeLabel(p, root)).join(" → ");
|
|
3655
|
+
const capped = chain.length > 2;
|
|
3656
|
+
const shown = capped ? [chain[0], chain[chain.length - 1]] : [...chain];
|
|
3657
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
3658
|
+
className: "sb-token-navigator__alias-forward",
|
|
3659
|
+
"data-testid": "row-indicator-alias-forward",
|
|
3660
|
+
"aria-label": `aliases ${full}`,
|
|
3661
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
3662
|
+
className: "sb-token-navigator__alias-arrow",
|
|
3663
|
+
"aria-hidden": true,
|
|
3664
|
+
children: "→"
|
|
3665
|
+
}), shown.map((target, i) => {
|
|
3666
|
+
const label = relativeLabel(target, root);
|
|
3667
|
+
return /* @__PURE__ */ jsxs("span", { children: [resolveInView(target) ? /* @__PURE__ */ jsx("button", {
|
|
3668
|
+
type: "button",
|
|
3669
|
+
className: "sb-token-navigator__alias-node",
|
|
3670
|
+
"data-testid": "alias-node",
|
|
3671
|
+
"aria-label": target,
|
|
3672
|
+
onClick: (e) => {
|
|
3673
|
+
e.stopPropagation();
|
|
3674
|
+
onNavigate(target);
|
|
3675
|
+
},
|
|
3676
|
+
children: label
|
|
3677
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
3678
|
+
className: "sb-token-navigator__alias-node sb-token-navigator__alias-node--offview",
|
|
3679
|
+
"data-testid": "alias-node",
|
|
3680
|
+
title: "outside current view",
|
|
3681
|
+
children: label
|
|
3682
|
+
}), capped && i === 0 ? /* @__PURE__ */ jsxs("span", {
|
|
3683
|
+
className: "sb-token-navigator__alias-arrow",
|
|
3684
|
+
"aria-hidden": true,
|
|
3685
|
+
children: [
|
|
3686
|
+
" ",
|
|
3687
|
+
"→ … →",
|
|
3688
|
+
" "
|
|
3689
|
+
]
|
|
3690
|
+
}) : i < shown.length - 1 ? /* @__PURE__ */ jsxs("span", {
|
|
3691
|
+
className: "sb-token-navigator__alias-arrow",
|
|
3692
|
+
"aria-hidden": true,
|
|
3693
|
+
children: [
|
|
3694
|
+
" ",
|
|
3695
|
+
"→",
|
|
3696
|
+
" "
|
|
3697
|
+
]
|
|
3698
|
+
}) : null] }, target);
|
|
3699
|
+
})]
|
|
3700
|
+
});
|
|
3701
|
+
}
|
|
3702
|
+
function DeprecatedBadge({ deprecated }) {
|
|
3703
|
+
const label = typeof deprecated === "string" ? `deprecated: ${deprecated}` : "deprecated";
|
|
3704
|
+
return /* @__PURE__ */ jsx("span", {
|
|
3705
|
+
className: "sb-token-navigator__deprecated",
|
|
3706
|
+
"data-testid": "row-indicator-deprecated",
|
|
3707
|
+
title: label,
|
|
3708
|
+
"aria-label": label,
|
|
3709
|
+
children: "deprecated"
|
|
3710
|
+
});
|
|
3711
|
+
}
|
|
3712
|
+
function VarianceBadge({ variance }) {
|
|
3713
|
+
if (variance.kind === "constant") return null;
|
|
3714
|
+
const axes = variance.varyingAxes;
|
|
3715
|
+
const label = variance.kind === "single" ? variance.axis : `${axes.length} axes`;
|
|
3716
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
3717
|
+
className: "sb-token-navigator__variance",
|
|
3718
|
+
"data-testid": "row-indicator-variance",
|
|
3719
|
+
"aria-label": `varies by ${axes.join(", ")}`,
|
|
3720
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
3721
|
+
className: "sb-token-navigator__variance-glyph",
|
|
3722
|
+
"aria-hidden": true,
|
|
3723
|
+
children: "⊹"
|
|
3724
|
+
}), label]
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
function ReverseCount({ referents, resolveInView, onNavigate }) {
|
|
3728
|
+
const [open, setOpen] = useState(false);
|
|
3729
|
+
const wrapRef = useRef(null);
|
|
3730
|
+
const count = referents.length;
|
|
3731
|
+
const single = count === 1;
|
|
3732
|
+
useEffect(() => {
|
|
3733
|
+
if (single || !open) return;
|
|
3734
|
+
(wrapRef.current?.querySelector("button[role=\"menuitem\"]:not(:disabled)"))?.focus();
|
|
3735
|
+
const handlePointerDown = (e) => {
|
|
3736
|
+
if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
|
|
3737
|
+
};
|
|
3738
|
+
document.addEventListener("pointerdown", handlePointerDown);
|
|
3739
|
+
return () => {
|
|
3740
|
+
document.removeEventListener("pointerdown", handlePointerDown);
|
|
3741
|
+
};
|
|
3742
|
+
}, [open, single]);
|
|
3743
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
3744
|
+
ref: wrapRef,
|
|
3745
|
+
className: "sb-token-navigator__reverse-wrap",
|
|
3746
|
+
onKeyDown: (e) => {
|
|
3747
|
+
if (e.key === "Escape") setOpen(false);
|
|
3748
|
+
},
|
|
3749
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
3750
|
+
type: "button",
|
|
3751
|
+
className: "sb-token-navigator__alias-reverse",
|
|
3752
|
+
"data-testid": "row-indicator-alias-reverse",
|
|
3753
|
+
"aria-label": `referenced by ${count} ${count === 1 ? "token" : "tokens"}`,
|
|
3754
|
+
"aria-haspopup": single ? void 0 : "menu",
|
|
3755
|
+
"aria-expanded": single ? void 0 : open,
|
|
3756
|
+
onClick: (e) => {
|
|
3757
|
+
e.stopPropagation();
|
|
3758
|
+
if (single) onNavigate(referents[0]);
|
|
3759
|
+
else setOpen((v) => !v);
|
|
3760
|
+
},
|
|
3761
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
3762
|
+
className: "sb-token-navigator__alias-arrow",
|
|
3763
|
+
"aria-hidden": true,
|
|
3764
|
+
children: "←"
|
|
3765
|
+
}), count]
|
|
3766
|
+
}), !single && open && /* @__PURE__ */ jsx("ul", {
|
|
3767
|
+
className: "sb-token-navigator__reverse-menu",
|
|
3768
|
+
role: "menu",
|
|
3769
|
+
children: referents.map((ref) => /* @__PURE__ */ jsx("li", {
|
|
3770
|
+
role: "none",
|
|
3771
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
3772
|
+
type: "button",
|
|
3773
|
+
role: "menuitem",
|
|
3774
|
+
className: "sb-token-navigator__reverse-item",
|
|
3775
|
+
disabled: !resolveInView(ref),
|
|
3776
|
+
title: resolveInView(ref) ? void 0 : "outside current view",
|
|
3777
|
+
onClick: (e) => {
|
|
3778
|
+
e.stopPropagation();
|
|
3779
|
+
setOpen(false);
|
|
3780
|
+
onNavigate(ref);
|
|
3781
|
+
},
|
|
3782
|
+
children: ref
|
|
3783
|
+
})
|
|
3784
|
+
}, ref))
|
|
3785
|
+
})]
|
|
3786
|
+
});
|
|
3787
|
+
}
|
|
3788
|
+
/** Per-row indicator strip: alias references, variance, gamut, deprecation. */
|
|
3789
|
+
function RowIndicators(props) {
|
|
3790
|
+
const { token, root, variance, colorFormat, resolveInView, onNavigate } = props;
|
|
3791
|
+
const aliasChain = Array.isArray(token.aliasChain) && token.aliasChain.length > 0 ? token.aliasChain : void 0;
|
|
3792
|
+
const reverseCount = Array.isArray(token.aliasedBy) && token.aliasedBy.length > 0 ? token.aliasedBy.length : 0;
|
|
3793
|
+
const isVarying = variance !== void 0 && variance.kind !== "constant";
|
|
3794
|
+
const outOfGamut = token.$type === "color" && (formatColor(token.$value, colorFormat)?.outOfGamut ?? false);
|
|
3795
|
+
const deprecated = token.$deprecated;
|
|
3796
|
+
const isDeprecated = deprecated === true || typeof deprecated === "string" && deprecated.length > 0;
|
|
3797
|
+
if (!aliasChain && reverseCount === 0 && !isVarying && !outOfGamut && !isDeprecated) return null;
|
|
3798
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
3799
|
+
className: "sb-token-navigator__indicators",
|
|
3800
|
+
children: [
|
|
3801
|
+
isDeprecated && deprecated !== void 0 && /* @__PURE__ */ jsx(DeprecatedBadge, { deprecated }),
|
|
3802
|
+
aliasChain && /* @__PURE__ */ jsx(ForwardChain, {
|
|
3803
|
+
chain: aliasChain,
|
|
3804
|
+
root,
|
|
3805
|
+
resolveInView,
|
|
3806
|
+
onNavigate
|
|
3807
|
+
}),
|
|
3808
|
+
reverseCount > 0 && token.aliasedBy && /* @__PURE__ */ jsx(ReverseCount, {
|
|
3809
|
+
referents: token.aliasedBy,
|
|
3810
|
+
resolveInView,
|
|
3811
|
+
onNavigate
|
|
3812
|
+
}),
|
|
3813
|
+
variance && /* @__PURE__ */ jsx(VarianceBadge, { variance }),
|
|
3814
|
+
outOfGamut && /* @__PURE__ */ jsx("span", {
|
|
3815
|
+
className: "sb-token-navigator__gamut",
|
|
3816
|
+
title: "Out of sRGB gamut for this format",
|
|
3817
|
+
"aria-label": "out of gamut",
|
|
3818
|
+
children: "⚠"
|
|
3819
|
+
})
|
|
3820
|
+
]
|
|
3821
|
+
});
|
|
3822
|
+
}
|
|
3823
|
+
//#endregion
|
|
3539
3824
|
//#region src/TokenNavigator.tsx
|
|
3540
3825
|
function buildTree(resolved, root, typeFilter) {
|
|
3541
3826
|
const rootPrefix = root && root.length > 0 ? `${root}.` : "";
|
|
@@ -3639,8 +3924,13 @@ function pruneTreeForMatches(nodes, matches, expandOut) {
|
|
|
3639
3924
|
}
|
|
3640
3925
|
return out;
|
|
3641
3926
|
}
|
|
3642
|
-
function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect }) {
|
|
3927
|
+
function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect, id }) {
|
|
3643
3928
|
const { resolved, activeTheme, activeAxes, cssVarPrefix } = useProject();
|
|
3929
|
+
const blockKey = useBlockKey("TokenNavigator", [
|
|
3930
|
+
root,
|
|
3931
|
+
type === void 0 ? "" : typeof type === "string" ? type : type.join(","),
|
|
3932
|
+
id
|
|
3933
|
+
]);
|
|
3644
3934
|
const typeFilter = useMemo(() => {
|
|
3645
3935
|
if (type === void 0) return void 0;
|
|
3646
3936
|
return new Set(Array.isArray(type) ? type : [type]);
|
|
@@ -3655,15 +3945,19 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
|
|
|
3655
3945
|
collectInitialExpanded(tree, initiallyExpanded, out);
|
|
3656
3946
|
return out;
|
|
3657
3947
|
}, [tree, initiallyExpanded]);
|
|
3658
|
-
const [expanded, setExpanded] =
|
|
3948
|
+
const [expanded, setExpanded] = usePersistedState(`${blockKey}::expanded`, initialExpanded);
|
|
3659
3949
|
const initiallyExpandedRef = useRef(initiallyExpanded);
|
|
3660
3950
|
useEffect(() => {
|
|
3661
3951
|
if (initiallyExpandedRef.current === initiallyExpanded) return;
|
|
3662
3952
|
initiallyExpandedRef.current = initiallyExpanded;
|
|
3663
3953
|
setExpanded(initialExpanded);
|
|
3664
|
-
}, [
|
|
3665
|
-
|
|
3666
|
-
|
|
3954
|
+
}, [
|
|
3955
|
+
initiallyExpanded,
|
|
3956
|
+
initialExpanded,
|
|
3957
|
+
setExpanded
|
|
3958
|
+
]);
|
|
3959
|
+
const [selectedPath, setSelectedPath] = usePersistedState(`${blockKey}::selected`, null);
|
|
3960
|
+
const [query, setQuery] = usePersistedState(`${blockKey}::query`, "");
|
|
3667
3961
|
const deferredQuery = useDeferredValue(query);
|
|
3668
3962
|
const { visibleTree, searchExpanded } = useMemo(() => {
|
|
3669
3963
|
if (!searchable || deferredQuery.trim() === "") return {
|
|
@@ -3696,13 +3990,44 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
|
|
|
3696
3990
|
else next.add(path);
|
|
3697
3991
|
return next;
|
|
3698
3992
|
});
|
|
3699
|
-
}, []);
|
|
3993
|
+
}, [setExpanded]);
|
|
3700
3994
|
const handleLeafClick = useCallback((path) => {
|
|
3701
3995
|
if (onSelect) onSelect(path);
|
|
3702
3996
|
else setSelectedPath(path);
|
|
3703
|
-
}, [onSelect]);
|
|
3997
|
+
}, [onSelect, setSelectedPath]);
|
|
3704
3998
|
const [storedFocus, setStoredFocus] = useState(null);
|
|
3705
3999
|
const treeItemRefs = useRef(/* @__PURE__ */ new Map());
|
|
4000
|
+
const resolveInView = useCallback((path) => isInView(path, {
|
|
4001
|
+
resolved,
|
|
4002
|
+
root,
|
|
4003
|
+
typeFilter
|
|
4004
|
+
}), [
|
|
4005
|
+
resolved,
|
|
4006
|
+
root,
|
|
4007
|
+
typeFilter
|
|
4008
|
+
]);
|
|
4009
|
+
const navigateTo = useCallback((target) => {
|
|
4010
|
+
setQuery("");
|
|
4011
|
+
setExpanded((prev) => {
|
|
4012
|
+
const next = new Set(prev);
|
|
4013
|
+
for (const p of ancestorGroupPaths(target, root)) next.add(p);
|
|
4014
|
+
return next;
|
|
4015
|
+
});
|
|
4016
|
+
if (onSelect) onSelect(target);
|
|
4017
|
+
else setSelectedPath(target);
|
|
4018
|
+
setStoredFocus(target);
|
|
4019
|
+
requestAnimationFrame(() => {
|
|
4020
|
+
const el = treeItemRefs.current.get(target);
|
|
4021
|
+
el?.scrollIntoView({ block: "nearest" });
|
|
4022
|
+
el?.focus();
|
|
4023
|
+
});
|
|
4024
|
+
}, [
|
|
4025
|
+
root,
|
|
4026
|
+
onSelect,
|
|
4027
|
+
setQuery,
|
|
4028
|
+
setExpanded,
|
|
4029
|
+
setSelectedPath
|
|
4030
|
+
]);
|
|
3706
4031
|
const registerTreeItem = useCallback((path) => (el) => {
|
|
3707
4032
|
if (el) treeItemRefs.current.set(path, el);
|
|
3708
4033
|
else treeItemRefs.current.delete(path);
|
|
@@ -3878,6 +4203,9 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
|
|
|
3878
4203
|
onToggle: toggle,
|
|
3879
4204
|
onFocusPath: setStoredFocus,
|
|
3880
4205
|
onLeafClick: handleLeafClick,
|
|
4206
|
+
root,
|
|
4207
|
+
resolveInView,
|
|
4208
|
+
onNavigate: navigateTo,
|
|
3881
4209
|
level: 1,
|
|
3882
4210
|
setsize: visibleTree.length,
|
|
3883
4211
|
posinset: i + 1
|
|
@@ -3891,13 +4219,16 @@ function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true,
|
|
|
3891
4219
|
]
|
|
3892
4220
|
});
|
|
3893
4221
|
}
|
|
3894
|
-
function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, level, setsize, posinset }) {
|
|
4222
|
+
function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle, onFocusPath, onLeafClick, root, resolveInView, onNavigate, level, setsize, posinset }) {
|
|
3895
4223
|
if (node.kind === "leaf") return /* @__PURE__ */ jsx(LeafRow, {
|
|
3896
4224
|
node,
|
|
3897
4225
|
isFocused: focusedPath === node.path,
|
|
3898
4226
|
registerTreeItem,
|
|
3899
4227
|
onFocusPath,
|
|
3900
4228
|
onLeafClick,
|
|
4229
|
+
root,
|
|
4230
|
+
resolveInView,
|
|
4231
|
+
onNavigate,
|
|
3901
4232
|
level,
|
|
3902
4233
|
setsize,
|
|
3903
4234
|
posinset
|
|
@@ -3945,6 +4276,9 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
|
|
|
3945
4276
|
onToggle,
|
|
3946
4277
|
onFocusPath,
|
|
3947
4278
|
onLeafClick,
|
|
4279
|
+
root,
|
|
4280
|
+
resolveInView,
|
|
4281
|
+
onNavigate,
|
|
3948
4282
|
level: level + 1,
|
|
3949
4283
|
setsize: node.children.length,
|
|
3950
4284
|
posinset: i + 1
|
|
@@ -3952,8 +4286,13 @@ function TreeNodeRow({ node, expanded, focusedPath, registerTreeItem, onToggle,
|
|
|
3952
4286
|
})]
|
|
3953
4287
|
});
|
|
3954
4288
|
}
|
|
3955
|
-
const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFocusPath, onLeafClick, level, setsize, posinset }) {
|
|
4289
|
+
const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFocusPath, onLeafClick, root, resolveInView, onNavigate, level, setsize, posinset }) {
|
|
3956
4290
|
const type = node.token.$type ?? "";
|
|
4291
|
+
const project = useProject();
|
|
4292
|
+
const colorFormat = useColorFormat();
|
|
4293
|
+
const variance = project.varianceByPath[node.path];
|
|
4294
|
+
const dep = node.token.$deprecated;
|
|
4295
|
+
const isDeprecated = dep === true || typeof dep === "string" && dep.length > 0;
|
|
3957
4296
|
return /* @__PURE__ */ jsx("li", {
|
|
3958
4297
|
ref: registerTreeItem(node.path),
|
|
3959
4298
|
role: "treeitem",
|
|
@@ -3967,6 +4306,7 @@ const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFoc
|
|
|
3967
4306
|
children: /* @__PURE__ */ jsxs("div", {
|
|
3968
4307
|
className: "sb-token-navigator__leaf-row",
|
|
3969
4308
|
"data-testid": "token-navigator-leaf-row",
|
|
4309
|
+
"data-deprecated": isDeprecated ? "true" : void 0,
|
|
3970
4310
|
onClick: () => {
|
|
3971
4311
|
onFocusPath(node.path);
|
|
3972
4312
|
onLeafClick(node.path);
|
|
@@ -3985,6 +4325,15 @@ const LeafRow = memo(function LeafRow({ node, isFocused, registerTreeItem, onFoc
|
|
|
3985
4325
|
className: "sb-token-navigator__type-pill",
|
|
3986
4326
|
children: type
|
|
3987
4327
|
}),
|
|
4328
|
+
/* @__PURE__ */ jsx(RowIndicators, {
|
|
4329
|
+
path: node.path,
|
|
4330
|
+
token: node.token,
|
|
4331
|
+
root,
|
|
4332
|
+
variance,
|
|
4333
|
+
colorFormat,
|
|
4334
|
+
resolveInView,
|
|
4335
|
+
onNavigate
|
|
4336
|
+
}),
|
|
3988
4337
|
/* @__PURE__ */ jsx(LeafPreview, {
|
|
3989
4338
|
path: node.path,
|
|
3990
4339
|
token: node.token
|
|
@@ -4055,11 +4404,17 @@ const LeafPreview = memo(function LeafPreview({ path, token }) {
|
|
|
4055
4404
|
});
|
|
4056
4405
|
//#endregion
|
|
4057
4406
|
//#region src/TokenTable.tsx
|
|
4058
|
-
function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
|
|
4407
|
+
function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect, id }) {
|
|
4059
4408
|
const { resolved, activeTheme, activeAxes, cssVarPrefix, listing } = useProject();
|
|
4060
4409
|
const colorFormat = useColorFormat();
|
|
4061
|
-
const
|
|
4062
|
-
|
|
4410
|
+
const blockKey = useBlockKey("TokenTable", [
|
|
4411
|
+
filter,
|
|
4412
|
+
type,
|
|
4413
|
+
caption,
|
|
4414
|
+
id
|
|
4415
|
+
]);
|
|
4416
|
+
const [selectedPath, setSelectedPath] = usePersistedState(`${blockKey}::selected`, null);
|
|
4417
|
+
const [query, setQuery] = usePersistedState(`${blockKey}::query`, "");
|
|
4063
4418
|
const deferredQuery = useDeferredValue(query);
|
|
4064
4419
|
const rows = useMemo(() => {
|
|
4065
4420
|
const projectFields = {
|
|
@@ -4106,7 +4461,7 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", s
|
|
|
4106
4461
|
const handleRowClick = useCallback((path) => {
|
|
4107
4462
|
if (onSelect) onSelect(path);
|
|
4108
4463
|
else setSelectedPath(path);
|
|
4109
|
-
}, [onSelect]);
|
|
4464
|
+
}, [onSelect, setSelectedPath]);
|
|
4110
4465
|
const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
|
|
4111
4466
|
const captionText = caption ?? `${rows.length} token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${type ? ` · $type=${type}` : ""}${matchSuffix} · ${activeTheme}`;
|
|
4112
4467
|
if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
|