wenay-react2 1.0.37 → 1.0.39

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.
Files changed (34) hide show
  1. package/README.md +2 -0
  2. package/lib/common/src/components/Input.d.ts +25 -6
  3. package/lib/common/src/components/Input.js +27 -8
  4. package/lib/common/src/components/Modal/ModalContextProvider.js +1 -1
  5. package/lib/common/src/components/Settings/SettingsDialog.d.ts +12 -2
  6. package/lib/common/src/components/Settings/SettingsDialog.js +412 -16
  7. package/lib/common/src/components/Toolbar/Toolbar.d.ts +21 -2
  8. package/lib/common/src/components/Toolbar/Toolbar.js +84 -14
  9. package/lib/common/src/grid/columnState/CardList.js +5 -2
  10. package/lib/common/src/grid/columnState/ColumnDots.d.ts +1 -1
  11. package/lib/common/src/grid/columnState/ColumnDots.js +9 -19
  12. package/lib/common/src/grid/columnState/ColumnsMenu.js +10 -18
  13. package/lib/common/src/grid/columnState/columnState.d.ts +4 -0
  14. package/lib/common/src/grid/columnState/columnState.js +39 -14
  15. package/lib/common/src/hooks/useReplay.d.ts +54 -0
  16. package/lib/common/src/hooks/useReplay.js +222 -0
  17. package/lib/common/src/logs/logStyles.d.ts +13 -0
  18. package/lib/common/src/logs/logStyles.js +18 -0
  19. package/lib/common/src/logs/logs.js +5 -7
  20. package/lib/common/src/logs/logsContext.js +22 -26
  21. package/lib/common/src/menu/menu.d.ts +2 -1
  22. package/lib/common/src/menu/menu.js +28 -21
  23. package/lib/common/src/menu/menuMouse.d.ts +17 -0
  24. package/lib/common/src/menu/menuMouse.js +70 -3
  25. package/lib/common/src/styles/tokens.d.ts +92 -1
  26. package/lib/common/src/styles/tokens.js +92 -1
  27. package/lib/common/src/utils/index.d.ts +1 -0
  28. package/lib/common/src/utils/index.js +1 -0
  29. package/lib/common/src/utils/searchHistory.d.ts +14 -0
  30. package/lib/common/src/utils/searchHistory.js +59 -0
  31. package/lib/style/menuRight.css +2 -2
  32. package/lib/style/style.css +634 -53
  33. package/lib/style/tokens.css +90 -4
  34. package/package.json +2 -2
package/README.md CHANGED
@@ -4,6 +4,8 @@ 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
10
  - Project rules for maintainers and AI agents: [doc/PROJECT_RULES.md](doc/PROJECT_RULES.md)
9
11
  - Recent version changes: [doc/changes/](doc/changes/)
@@ -1,21 +1,40 @@
1
1
  import React from "react";
2
- export declare function TextInputPanel({ callback, name, txt }: {
2
+ export type TextInputPanelProps = {
3
3
  callback: (txt: string) => void;
4
4
  name?: string;
5
5
  txt?: string;
6
- }): import("react/jsx-runtime").JSX.Element;
7
- export declare function TextInputModal({ callback, name, outClick, keyForSave, txt }: Parameters<typeof TextInputPanel>[0] & {
6
+ };
7
+ export declare function useTextInputPanel({ callback, txt }: Pick<TextInputPanelProps, "callback" | "txt">): {
8
+ getValue: () => string;
9
+ setValue: (next: string) => void;
10
+ submit: () => void;
11
+ inputProps: {
12
+ defaultValue: string;
13
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
14
+ };
15
+ };
16
+ export declare function TextInputPanel({ callback, name, txt }: TextInputPanelProps): import("react/jsx-runtime").JSX.Element;
17
+ export declare function TextInputModal({ callback, name, outClick, keyForSave, txt }: TextInputPanelProps & {
8
18
  outClick: () => any;
9
19
  keyForSave?: string;
10
20
  }): import("react/jsx-runtime").JSX.Element;
11
- export declare function FileInputModal({ callback, name, outClick, keyForSave }: Parameters<typeof FileInputPanel>[0] & {
21
+ export declare function FileInputModal({ callback, name, outClick, keyForSave }: FileInputPanelProps & {
12
22
  outClick: () => any;
13
23
  keyForSave?: string;
14
24
  }): import("react/jsx-runtime").JSX.Element;
15
- export declare function FileInputPanel({ callback, name }: {
25
+ export type FileInputPanelProps = {
16
26
  callback: (file: File | null) => void;
17
27
  name?: string;
18
- }): import("react/jsx-runtime").JSX.Element;
28
+ };
29
+ export declare function useFileInputPanel({ callback }: Pick<FileInputPanelProps, "callback">): {
30
+ getFile: () => File | null;
31
+ setFile: (next: File | null) => void;
32
+ submit: () => void;
33
+ inputProps: {
34
+ onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
35
+ };
36
+ };
37
+ export declare function FileInputPanel({ callback, name }: FileInputPanelProps): import("react/jsx-runtime").JSX.Element;
19
38
  export declare function FreeModal({ outClick, children, zIndex, size, keyForSave }: {
20
39
  zIndex?: number;
21
40
  outClick: () => any;
@@ -7,11 +7,21 @@ function ModalWrapper({ outClick, children, zIndex, size = { height: 150, width:
7
7
  const defaultPosition = position ?? { y: -(size.height / 2), x: -(size.width / 2) };
8
8
  return _jsx(OutsideClickArea, { outsideClick: outClick, style: { position: "absolute", top: "50%", left: "50%" }, children: _jsx(FloatingWindow, { keyForSave: keyForSave, size: size, zIndex: zIndex, position: defaultPosition, className: "fon border fonLight", moveOnlyHeader: true, children: children }) });
9
9
  }
10
- export function TextInputPanel({ callback, name = "", txt = "" }) {
10
+ export function useTextInputPanel({ callback, txt = "" }) {
11
11
  const txtName = useRef(txt);
12
- return _jsxs("div", { className: "maxSize", style: { padding: 20, }, children: [_jsx("label", { children: name }), _jsx("input", { type: "text", style: { width: "100%" }, defaultValue: txtName.current, onChange: (e) => {
13
- txtName.current = e.target.value;
14
- } }), _jsx("div", { style: { marginTop: 20 }, className: "msTradeAlt msTradeActive", onClick: () => { callback(txtName.current); }, children: "send" })] });
12
+ return {
13
+ getValue: () => txtName.current,
14
+ setValue: (next) => { txtName.current = next; },
15
+ submit: () => callback(txtName.current),
16
+ inputProps: {
17
+ defaultValue: txtName.current,
18
+ onChange: (e) => { txtName.current = e.target.value; },
19
+ },
20
+ };
21
+ }
22
+ export function TextInputPanel({ callback, name = "", txt = "" }) {
23
+ const input = useTextInputPanel({ callback, txt });
24
+ return _jsxs("div", { className: "maxSize", style: { padding: 20, }, children: [_jsx("label", { children: name }), _jsx("input", { type: "text", style: { width: "100%" }, ...input.inputProps }), _jsx("div", { style: { marginTop: 20 }, className: "msTradeAlt msTradeActive", onClick: input.submit, children: "send" })] });
15
25
  }
16
26
  export function TextInputModal({ callback, name, outClick, keyForSave = "TextInputModal", txt }) {
17
27
  return _jsx(ModalWrapper, { outClick: outClick, keyForSave: keyForSave, size: { height: 150, width: 300 }, position: { y: -150, x: -250 }, children: _jsx(TextInputPanel, { callback: callback, name: name, txt: txt }) });
@@ -19,11 +29,20 @@ export function TextInputModal({ callback, name, outClick, keyForSave = "TextInp
19
29
  export function FileInputModal({ callback, name, outClick, keyForSave = "FileInputModal" }) {
20
30
  return _jsx(ModalWrapper, { outClick: outClick, keyForSave: keyForSave, size: { height: 150, width: 300 }, position: { y: -150, x: -250 }, children: _jsx(FileInputPanel, { callback: callback, name: name }) });
21
31
  }
22
- export function FileInputPanel({ callback, name = "" }) {
32
+ export function useFileInputPanel({ callback }) {
23
33
  const file = useRef(null);
24
- return _jsxs("div", { className: "maxSize", style: { padding: 20, }, children: [_jsx("label", { children: name }), _jsx("input", { type: "file", style: { width: "100%" }, onChange: (e) => {
25
- file.current = e.target.files?.[0] ?? null;
26
- } }), _jsx("div", { style: { marginTop: 20 }, className: "msTradeAlt msTradeActive", onClick: () => { callback(file.current); }, children: "send" })] });
34
+ return {
35
+ getFile: () => file.current,
36
+ setFile: (next) => { file.current = next; },
37
+ submit: () => callback(file.current),
38
+ inputProps: {
39
+ onChange: (e) => { file.current = e.target.files?.[0] ?? null; },
40
+ },
41
+ };
42
+ }
43
+ export function FileInputPanel({ callback, name = "" }) {
44
+ const input = useFileInputPanel({ callback });
45
+ return _jsxs("div", { className: "maxSize", style: { padding: 20, }, children: [_jsx("label", { children: name }), _jsx("input", { type: "file", style: { width: "100%" }, ...input.inputProps }), _jsx("div", { style: { marginTop: 20 }, className: "msTradeAlt msTradeActive", onClick: input.submit, children: "send" })] });
27
46
  }
28
47
  export function FreeModal({ outClick, children, zIndex, size = { height: 150, width: 300 }, keyForSave = "FreeModal" }) {
29
48
  return _jsx(ModalWrapper, { outClick: outClick, keyForSave: keyForSave, size: size, zIndex: zIndex, children: children });
@@ -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,417 @@ 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 searchBoxRef = useRef(null);
237
+ const navResizeRef = useRef(null);
238
+ const navWidthRef = useRef(navWidth);
35
239
  updateBy(registry);
240
+ const searchHistory = settingsSearchHistory.use();
241
+ const sections = [...(props.sections ?? []), ...registry.list];
242
+ const tree = buildSettingsTree(sections);
243
+ const treeSignature = getTreeSignature(tree.ordered);
244
+ const branchKeys = getBranchKeys(tree.ordered);
245
+ const searchTerms = getSearchTerms(search);
246
+ const filtered = filterSettingsTree(tree.roots, searchTerms);
247
+ const activeNode = active == null ? undefined : tree.byKey.get(active);
248
+ const defaultNode = props.defaultSection == null ? undefined : tree.byKey.get(props.defaultSection);
249
+ const firstVisibleNode = tree.ordered.find(node => filtered.visibleKeys.has(node.section.key));
250
+ const firstMatchedNode = tree.ordered.find(node => filtered.matchedKeys.has(node.section.key));
251
+ const fallbackNode = defaultNode ?? tree.ordered[0];
252
+ const currentNode = searchTerms.length != 0
253
+ ? (activeNode != null && filtered.matchedKeys.has(activeNode.section.key) ? activeNode : firstMatchedNode ?? firstVisibleNode ?? activeNode ?? fallbackNode)
254
+ : activeNode ?? fallbackNode;
255
+ const current = currentNode?.section;
256
+ const base = props.sectionClassName ?? "wenayDlgSection";
257
+ const activeCls = props.sectionActiveClassName ?? "wenayDlgSectionActive";
258
+ const branchOpenCount = branchKeys.filter(key => expanded.has(key)).length;
259
+ const treeToolsVisible = branchKeys.length > 1;
260
+ const treeToolState = branchOpenCount == 0 ? "collapsed" : branchOpenCount == branchKeys.length ? "expanded" : "branch";
261
+ const treeToolTitle = treeToolState == "expanded" ? "Show current branch only" : treeToolState == "branch" ? "Collapse tree" : "Expand tree";
262
+ useEffect(() => {
263
+ navWidthRef.current = navWidth;
264
+ }, [navWidth]);
265
+ useEffect(() => {
266
+ if (!open)
267
+ return;
268
+ setExpanded(new Set(branchKeys));
269
+ }, [open, treeSignature]);
270
+ useEffect(() => {
271
+ if (!navResizing)
272
+ return;
273
+ const onPointerMove = (e) => {
274
+ const drag = navResizeRef.current;
275
+ if (drag == null)
276
+ return;
277
+ setNavWidth(clampSettingsNavWidth(drag.startWidth + e.clientX - drag.startX));
278
+ };
279
+ const onPointerUp = () => {
280
+ commitNavWidth(navWidthRef.current);
281
+ navResizeRef.current = null;
282
+ setNavResizing(false);
283
+ };
284
+ document.addEventListener("pointermove", onPointerMove);
285
+ document.addEventListener("pointerup", onPointerUp);
286
+ return () => {
287
+ document.removeEventListener("pointermove", onPointerMove);
288
+ document.removeEventListener("pointerup", onPointerUp);
289
+ };
290
+ }, [navResizing]);
36
291
  useEffect(() => {
37
292
  if (!open)
38
293
  return;
39
294
  const onKeyDown = (e) => {
40
- if (e.key == 'Escape')
41
- setOpen(false);
295
+ if (e.key != "Escape" || e.isComposing)
296
+ return;
297
+ if (search != "") {
298
+ e.preventDefault();
299
+ setSearch("");
300
+ searchInputRef.current?.focus();
301
+ return;
302
+ }
303
+ setOpen(false);
42
304
  };
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)] });
305
+ document.addEventListener("keydown", onKeyDown);
306
+ return () => document.removeEventListener("keydown", onKeyDown);
307
+ }, [open, search]);
308
+ function openDialog() {
309
+ setSearch("");
310
+ setHistoryOpen(false);
311
+ setOpen(true);
312
+ }
313
+ function onTriggerKeyDown(e) {
314
+ if (e.key != "Enter" && e.key != " ")
315
+ return;
316
+ e.preventDefault();
317
+ openDialog();
318
+ }
319
+ function toggleExpanded(key) {
320
+ setExpanded(prev => {
321
+ const next = new Set(prev);
322
+ if (next.has(key))
323
+ next.delete(key);
324
+ else
325
+ next.add(key);
326
+ return next;
327
+ });
328
+ }
329
+ function expandAll() {
330
+ setExpanded(new Set(branchKeys));
331
+ }
332
+ function collapseAll() {
333
+ setExpanded(new Set());
334
+ }
335
+ function collapseOutsideCurrent() {
336
+ setExpanded(getCurrentBranchKeys(currentNode));
337
+ }
338
+ function commitSearch(value = search) {
339
+ settingsSearchHistory.add(value);
340
+ setHistoryOpen(false);
341
+ }
342
+ function commitNavWidth(value) {
343
+ const next = clampSettingsNavWidth(value);
344
+ settingsDialogLayout.navWidth = next;
345
+ setNavWidth(next);
346
+ renderBy(settingsDialogLayout);
347
+ memoryMarkDirty("SettingsDialog.layout");
348
+ }
349
+ function beginNavResize(e) {
350
+ e.preventDefault();
351
+ navResizeRef.current = { startX: e.clientX, startWidth: navWidth };
352
+ setHistoryOpen(false);
353
+ setNavResizing(true);
354
+ }
355
+ function onNavResizeKeyDown(e) {
356
+ if (e.key == "ArrowLeft") {
357
+ e.preventDefault();
358
+ commitNavWidth(navWidth - 16);
359
+ }
360
+ else if (e.key == "ArrowRight") {
361
+ e.preventDefault();
362
+ commitNavWidth(navWidth + 16);
363
+ }
364
+ else if (e.key == "Home") {
365
+ e.preventDefault();
366
+ commitNavWidth(settingsDialogNav.min);
367
+ }
368
+ else if (e.key == "End") {
369
+ e.preventDefault();
370
+ commitNavWidth(settingsDialogNav.max);
371
+ }
372
+ else if (e.key == "Enter" || e.key == " ") {
373
+ e.preventDefault();
374
+ commitNavWidth(settingsDialogNav.def);
375
+ }
376
+ }
377
+ function useHistoryItem(value) {
378
+ setSearch(value);
379
+ settingsSearchHistory.add(value);
380
+ setHistoryOpen(false);
381
+ searchInputRef.current?.focus();
382
+ }
383
+ function closeHistoryWhenSearchFocusLeaves(e) {
384
+ const next = e.relatedTarget;
385
+ if (next && e.currentTarget.contains(next))
386
+ return;
387
+ window.setTimeout(() => {
388
+ const box = searchBoxRef.current;
389
+ if (box == null || !box.contains(document.activeElement))
390
+ setHistoryOpen(false);
391
+ }, 0);
392
+ }
393
+ function cycleTreeTool() {
394
+ if (treeToolState == "expanded")
395
+ collapseOutsideCurrent();
396
+ else if (treeToolState == "branch")
397
+ collapseAll();
398
+ else
399
+ expandAll();
400
+ }
401
+ function renderTreeNode(node) {
402
+ if (!filtered.visibleKeys.has(node.section.key))
403
+ return null;
404
+ const key = node.section.key;
405
+ const hasChildren = node.children.length != 0;
406
+ const isOpen = expanded.has(key) || (searchTerms.length != 0 && filtered.expandedKeys.has(key));
407
+ const isActive = key == current?.key;
408
+ const rowClass = classNames(["wenayDlgTreeRow", base, isActive && activeCls]);
409
+ function onRowKeyDown(e) {
410
+ if (e.key == "Enter" || e.key == " ") {
411
+ e.preventDefault();
412
+ setActive(key);
413
+ }
414
+ if (e.key == "ArrowRight" && hasChildren && !isOpen) {
415
+ e.preventDefault();
416
+ toggleExpanded(key);
417
+ }
418
+ if (e.key == "ArrowLeft" && hasChildren && isOpen) {
419
+ e.preventDefault();
420
+ toggleExpanded(key);
421
+ }
422
+ }
423
+ 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 => {
424
+ e.stopPropagation();
425
+ if (hasChildren)
426
+ toggleExpanded(key);
427
+ } }), _jsx("span", { className: "wenayDlgTreeLabel", children: renderHighlightedLabel(node.section.name, searchTerms) })] }), hasChildren && isOpen && node.children.map(renderTreeNode)] }, key);
428
+ }
429
+ 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", { ref: searchBoxRef, className: "wenayDlgSearchBox", onBlur: closeHistoryWhenSearchFocusLeaves, children: [_jsx("input", { ref: searchInputRef, className: "wenayDlgSearch", value: search, placeholder: "Search", "aria-label": "Search settings", onFocus: () => setHistoryOpen(searchHistory.length != 0), onChange: e => {
430
+ setSearch(e.currentTarget.value);
431
+ setHistoryOpen(searchHistory.length != 0);
432
+ }, onKeyDown: e => {
433
+ if (e.key == "Enter")
434
+ commitSearch();
435
+ if (e.key == "ArrowDown" && searchHistory[0]) {
436
+ e.preventDefault();
437
+ useHistoryItem(searchHistory[0]);
438
+ }
439
+ } }), searchHistory.length != 0 && _jsx("button", { type: "button", className: "wenayDlgSearchHistoryBtn", title: "Search history", "aria-label": "Search history", "aria-expanded": historyOpen, onClick: () => {
440
+ setHistoryOpen(v => !v);
441
+ searchInputRef.current?.focus();
442
+ }, 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: () => {
443
+ setSearch("");
444
+ setHistoryOpen(searchHistory.length != 0);
445
+ searchInputRef.current?.focus();
446
+ }, 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: () => {
447
+ settingsSearchHistory.clear();
448
+ setHistoryOpen(false);
449
+ searchInputRef.current?.focus();
450
+ }, 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
451
  }