@unpunnyfuns/swatchbook-blocks 0.5.0 → 0.6.1
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 +27 -0
- package/dist/index.mjs +196 -79
- package/dist/index.mjs.map +1 -1
- package/dist/style.css +51 -0
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -590,12 +590,28 @@ declare function TokenUsageSnippet({
|
|
|
590
590
|
interface TokenNavigatorProps {
|
|
591
591
|
/** If provided, mount at this dot-path subtree and hide everything outside it. */
|
|
592
592
|
root?: string;
|
|
593
|
+
/**
|
|
594
|
+
* Restrict the tree to tokens with the given DTCG `$type`(s). Pass a single
|
|
595
|
+
* string to scope to one type (`type="color"`), or an array for a narrow
|
|
596
|
+
* small-multiples view (`type={['duration', 'cubicBezier', 'transition']}`).
|
|
597
|
+
* Composes with `root` — both constraints must hold. Group nodes that end
|
|
598
|
+
* up with no surviving leaves collapse out.
|
|
599
|
+
*/
|
|
600
|
+
type?: string | readonly string[];
|
|
593
601
|
/**
|
|
594
602
|
* Depth (from the mounted root) that is expanded on first render.
|
|
595
603
|
* `0` = everything collapsed, `1` = top-level groups open (default),
|
|
596
604
|
* `2` = one level deeper, etc.
|
|
597
605
|
*/
|
|
598
606
|
initiallyExpanded?: number;
|
|
607
|
+
/**
|
|
608
|
+
* Render a runtime search input above the tree. Matches are
|
|
609
|
+
* case-insensitive substrings against a leaf's token path; groups
|
|
610
|
+
* that contain no matching leaves collapse out, and every group on
|
|
611
|
+
* the path to a match auto-expands so hits are visible without
|
|
612
|
+
* clicking. Defaults to `true`.
|
|
613
|
+
*/
|
|
614
|
+
searchable?: boolean;
|
|
599
615
|
/**
|
|
600
616
|
* Called with a leaf's full dot-path when it is clicked. When set, the
|
|
601
617
|
* inline `<TokenDetail>` slide-over is suppressed — the consumer owns
|
|
@@ -605,7 +621,9 @@ interface TokenNavigatorProps {
|
|
|
605
621
|
}
|
|
606
622
|
declare function TokenNavigator({
|
|
607
623
|
root,
|
|
624
|
+
type,
|
|
608
625
|
initiallyExpanded,
|
|
626
|
+
searchable,
|
|
609
627
|
onSelect
|
|
610
628
|
}: TokenNavigatorProps): ReactElement;
|
|
611
629
|
//#endregion
|
|
@@ -632,6 +650,14 @@ interface TokenTableProps {
|
|
|
632
650
|
sortBy?: SortBy;
|
|
633
651
|
/** `'asc'` (default) or `'desc'`. */
|
|
634
652
|
sortDir?: SortDir;
|
|
653
|
+
/**
|
|
654
|
+
* Render a runtime search input above the table that narrows rows by
|
|
655
|
+
* case-insensitive substring against the token path, type, or value.
|
|
656
|
+
* Defaults to `true` because browsing a multi-hundred-token reference
|
|
657
|
+
* without search is painful. Pass `false` to hide the input (the
|
|
658
|
+
* `filter` / `type` props still apply at authoring time).
|
|
659
|
+
*/
|
|
660
|
+
searchable?: boolean;
|
|
635
661
|
/**
|
|
636
662
|
* Called with the clicked row's dot-path. When set, the built-in
|
|
637
663
|
* `<TokenDetail>` slide-over is suppressed — the consumer owns the
|
|
@@ -645,6 +671,7 @@ declare function TokenTable({
|
|
|
645
671
|
caption,
|
|
646
672
|
sortBy,
|
|
647
673
|
sortDir,
|
|
674
|
+
searchable,
|
|
648
675
|
onSelect
|
|
649
676
|
}: TokenTableProps): ReactElement;
|
|
650
677
|
//#endregion
|
package/dist/index.mjs
CHANGED
|
@@ -2830,12 +2830,13 @@ function DetailOverlay({ path, onClose, testId = "swatchbook-overlay" }) {
|
|
|
2830
2830
|
}
|
|
2831
2831
|
//#endregion
|
|
2832
2832
|
//#region src/TokenNavigator.tsx
|
|
2833
|
-
function buildTree(resolved, root) {
|
|
2833
|
+
function buildTree(resolved, root, typeFilter) {
|
|
2834
2834
|
const rootPrefix = root && root.length > 0 ? `${root}.` : "";
|
|
2835
2835
|
const rootSegments = root ? root.split(".") : [];
|
|
2836
|
-
const entries = Object.entries(resolved).filter(([path]) => {
|
|
2837
|
-
if (!root) return
|
|
2838
|
-
|
|
2836
|
+
const entries = Object.entries(resolved).filter(([path, token]) => {
|
|
2837
|
+
if (root && !(path === root || path.startsWith(rootPrefix))) return false;
|
|
2838
|
+
if (typeFilter && !(token.$type && typeFilter.has(token.$type))) return false;
|
|
2839
|
+
return true;
|
|
2839
2840
|
});
|
|
2840
2841
|
const rootNode = {
|
|
2841
2842
|
kind: "group",
|
|
@@ -2900,9 +2901,39 @@ function countLeaves(node) {
|
|
|
2900
2901
|
for (const c of node.children) n += countLeaves(c);
|
|
2901
2902
|
return n;
|
|
2902
2903
|
}
|
|
2903
|
-
|
|
2904
|
+
/**
|
|
2905
|
+
* Return a pruned copy of the tree containing only leaves whose path
|
|
2906
|
+
* matches `needle` (case-insensitive substring) plus the groups on the
|
|
2907
|
+
* way to them. Every surviving group's path is added to `expandOut` so
|
|
2908
|
+
* callers can force those groups open.
|
|
2909
|
+
*/
|
|
2910
|
+
function pruneTreeForSearch(nodes, needle, expandOut) {
|
|
2911
|
+
const out = [];
|
|
2912
|
+
for (const node of nodes) if (node.kind === "leaf") {
|
|
2913
|
+
if (node.path.toLowerCase().includes(needle)) out.push(node);
|
|
2914
|
+
} else {
|
|
2915
|
+
const children = pruneTreeForSearch(node.children, needle, expandOut);
|
|
2916
|
+
if (children.length > 0) {
|
|
2917
|
+
expandOut.add(node.path);
|
|
2918
|
+
out.push({
|
|
2919
|
+
...node,
|
|
2920
|
+
children
|
|
2921
|
+
});
|
|
2922
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
return out;
|
|
2925
|
+
}
|
|
2926
|
+
function TokenNavigator({ root, type, initiallyExpanded = 1, searchable = true, onSelect }) {
|
|
2904
2927
|
const { resolved, activeTheme, cssVarPrefix } = useProject();
|
|
2905
|
-
const
|
|
2928
|
+
const typeFilter = useMemo(() => {
|
|
2929
|
+
if (type === void 0) return void 0;
|
|
2930
|
+
return new Set(Array.isArray(type) ? type : [type]);
|
|
2931
|
+
}, [type]);
|
|
2932
|
+
const tree = useMemo(() => buildTree(resolved, root, typeFilter), [
|
|
2933
|
+
resolved,
|
|
2934
|
+
root,
|
|
2935
|
+
typeFilter
|
|
2936
|
+
]);
|
|
2906
2937
|
const initialExpanded = useMemo(() => {
|
|
2907
2938
|
const out = /* @__PURE__ */ new Set();
|
|
2908
2939
|
collectInitialExpanded(tree, initiallyExpanded, out);
|
|
@@ -2913,6 +2944,29 @@ function TokenNavigator({ root, initiallyExpanded = 1, onSelect }) {
|
|
|
2913
2944
|
setExpanded(initialExpanded);
|
|
2914
2945
|
}, [initialExpanded]);
|
|
2915
2946
|
const [selectedPath, setSelectedPath] = useState(null);
|
|
2947
|
+
const [query, setQuery] = useState("");
|
|
2948
|
+
const { visibleTree, searchExpanded } = useMemo(() => {
|
|
2949
|
+
if (!searchable || query.trim() === "") return {
|
|
2950
|
+
visibleTree: tree,
|
|
2951
|
+
searchExpanded: null
|
|
2952
|
+
};
|
|
2953
|
+
const needle = query.trim().toLowerCase();
|
|
2954
|
+
const expandOut = /* @__PURE__ */ new Set();
|
|
2955
|
+
return {
|
|
2956
|
+
visibleTree: pruneTreeForSearch(tree, needle, expandOut),
|
|
2957
|
+
searchExpanded: expandOut
|
|
2958
|
+
};
|
|
2959
|
+
}, [
|
|
2960
|
+
tree,
|
|
2961
|
+
query,
|
|
2962
|
+
searchable
|
|
2963
|
+
]);
|
|
2964
|
+
const effectiveExpanded = useMemo(() => {
|
|
2965
|
+
if (!searchExpanded) return expanded;
|
|
2966
|
+
const merged = new Set(expanded);
|
|
2967
|
+
for (const p of searchExpanded) merged.add(p);
|
|
2968
|
+
return merged;
|
|
2969
|
+
}, [expanded, searchExpanded]);
|
|
2916
2970
|
const toggle = useCallback((path) => {
|
|
2917
2971
|
setExpanded((prev) => {
|
|
2918
2972
|
const next = new Set(prev);
|
|
@@ -2925,27 +2979,56 @@ function TokenNavigator({ root, initiallyExpanded = 1, onSelect }) {
|
|
|
2925
2979
|
if (onSelect) onSelect(path);
|
|
2926
2980
|
else setSelectedPath(path);
|
|
2927
2981
|
}, [onSelect]);
|
|
2982
|
+
const typeLabel = typeFilter ? ` · ${[...typeFilter].map((t) => `$type=${t}`).join(", ")}` : "";
|
|
2928
2983
|
if (tree.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
2929
2984
|
...themeAttrs(cssVarPrefix, activeTheme),
|
|
2930
|
-
children: /* @__PURE__ */ jsx(EmptyState, { children: root ? `No tokens under "${root}".` : "No tokens in the active theme." })
|
|
2931
|
-
});
|
|
2985
|
+
children: /* @__PURE__ */ jsx(EmptyState, { children: root ? `No tokens under "${root}"${typeFilter ? ` matching ${typeLabel.slice(3)}` : ""}.` : typeFilter ? `No tokens matching ${typeLabel.slice(3)} in the active theme.` : "No tokens in the active theme." })
|
|
2986
|
+
});
|
|
2987
|
+
const trimmedQuery = query.trim();
|
|
2988
|
+
const matchCount = useMemo(() => {
|
|
2989
|
+
if (!searchExpanded) return 0;
|
|
2990
|
+
let n = 0;
|
|
2991
|
+
for (const node of visibleTree) n += countLeaves(node);
|
|
2992
|
+
return n;
|
|
2993
|
+
}, [visibleTree, searchExpanded]);
|
|
2932
2994
|
return /* @__PURE__ */ jsxs("div", {
|
|
2933
2995
|
...themeAttrs(cssVarPrefix, activeTheme),
|
|
2934
2996
|
children: [
|
|
2997
|
+
searchable && /* @__PURE__ */ jsx("div", {
|
|
2998
|
+
className: "sb-token-navigator__search",
|
|
2999
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
3000
|
+
type: "search",
|
|
3001
|
+
className: "sb-token-navigator__search-input",
|
|
3002
|
+
placeholder: "Search tokens…",
|
|
3003
|
+
value: query,
|
|
3004
|
+
onChange: (e) => setQuery(e.target.value),
|
|
3005
|
+
"aria-label": "Search tokens by path",
|
|
3006
|
+
"data-testid": "token-navigator-search"
|
|
3007
|
+
})
|
|
3008
|
+
}),
|
|
2935
3009
|
/* @__PURE__ */ jsxs("div", {
|
|
2936
3010
|
className: "sb-token-navigator__caption",
|
|
2937
3011
|
children: [
|
|
2938
3012
|
root ? `Tokens under ${root}` : "Token graph",
|
|
3013
|
+
typeLabel,
|
|
3014
|
+
trimmedQuery !== "" ? ` · ${matchCount} matching "${trimmedQuery}"` : "",
|
|
2939
3015
|
" · ",
|
|
2940
3016
|
activeTheme
|
|
2941
3017
|
]
|
|
2942
3018
|
}),
|
|
2943
|
-
/* @__PURE__ */
|
|
3019
|
+
visibleTree.length === 0 ? /* @__PURE__ */ jsxs("div", {
|
|
3020
|
+
className: "sb-block__empty",
|
|
3021
|
+
children: [
|
|
3022
|
+
"No tokens match \"",
|
|
3023
|
+
trimmedQuery,
|
|
3024
|
+
"\"."
|
|
3025
|
+
]
|
|
3026
|
+
}) : /* @__PURE__ */ jsx("ul", {
|
|
2944
3027
|
className: "sb-token-navigator__tree",
|
|
2945
3028
|
role: "tree",
|
|
2946
|
-
children:
|
|
3029
|
+
children: visibleTree.map((node) => /* @__PURE__ */ jsx(TreeNodeRow, {
|
|
2947
3030
|
node,
|
|
2948
|
-
expanded,
|
|
3031
|
+
expanded: effectiveExpanded,
|
|
2949
3032
|
onToggle: toggle,
|
|
2950
3033
|
onLeafClick: handleLeafClick
|
|
2951
3034
|
}, node.path || node.segment))
|
|
@@ -3107,10 +3190,11 @@ function LeafPreview({ path, token }) {
|
|
|
3107
3190
|
}
|
|
3108
3191
|
//#endregion
|
|
3109
3192
|
//#region src/TokenTable.tsx
|
|
3110
|
-
function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", onSelect }) {
|
|
3193
|
+
function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", searchable = true, onSelect }) {
|
|
3111
3194
|
const { resolved, activeTheme, cssVarPrefix } = useProject();
|
|
3112
3195
|
const colorFormat = useColorFormat();
|
|
3113
3196
|
const [selectedPath, setSelectedPath] = useState(null);
|
|
3197
|
+
const [query, setQuery] = useState("");
|
|
3114
3198
|
const rows = useMemo(() => {
|
|
3115
3199
|
return sortTokens(Object.entries(resolved).filter(([path, token]) => {
|
|
3116
3200
|
if (!globMatch(path, filter)) return false;
|
|
@@ -3140,11 +3224,21 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", o
|
|
|
3140
3224
|
sortBy,
|
|
3141
3225
|
sortDir
|
|
3142
3226
|
]);
|
|
3227
|
+
const visibleRows = useMemo(() => {
|
|
3228
|
+
if (!searchable || query.trim() === "") return rows;
|
|
3229
|
+
const needle = query.trim().toLowerCase();
|
|
3230
|
+
return rows.filter((row) => row.path.toLowerCase().includes(needle) || row.type.toLowerCase().includes(needle) || row.value.toLowerCase().includes(needle));
|
|
3231
|
+
}, [
|
|
3232
|
+
rows,
|
|
3233
|
+
query,
|
|
3234
|
+
searchable
|
|
3235
|
+
]);
|
|
3143
3236
|
const handleRowClick = useCallback((path) => {
|
|
3144
3237
|
if (onSelect) onSelect(path);
|
|
3145
3238
|
else setSelectedPath(path);
|
|
3146
3239
|
}, [onSelect]);
|
|
3147
|
-
const
|
|
3240
|
+
const matchSuffix = searchable && query.trim() !== "" ? ` · ${visibleRows.length} matching "${query.trim()}"` : "";
|
|
3241
|
+
const captionText = caption ?? `${rows.length} token${rows.length === 1 ? "" : "s"}${filter ? ` matching \`${filter}\`` : ""}${type ? ` · $type=${type}` : ""}${matchSuffix} · ${activeTheme}`;
|
|
3148
3242
|
if (rows.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
3149
3243
|
...themeAttrs(cssVarPrefix, activeTheme),
|
|
3150
3244
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -3154,72 +3248,95 @@ function TokenTable({ filter, type, caption, sortBy = "path", sortDir = "asc", o
|
|
|
3154
3248
|
});
|
|
3155
3249
|
return /* @__PURE__ */ jsxs("div", {
|
|
3156
3250
|
...themeAttrs(cssVarPrefix, activeTheme),
|
|
3157
|
-
children: [
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
/* @__PURE__ */ jsx("
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
children: [/* @__PURE__ */ jsx("td", {
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3251
|
+
children: [
|
|
3252
|
+
searchable && /* @__PURE__ */ jsx("div", {
|
|
3253
|
+
className: "sb-token-table__search",
|
|
3254
|
+
children: /* @__PURE__ */ jsx("input", {
|
|
3255
|
+
type: "search",
|
|
3256
|
+
className: "sb-token-table__search-input",
|
|
3257
|
+
placeholder: "Search tokens…",
|
|
3258
|
+
value: query,
|
|
3259
|
+
onChange: (e) => setQuery(e.target.value),
|
|
3260
|
+
"aria-label": "Search tokens by path, type, or value",
|
|
3261
|
+
"data-testid": "token-table-search"
|
|
3262
|
+
})
|
|
3263
|
+
}),
|
|
3264
|
+
/* @__PURE__ */ jsxs("table", {
|
|
3265
|
+
className: "sb-token-table__table",
|
|
3266
|
+
children: [
|
|
3267
|
+
/* @__PURE__ */ jsx("caption", {
|
|
3268
|
+
className: "sb-token-table__caption",
|
|
3269
|
+
children: captionText
|
|
3270
|
+
}),
|
|
3271
|
+
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", {
|
|
3272
|
+
className: cx("sb-token-table__th", "sb-token-table__th--path"),
|
|
3273
|
+
children: "Path"
|
|
3274
|
+
}), /* @__PURE__ */ jsx("th", {
|
|
3275
|
+
className: cx("sb-token-table__th", "sb-token-table__th--value"),
|
|
3276
|
+
children: "Value"
|
|
3277
|
+
})] }) }),
|
|
3278
|
+
/* @__PURE__ */ jsxs("tbody", { children: [visibleRows.length === 0 && /* @__PURE__ */ jsx("tr", { children: /* @__PURE__ */ jsxs("td", {
|
|
3279
|
+
colSpan: 2,
|
|
3280
|
+
className: "sb-token-table__td sb-token-table__empty-row",
|
|
3281
|
+
children: [
|
|
3282
|
+
"No tokens match \"",
|
|
3283
|
+
query.trim(),
|
|
3284
|
+
"\"."
|
|
3285
|
+
]
|
|
3286
|
+
}) }), visibleRows.map((row) => /* @__PURE__ */ jsxs("tr", {
|
|
3287
|
+
className: "sb-token-table__row",
|
|
3288
|
+
onClick: () => handleRowClick(row.path),
|
|
3289
|
+
onKeyDown: (e) => {
|
|
3290
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
3291
|
+
e.preventDefault();
|
|
3292
|
+
handleRowClick(row.path);
|
|
3293
|
+
}
|
|
3294
|
+
},
|
|
3295
|
+
tabIndex: 0,
|
|
3296
|
+
"aria-label": `Inspect ${row.path}`,
|
|
3297
|
+
"data-testid": "token-table-row",
|
|
3298
|
+
"data-path": row.path,
|
|
3299
|
+
children: [/* @__PURE__ */ jsx("td", {
|
|
3300
|
+
className: cx("sb-token-table__td", "sb-token-table__path"),
|
|
3301
|
+
children: row.path
|
|
3302
|
+
}), /* @__PURE__ */ jsx("td", {
|
|
3303
|
+
className: "sb-token-table__td",
|
|
3304
|
+
children: /* @__PURE__ */ jsxs("span", {
|
|
3305
|
+
className: "sb-token-table__value-cell",
|
|
3306
|
+
children: [
|
|
3307
|
+
row.type && /* @__PURE__ */ jsx("span", {
|
|
3308
|
+
className: "sb-token-table__type-pill",
|
|
3309
|
+
children: row.type
|
|
3310
|
+
}),
|
|
3311
|
+
row.isColor && /* @__PURE__ */ jsx("span", {
|
|
3312
|
+
className: "sb-token-table__swatch",
|
|
3313
|
+
style: { background: row.cssVar },
|
|
3314
|
+
"aria-hidden": true
|
|
3315
|
+
}),
|
|
3316
|
+
/* @__PURE__ */ jsx("span", {
|
|
3317
|
+
className: "sb-token-table__value-text",
|
|
3318
|
+
title: row.value,
|
|
3319
|
+
"data-testid": "token-table-value",
|
|
3320
|
+
children: row.value
|
|
3321
|
+
}),
|
|
3322
|
+
row.outOfGamut && /* @__PURE__ */ jsx("span", {
|
|
3323
|
+
title: "Out of sRGB gamut for this format",
|
|
3324
|
+
"aria-label": "out of gamut",
|
|
3325
|
+
className: "sb-token-table__gamut-warn",
|
|
3326
|
+
children: "⚠"
|
|
3327
|
+
})
|
|
3328
|
+
]
|
|
3329
|
+
})
|
|
3330
|
+
})]
|
|
3331
|
+
}, row.path))] })
|
|
3332
|
+
]
|
|
3333
|
+
}),
|
|
3334
|
+
selectedPath !== null && /* @__PURE__ */ jsx(DetailOverlay, {
|
|
3335
|
+
path: selectedPath,
|
|
3336
|
+
onClose: () => setSelectedPath(null),
|
|
3337
|
+
testId: "token-table-overlay"
|
|
3338
|
+
})
|
|
3339
|
+
]
|
|
3223
3340
|
});
|
|
3224
3341
|
}
|
|
3225
3342
|
//#endregion
|