@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 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, resolveAllAt } from "@unpunnyfuns/swatchbook-core/graph";
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) => resolveAllAt(graph, 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 [query, setQuery] = useState("");
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] = useState({});
821
- const [expandedByBase, setExpandedByBase] = useState(() => /* @__PURE__ */ new Set());
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] = useState(initialExpanded);
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
- }, [initiallyExpanded, initialExpanded]);
3665
- const [selectedPath, setSelectedPath] = useState(null);
3666
- const [query, setQuery] = useState("");
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 [selectedPath, setSelectedPath] = useState(null);
4062
- const [query, setQuery] = useState("");
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", {