wenay-react2 1.0.36 → 1.0.38

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/README.md CHANGED
@@ -4,11 +4,12 @@ Documentation index only.
4
4
 
5
5
  - Brief API guide: [doc/wenay-react2.md](doc/wenay-react2.md)
6
6
  - Detailed / rare API guide: [doc/wenay-react2-rare.md](doc/wenay-react2-rare.md)
7
+ - Project functionality map: [doc/PROJECT_FUNCTIONALITY.md](doc/PROJECT_FUNCTIONALITY.md)
8
+ - Example usage standards: [doc/EXAMPLE_USAGE.md](doc/EXAMPLE_USAGE.md)
7
9
  - wenay-react2 rename map: [doc/WENAY_REACT2_RENAMES.md](doc/WENAY_REACT2_RENAMES.md)
8
- - Pulled wenay-common2 brief guide: [doc/wenay-common2.md](doc/wenay-common2.md)
9
- - Pulled wenay-common2 detailed / rare guide: [doc/wenay-common2-rare.md](doc/wenay-common2-rare.md)
10
- - Pulled wenay-common2 rename map: [doc/NAMING_RENAMES.md](doc/NAMING_RENAMES.md)
11
10
  - Project rules for maintainers and AI agents: [doc/PROJECT_RULES.md](doc/PROJECT_RULES.md)
12
11
  - Recent version changes: [doc/changes/](doc/changes/)
13
12
 
14
- Feature-specific docs may also live next to their code, for example agGrid4 docs under `src/common/src/grid/agGrid4/`.
13
+ `wenay-common2` is an external dependency. Read its current docs from the installed module/package when needed; keep this README focused on `wenay-react2` documentation.
14
+
15
+ Feature-specific docs may also live next to their code, for example agGrid4 docs under `src/common/src/grid/agGrid4/`.
@@ -29,7 +29,7 @@ export const ModalProvider = ({ children, closeOnOutsideClick = true, closeOnEsc
29
29
  return (_jsxs(ModalContext.Provider, { value: modalApi, children: [children, modal && createPortal(_jsx("div", { style: {
30
30
  position: 'fixed', inset: 0, zIndex: tokens.zIndex.modal,
31
31
  display: 'flex', alignItems: 'center', justifyContent: 'center',
32
- backgroundColor: 'rgba(0,0,0,0.5)'
32
+ backgroundColor: 'var(--dlg-scrim, rgba(0, 0, 0, 0.5))'
33
33
  }, children: _jsx(OutsideClickArea, { outsideClick: () => { if (closeOnOutsideClick)
34
34
  setModal(null); }, status: true, children: modal }) }), document.body)] }));
35
35
  };
@@ -1,19 +1,29 @@
1
1
  import React from "react";
2
+ export type SettingsSearchSource = React.ReactNode | readonly React.ReactNode[] | (() => React.ReactNode | readonly React.ReactNode[]);
2
3
  export type SettingsSection = {
3
4
  key: string;
4
5
  name: string;
5
6
  render: () => React.ReactNode;
7
+ /** Nested sections for the left settings tree. Flat sections remain roots. */
8
+ children?: SettingsSection[];
9
+ /** Attach this section under another section key, useful for registered external sections. */
10
+ parentKey?: string;
11
+ /** Extra searchable text for content that cannot be extracted from React children. */
12
+ searchText?: SettingsSearchSource;
13
+ /** Semantic aliases: commands, domain terms, translated labels, etc. */
14
+ keywords?: readonly string[];
6
15
  };
7
16
  /** Register an external section. Re-register with the same key replaces the previous one.
8
17
  * The returned function removes exactly this registration (a no-op if it was replaced). */
9
18
  export declare function registerSettingsSection(s: SettingsSection): () => void;
10
19
  /** Current external sections (static props sections are not included). */
11
20
  export declare function getSettingsSections(): readonly SettingsSection[];
12
- /** Centered settings dialog: sections column on the left, active section content on the right.
21
+ /** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
13
22
  * Sections = props.sections (first) + everything from registerSettingsSection.
23
+ * Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
14
24
  * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
15
25
  export declare function SettingsDialog(props: {
16
- trigger: React.ReactNode;
26
+ trigger?: React.ReactNode;
17
27
  sections?: SettingsSection[];
18
28
  defaultSection?: string;
19
29
  /** Section buttons: apps pass their own .chip / .chipActive; defaults are minimal library styles */
@@ -1,10 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
2
+ import React, { useEffect, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { renderBy, updateBy } from "../../../updateBy";
5
+ import { OutsideClickArea } from "../../hooks/useOutside";
6
+ import { FloatingWindowBase } from "../Dnd/FloatingWindow";
7
+ import { createSearchHistory } from "../../utils/searchHistory";
8
+ import { memoryGetOrCreate, memoryMarkDirty } from "../../utils/memoryStore";
9
+ const settingsDialogSize = { width: 820, height: 560 };
10
+ const settingsDialogPosition = { x: -settingsDialogSize.width / 2, y: -settingsDialogSize.height / 2 };
11
+ const settingsDialogNav = { min: 160, def: 220, max: 360 };
5
12
  // Module singleton (closure + updateBy subscription, no React context): any module
6
13
  // registers a section on mount and removes it on unmount; the dialog re-renders on changes.
7
14
  const registry = { list: [] };
15
+ const settingsSearchHistory = createSearchHistory({ key: "SettingsDialog.searchHistory", max: 8 });
16
+ const settingsDialogLayout = memoryGetOrCreate("SettingsDialog.layout", { navWidth: settingsDialogNav.def });
8
17
  /** Register an external section. Re-register with the same key replaces the previous one.
9
18
  * The returned function removes exactly this registration (a no-op if it was replaced). */
10
19
  export function registerSettingsSection(s) {
@@ -26,30 +35,406 @@ export function registerSettingsSection(s) {
26
35
  export function getSettingsSections() {
27
36
  return registry.list;
28
37
  }
29
- /** Centered settings dialog: sections column on the left, active section content on the right.
38
+ function classNames(parts) {
39
+ return parts.filter(Boolean).join(" ");
40
+ }
41
+ function clampSettingsNavWidth(value) {
42
+ return Math.max(settingsDialogNav.min, Math.min(settingsDialogNav.max, Math.round(value)));
43
+ }
44
+ function DefaultSettingsTrigger() {
45
+ return _jsx("span", { className: "wenayTb wenayDlgTriggerBar", title: "Open settings", "aria-hidden": "true", children: _jsx("span", { className: "wenayTbItem wenayDlgTriggerItem", children: _jsxs("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: [_jsx("circle", { cx: "3", cy: "7", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "7", cy: "7", r: "1.25", fill: "currentColor" }), _jsx("circle", { cx: "11", cy: "7", r: "1.25", fill: "currentColor" })] }) }) });
46
+ }
47
+ function buildSettingsTree(sections) {
48
+ const roots = [];
49
+ const ordered = [];
50
+ const byKey = new Map();
51
+ const parentByKey = new Map();
52
+ function collect(section, nestedParentKey) {
53
+ if (byKey.has(section.key))
54
+ return;
55
+ const node = { section, children: [], depth: 0 };
56
+ byKey.set(section.key, node);
57
+ ordered.push(node);
58
+ parentByKey.set(section.key, section.parentKey ?? nestedParentKey);
59
+ section.children?.forEach(child => collect(child, section.key));
60
+ }
61
+ sections.forEach(section => collect(section));
62
+ ordered.forEach(node => {
63
+ const parentKey = parentByKey.get(node.section.key);
64
+ const parent = parentKey == null ? undefined : byKey.get(parentKey);
65
+ if (parent != null && parent != node) {
66
+ node.parent = parent;
67
+ parent.children.push(node);
68
+ }
69
+ else {
70
+ roots.push(node);
71
+ }
72
+ });
73
+ function assignDepth(nodes, depth) {
74
+ nodes.forEach(node => {
75
+ node.depth = depth;
76
+ assignDepth(node.children, depth + 1);
77
+ });
78
+ }
79
+ assignDepth(roots, 0);
80
+ return { roots, ordered, byKey };
81
+ }
82
+ function getTreeSignature(nodes) {
83
+ return nodes.map(node => `${node.section.key}:${node.parent?.section.key ?? ""}:${node.children.map(child => child.section.key).join(",")}`).join("|");
84
+ }
85
+ function getBranchKeys(nodes) {
86
+ return nodes.filter(node => node.children.length != 0).map(node => node.section.key);
87
+ }
88
+ function normalizeSearch(value) {
89
+ return value.toLocaleLowerCase().replace(/\s+/g, " ").trim();
90
+ }
91
+ const enKeyboard = "`qwertyuiop[]asdfghjkl;'zxcvbnm,./";
92
+ const ruKeyboard = "ёйцукенгшщзхъфывапролджэячсмитьбю.";
93
+ const ruToEnKeyboard = new Map(Array.from(ruKeyboard).map((char, i) => [char, enKeyboard[i]]));
94
+ const enToRuKeyboard = new Map(Array.from(enKeyboard).map((char, i) => [char, ruKeyboard[i]]));
95
+ function convertKeyboardLayout(value, map) {
96
+ return Array.from(value).map(char => map.get(char) ?? char).join("");
97
+ }
98
+ function uniqueSearchVariants(token) {
99
+ return Array.from(new Set([
100
+ token,
101
+ convertKeyboardLayout(token, ruToEnKeyboard),
102
+ convertKeyboardLayout(token, enToRuKeyboard),
103
+ ].filter(Boolean)));
104
+ }
105
+ function getSearchTerms(value) {
106
+ return normalizeSearch(value).split(" ").filter(Boolean).map(text => ({
107
+ variants: uniqueSearchVariants(text),
108
+ }));
109
+ }
110
+ function isIterableNode(value) {
111
+ return typeof value == "object" && value != null && Symbol.iterator in value;
112
+ }
113
+ function reactNodeText(node) {
114
+ if (node == null || typeof node == "boolean")
115
+ return "";
116
+ if (typeof node == "string" || typeof node == "number" || typeof node == "bigint")
117
+ return String(node);
118
+ if (Array.isArray(node))
119
+ return node.map(reactNodeText).join(" ");
120
+ if (React.isValidElement(node)) {
121
+ const props = node.props;
122
+ return [props.title, props["aria-label"], props.label, props.children].map(reactNodeText).join(" ");
123
+ }
124
+ if (isIterableNode(node))
125
+ return Array.from(node).map(reactNodeText).join(" ");
126
+ return "";
127
+ }
128
+ function searchSourceText(source) {
129
+ const value = typeof source == "function" ? source() : source;
130
+ return reactNodeText(value);
131
+ }
132
+ function sectionSearchText(section) {
133
+ const parts = [section.key, section.name, ...(section.keywords ?? [])];
134
+ if (section.searchText != null)
135
+ parts.push(searchSourceText(section.searchText));
136
+ try {
137
+ parts.push(reactNodeText(section.render()));
138
+ }
139
+ catch {
140
+ // Search should not break the dialog if a consumer render throws outside its normal path.
141
+ }
142
+ return normalizeSearch(parts.join(" "));
143
+ }
144
+ function sectionMatches(section, terms) {
145
+ const text = sectionSearchText(section);
146
+ return terms.every(term => term.variants.some(variant => text.includes(variant)));
147
+ }
148
+ function getCurrentBranchKeys(node) {
149
+ const keys = new Set();
150
+ let current = node;
151
+ while (current != null) {
152
+ if (current.children.length != 0)
153
+ keys.add(current.section.key);
154
+ current = current.parent;
155
+ }
156
+ return keys;
157
+ }
158
+ function getLabelMatchRanges(label, terms) {
159
+ const lowerLabel = label.toLocaleLowerCase();
160
+ const ranges = [];
161
+ terms.forEach(term => {
162
+ const match = term.variants.reduce((best, variant) => {
163
+ const start = lowerLabel.indexOf(variant);
164
+ if (start == -1)
165
+ return best;
166
+ const range = { start, end: start + variant.length };
167
+ if (best == null || range.start < best.start || (range.start == best.start && range.end > best.end))
168
+ return range;
169
+ return best;
170
+ }, undefined);
171
+ if (match != null)
172
+ ranges.push(match);
173
+ });
174
+ ranges.sort((a, b) => a.start - b.start || b.end - a.end);
175
+ return ranges.reduce((acc, range) => {
176
+ const last = acc[acc.length - 1];
177
+ if (last == null || range.start > last.end)
178
+ acc.push({ ...range });
179
+ else if (range.end > last.end)
180
+ last.end = range.end;
181
+ return acc;
182
+ }, []);
183
+ }
184
+ function renderHighlightedLabel(label, terms) {
185
+ const ranges = getLabelMatchRanges(label, terms);
186
+ if (ranges.length == 0)
187
+ return label;
188
+ const parts = [];
189
+ let offset = 0;
190
+ ranges.forEach((range, i) => {
191
+ if (range.start > offset)
192
+ parts.push(label.slice(offset, range.start));
193
+ parts.push(_jsx("mark", { className: "wenayDlgTreeMark", children: label.slice(range.start, range.end) }, `${range.start}-${range.end}-${i}`));
194
+ offset = range.end;
195
+ });
196
+ if (offset < label.length)
197
+ parts.push(label.slice(offset));
198
+ return parts;
199
+ }
200
+ function filterSettingsTree(roots, terms) {
201
+ const visibleKeys = new Set();
202
+ const matchedKeys = new Set();
203
+ const expandedKeys = new Set();
204
+ function visit(node) {
205
+ const selfMatches = terms.length == 0 || sectionMatches(node.section, terms);
206
+ let childVisible = false;
207
+ node.children.forEach(child => {
208
+ if (visit(child))
209
+ childVisible = true;
210
+ });
211
+ const visible = terms.length == 0 || selfMatches || childVisible;
212
+ if (visible)
213
+ visibleKeys.add(node.section.key);
214
+ if (terms.length != 0 && selfMatches)
215
+ matchedKeys.add(node.section.key);
216
+ if (terms.length != 0 && childVisible)
217
+ expandedKeys.add(node.section.key);
218
+ return visible;
219
+ }
220
+ roots.forEach(visit);
221
+ return { visibleKeys, matchedKeys, expandedKeys };
222
+ }
223
+ /** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
30
224
  * Sections = props.sections (first) + everything from registerSettingsSection.
225
+ * Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
31
226
  * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
32
227
  export function SettingsDialog(props) {
33
228
  const [open, setOpen] = useState(false);
34
229
  const [active, setActive] = useState(props.defaultSection);
230
+ const [search, setSearch] = useState("");
231
+ const [expanded, setExpanded] = useState(() => new Set());
232
+ const [historyOpen, setHistoryOpen] = useState(false);
233
+ const [navWidth, setNavWidth] = useState(() => clampSettingsNavWidth(settingsDialogLayout.navWidth));
234
+ const [navResizing, setNavResizing] = useState(false);
235
+ const searchInputRef = useRef(null);
236
+ const navResizeRef = useRef(null);
237
+ const navWidthRef = useRef(navWidth);
35
238
  updateBy(registry);
239
+ const searchHistory = settingsSearchHistory.use();
240
+ const sections = [...(props.sections ?? []), ...registry.list];
241
+ const tree = buildSettingsTree(sections);
242
+ const treeSignature = getTreeSignature(tree.ordered);
243
+ const branchKeys = getBranchKeys(tree.ordered);
244
+ const searchTerms = getSearchTerms(search);
245
+ const filtered = filterSettingsTree(tree.roots, searchTerms);
246
+ const activeNode = active == null ? undefined : tree.byKey.get(active);
247
+ const defaultNode = props.defaultSection == null ? undefined : tree.byKey.get(props.defaultSection);
248
+ const firstVisibleNode = tree.ordered.find(node => filtered.visibleKeys.has(node.section.key));
249
+ const firstMatchedNode = tree.ordered.find(node => filtered.matchedKeys.has(node.section.key));
250
+ const fallbackNode = defaultNode ?? tree.ordered[0];
251
+ const currentNode = searchTerms.length != 0
252
+ ? (activeNode != null && filtered.matchedKeys.has(activeNode.section.key) ? activeNode : firstMatchedNode ?? firstVisibleNode ?? activeNode ?? fallbackNode)
253
+ : activeNode ?? fallbackNode;
254
+ const current = currentNode?.section;
255
+ const base = props.sectionClassName ?? "wenayDlgSection";
256
+ const activeCls = props.sectionActiveClassName ?? "wenayDlgSectionActive";
257
+ const branchOpenCount = branchKeys.filter(key => expanded.has(key)).length;
258
+ const treeToolsVisible = branchKeys.length > 1;
259
+ const treeToolState = branchOpenCount == 0 ? "collapsed" : branchOpenCount == branchKeys.length ? "expanded" : "branch";
260
+ const treeToolTitle = treeToolState == "expanded" ? "Show current branch only" : treeToolState == "branch" ? "Collapse tree" : "Expand tree";
261
+ useEffect(() => {
262
+ navWidthRef.current = navWidth;
263
+ }, [navWidth]);
264
+ useEffect(() => {
265
+ if (!open)
266
+ return;
267
+ setExpanded(new Set(branchKeys));
268
+ }, [open, treeSignature]);
269
+ useEffect(() => {
270
+ if (!navResizing)
271
+ return;
272
+ const onPointerMove = (e) => {
273
+ const drag = navResizeRef.current;
274
+ if (drag == null)
275
+ return;
276
+ setNavWidth(clampSettingsNavWidth(drag.startWidth + e.clientX - drag.startX));
277
+ };
278
+ const onPointerUp = () => {
279
+ commitNavWidth(navWidthRef.current);
280
+ navResizeRef.current = null;
281
+ setNavResizing(false);
282
+ };
283
+ document.addEventListener("pointermove", onPointerMove);
284
+ document.addEventListener("pointerup", onPointerUp);
285
+ return () => {
286
+ document.removeEventListener("pointermove", onPointerMove);
287
+ document.removeEventListener("pointerup", onPointerUp);
288
+ };
289
+ }, [navResizing]);
36
290
  useEffect(() => {
37
291
  if (!open)
38
292
  return;
39
293
  const onKeyDown = (e) => {
40
- if (e.key == 'Escape')
41
- setOpen(false);
294
+ if (e.key != "Escape" || e.isComposing)
295
+ return;
296
+ if (search != "") {
297
+ e.preventDefault();
298
+ setSearch("");
299
+ searchInputRef.current?.focus();
300
+ return;
301
+ }
302
+ setOpen(false);
42
303
  };
43
- document.addEventListener('keydown', onKeyDown);
44
- return () => document.removeEventListener('keydown', onKeyDown);
45
- }, [open]);
46
- const sections = [...(props.sections ?? []), ...registry.list];
47
- // fall back to the first section when active was never set or its section unmounted
48
- const current = sections.find(s => s.key == active) ?? sections[0];
49
- const base = props.sectionClassName ?? "wenayDlgSection";
50
- const activeCls = props.sectionActiveClassName ?? "wenayDlgSectionActive";
51
- return _jsxs(_Fragment, { children: [_jsx("span", { onClick: () => setOpen(true), style: { display: "inline-block", cursor: "pointer" }, children: props.trigger }), open && createPortal(_jsx("div", { className: "wenayDlgScrim", onClick: e => {
52
- if (e.target == e.currentTarget)
53
- setOpen(false);
54
- }, children: _jsxs("div", { className: "wenayDlg", children: [_jsx("div", { className: "wenayDlgNav", children: sections.map(s => (_jsx("div", { className: s.key == current?.key ? `${base} ${activeCls}` : base, onClick: () => setActive(s.key), children: s.name }, s.key))) }), _jsx("div", { className: "wenayDlgContent", children: current?.render() }), _jsx("div", { className: "wenayCloseBtn wenayDlgClose", title: "Close", onClick: () => setOpen(false), children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2 2 L10 10 M10 2 L2 10", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) })] }) }), document.body)] });
304
+ document.addEventListener("keydown", onKeyDown);
305
+ return () => document.removeEventListener("keydown", onKeyDown);
306
+ }, [open, search]);
307
+ function openDialog() {
308
+ setSearch("");
309
+ setHistoryOpen(false);
310
+ setOpen(true);
311
+ }
312
+ function onTriggerKeyDown(e) {
313
+ if (e.key != "Enter" && e.key != " ")
314
+ return;
315
+ e.preventDefault();
316
+ openDialog();
317
+ }
318
+ function toggleExpanded(key) {
319
+ setExpanded(prev => {
320
+ const next = new Set(prev);
321
+ if (next.has(key))
322
+ next.delete(key);
323
+ else
324
+ next.add(key);
325
+ return next;
326
+ });
327
+ }
328
+ function expandAll() {
329
+ setExpanded(new Set(branchKeys));
330
+ }
331
+ function collapseAll() {
332
+ setExpanded(new Set());
333
+ }
334
+ function collapseOutsideCurrent() {
335
+ setExpanded(getCurrentBranchKeys(currentNode));
336
+ }
337
+ function commitSearch(value = search) {
338
+ settingsSearchHistory.add(value);
339
+ setHistoryOpen(false);
340
+ }
341
+ function commitNavWidth(value) {
342
+ const next = clampSettingsNavWidth(value);
343
+ settingsDialogLayout.navWidth = next;
344
+ setNavWidth(next);
345
+ renderBy(settingsDialogLayout);
346
+ memoryMarkDirty("SettingsDialog.layout");
347
+ }
348
+ function beginNavResize(e) {
349
+ e.preventDefault();
350
+ navResizeRef.current = { startX: e.clientX, startWidth: navWidth };
351
+ setHistoryOpen(false);
352
+ setNavResizing(true);
353
+ }
354
+ function onNavResizeKeyDown(e) {
355
+ if (e.key == "ArrowLeft") {
356
+ e.preventDefault();
357
+ commitNavWidth(navWidth - 16);
358
+ }
359
+ else if (e.key == "ArrowRight") {
360
+ e.preventDefault();
361
+ commitNavWidth(navWidth + 16);
362
+ }
363
+ else if (e.key == "Home") {
364
+ e.preventDefault();
365
+ commitNavWidth(settingsDialogNav.min);
366
+ }
367
+ else if (e.key == "End") {
368
+ e.preventDefault();
369
+ commitNavWidth(settingsDialogNav.max);
370
+ }
371
+ else if (e.key == "Enter" || e.key == " ") {
372
+ e.preventDefault();
373
+ commitNavWidth(settingsDialogNav.def);
374
+ }
375
+ }
376
+ function useHistoryItem(value) {
377
+ setSearch(value);
378
+ settingsSearchHistory.add(value);
379
+ setHistoryOpen(false);
380
+ searchInputRef.current?.focus();
381
+ }
382
+ function cycleTreeTool() {
383
+ if (treeToolState == "expanded")
384
+ collapseOutsideCurrent();
385
+ else if (treeToolState == "branch")
386
+ collapseAll();
387
+ else
388
+ expandAll();
389
+ }
390
+ function renderTreeNode(node) {
391
+ if (!filtered.visibleKeys.has(node.section.key))
392
+ return null;
393
+ const key = node.section.key;
394
+ const hasChildren = node.children.length != 0;
395
+ const isOpen = expanded.has(key) || (searchTerms.length != 0 && filtered.expandedKeys.has(key));
396
+ const isActive = key == current?.key;
397
+ const rowClass = classNames(["wenayDlgTreeRow", base, isActive && activeCls]);
398
+ function onRowKeyDown(e) {
399
+ if (e.key == "Enter" || e.key == " ") {
400
+ e.preventDefault();
401
+ setActive(key);
402
+ }
403
+ if (e.key == "ArrowRight" && hasChildren && !isOpen) {
404
+ e.preventDefault();
405
+ toggleExpanded(key);
406
+ }
407
+ if (e.key == "ArrowLeft" && hasChildren && isOpen) {
408
+ e.preventDefault();
409
+ toggleExpanded(key);
410
+ }
411
+ }
412
+ return _jsxs(React.Fragment, { children: [_jsxs("div", { role: "treeitem", "aria-level": node.depth + 1, "aria-expanded": hasChildren ? isOpen : undefined, "aria-selected": isActive, tabIndex: 0, className: rowClass, style: { paddingLeft: 6 + node.depth * 14 }, onClick: () => setActive(key), onKeyDown: onRowKeyDown, children: [_jsx("button", { type: "button", className: classNames(["wenayDlgTreeToggle", !hasChildren && "wenayDlgTreeToggleEmpty", isOpen && "wenayDlgTreeToggleOpen"]), disabled: !hasChildren, "aria-label": isOpen ? "Collapse" : "Expand", onClick: e => {
413
+ e.stopPropagation();
414
+ if (hasChildren)
415
+ toggleExpanded(key);
416
+ } }), _jsx("span", { className: "wenayDlgTreeLabel", children: renderHighlightedLabel(node.section.name, searchTerms) })] }), hasChildren && isOpen && node.children.map(renderTreeNode)] }, key);
417
+ }
418
+ return _jsxs(_Fragment, { children: [_jsx("span", { role: "button", tabIndex: 0, "aria-label": props.trigger == null ? "Open settings" : undefined, onClick: openDialog, onKeyDown: onTriggerKeyDown, style: { display: "inline-block", cursor: "pointer" }, children: props.trigger ?? _jsx(DefaultSettingsTrigger, {}) }), open && createPortal(_jsx("div", { className: "wenayDlgScrim", children: _jsx(OutsideClickArea, { outsideClick: () => setOpen(false), status: open, className: "wenayDlgOutside", children: _jsx(FloatingWindowBase, { className: "wenayDlgWindow", size: settingsDialogSize, position: settingsDialogPosition, zIndex: 10000, moveOnlyHeader: true, overflow: false, onCLickClose: () => setOpen(false), header: _jsx("div", { className: "wenayDlgHeader", children: "Settings" }), children: _jsxs("div", { className: classNames(["wenayDlg", navResizing && "wenayDlg_resizing"]), children: [_jsxs("div", { className: "wenayDlgNav", style: { width: navWidth }, children: [_jsxs("div", { className: "wenayDlgNavTop", children: [_jsxs("div", { className: "wenayDlgSearchBox", children: [_jsx("input", { ref: searchInputRef, className: "wenayDlgSearch", value: search, placeholder: "Search", "aria-label": "Search settings", onFocus: () => setHistoryOpen(searchHistory.length != 0), onChange: e => {
419
+ setSearch(e.currentTarget.value);
420
+ setHistoryOpen(searchHistory.length != 0);
421
+ }, onKeyDown: e => {
422
+ if (e.key == "Enter")
423
+ commitSearch();
424
+ if (e.key == "ArrowDown" && searchHistory[0]) {
425
+ e.preventDefault();
426
+ useHistoryItem(searchHistory[0]);
427
+ }
428
+ } }), searchHistory.length != 0 && _jsx("button", { type: "button", className: "wenayDlgSearchHistoryBtn", title: "Search history", "aria-label": "Search history", "aria-expanded": historyOpen, onClick: () => {
429
+ setHistoryOpen(v => !v);
430
+ searchInputRef.current?.focus();
431
+ }, children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: _jsx("path", { d: "M7 2a5 5 0 1 1-4.4 2.6M2 2v3h3M7 4v3l2 1" }) }) }), search != "" && _jsx("button", { type: "button", className: "wenayDlgSearchClear", title: "Clear search", "aria-label": "Clear search", onClick: () => {
432
+ setSearch("");
433
+ setHistoryOpen(searchHistory.length != 0);
434
+ searchInputRef.current?.focus();
435
+ }, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2 2 L10 10 M10 2 L2 10", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) }), historyOpen && searchHistory.length != 0 && _jsxs("div", { className: "wenayDlgSearchHistory", role: "listbox", children: [searchHistory.map(item => _jsx("button", { type: "button", className: "wenayDlgSearchHistoryItem", onMouseDown: e => e.preventDefault(), onClick: () => useHistoryItem(item), children: item }, item)), _jsx("button", { type: "button", className: "wenayDlgSearchHistoryClear", onMouseDown: e => e.preventDefault(), onClick: () => {
436
+ settingsSearchHistory.clear();
437
+ setHistoryOpen(false);
438
+ searchInputRef.current?.focus();
439
+ }, children: "Clear history" })] })] }), treeToolsVisible && _jsx("button", { className: classNames(["wenayDlgTreeTool", `wenayDlgTreeTool_${treeToolState}`]), type: "button", title: treeToolTitle, "aria-label": treeToolTitle, onClick: cycleTreeTool, children: _jsxs("span", { className: "wenayDlgTreeToolDots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }) })] }), _jsxs("div", { className: "wenayDlgTree", role: "tree", children: [tree.roots.map(renderTreeNode), firstVisibleNode == null && _jsx("div", { className: "wenayDlgNoResults", children: "No settings found" })] })] }), _jsx("div", { className: "wenayDlgDivider", role: "separator", "aria-orientation": "vertical", "aria-label": "Resize settings navigation", "aria-valuemin": settingsDialogNav.min, "aria-valuemax": settingsDialogNav.max, "aria-valuenow": navWidth, tabIndex: 0, title: "Drag to resize navigation; double-click to reset", onPointerDown: beginNavResize, onDoubleClick: () => commitNavWidth(settingsDialogNav.def), onKeyDown: onNavResizeKeyDown, children: _jsx("span", {}) }), _jsx("div", { className: "wenayDlgContent", children: current?.render() })] }) }) }) }), document.body)] });
55
440
  }
@@ -42,10 +42,11 @@ export type UiListConfig = {
42
42
  [key: string]: boolean;
43
43
  };
44
44
  };
45
+ export type ToolbarSourceMode = 'orderVisible' | 'order';
45
46
  /** An external owner of order/visibility that a Toolbar can run on instead of
46
47
  * its own store - e.g. columnState's listSource: the SAME config the grid
47
48
  * syncs with, so dragging a column in the grid reorders the toolbar and
48
- * vice versa. Density and the gear flag stay toolbar-local (persisted under
49
+ * vice versa. Density and the gear/reset flags stay toolbar-local (persisted under
49
50
  * the toolbar's own key). useConfig must be a React hook (subscription). */
50
51
  export type UiListSource = {
51
52
  useConfig: () => UiListConfig;
@@ -87,16 +88,29 @@ export declare function createToolbar(opts: {
87
88
  title?: string;
88
89
  icon?: React.ReactNode;
89
90
  };
91
+ /** reset button face. The reset feature exists by default, but the bar
92
+ * button is hidden until Settings/showReset enables it; pass false to
93
+ * remove the feature. */
94
+ resetItem?: false | {
95
+ title?: string;
96
+ icon?: React.ReactNode;
97
+ defaultVisible?: boolean;
98
+ };
90
99
  /** external owner of order/visibility (columnState.api.listSource etc.):
91
100
  * the toolbar becomes a VIEW over that config - Bar/Settings edit it, and
92
101
  * outside changes (a column dragged in the grid) reorder the toolbar.
93
- * Item keys should match the source's keys 1:1. Density and the gear flag
102
+ * Item keys should match the source's keys 1:1. Density and the gear/reset flags
94
103
  * remain in the toolbar's own store under `key`. Default: own store. */
95
104
  source?: UiListSource;
105
+ /** Default 'orderVisible': source owns both item order and item visibility.
106
+ * 'order': source owns only the relative order of source keys; item membership,
107
+ * density, pseudo-controls, and extra non-source item positions stay local. */
108
+ sourceMode?: ToolbarSourceMode;
96
109
  }): {
97
110
  Bar: (p?: {
98
111
  className?: string;
99
112
  settings?: boolean;
113
+ reset?: boolean;
100
114
  popAlign?: "left" | "right";
101
115
  }) => import("react/jsx-runtime").JSX.Element;
102
116
  Settings: (p?: {
@@ -112,6 +126,11 @@ export declare function createToolbar(opts: {
112
126
  }[];
113
127
  getConfig: () => ToolbarConfig;
114
128
  setConfig: (next: ToolbarConfig) => void;
129
+ setOrder: (order: string[]) => void;
130
+ show: (key: string, on: boolean) => void;
131
+ setDensity: (density: string) => void;
132
+ showSettings: (on: boolean) => void;
133
+ showReset: (on: boolean) => void;
115
134
  reset: () => void;
116
135
  onChange: import("wenay-common2").ListenApi<[ToolbarConfig]>;
117
136
  };