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 +5 -4
- package/lib/common/src/components/Modal/ModalContextProvider.js +1 -1
- package/lib/common/src/components/Settings/SettingsDialog.d.ts +12 -2
- package/lib/common/src/components/Settings/SettingsDialog.js +401 -16
- package/lib/common/src/components/Toolbar/Toolbar.d.ts +21 -2
- package/lib/common/src/components/Toolbar/Toolbar.js +84 -14
- package/lib/common/src/grid/columnState/CardList.js +5 -2
- package/lib/common/src/grid/columnState/ColumnDots.js +8 -18
- package/lib/common/src/grid/columnState/columnState.d.ts +4 -0
- package/lib/common/src/grid/columnState/columnState.js +39 -14
- package/lib/common/src/hooks/useReplay.d.ts +54 -0
- package/lib/common/src/hooks/useReplay.js +222 -0
- package/lib/common/src/logs/logStyles.d.ts +13 -0
- package/lib/common/src/logs/logStyles.js +18 -0
- package/lib/common/src/logs/logs.js +5 -7
- package/lib/common/src/logs/logsContext.js +22 -26
- package/lib/common/src/menu/menu.d.ts +2 -1
- package/lib/common/src/menu/menu.js +28 -21
- package/lib/common/src/styles/tokens.d.ts +14 -1
- package/lib/common/src/styles/tokens.js +14 -1
- package/lib/common/src/utils/index.d.ts +1 -0
- package/lib/common/src/utils/index.js +1 -0
- package/lib/common/src/utils/searchHistory.d.ts +14 -0
- package/lib/common/src/utils/searchHistory.js +59 -0
- package/lib/style/menuRight.css +2 -2
- package/lib/style/style.css +563 -54
- package/lib/style/tokens.css +15 -3
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
41
|
-
|
|
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(
|
|
44
|
-
return () => document.removeEventListener(
|
|
45
|
-
}, [open]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
|
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
|
|
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
|
};
|