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.
- package/README.md +2 -0
- package/lib/common/src/components/Input.d.ts +25 -6
- package/lib/common/src/components/Input.js +27 -8
- 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 +412 -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.d.ts +1 -1
- package/lib/common/src/grid/columnState/ColumnDots.js +9 -19
- package/lib/common/src/grid/columnState/ColumnsMenu.js +10 -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/menu/menuMouse.d.ts +17 -0
- package/lib/common/src/menu/menuMouse.js +70 -3
- package/lib/common/src/styles/tokens.d.ts +92 -1
- package/lib/common/src/styles/tokens.js +92 -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 +634 -53
- package/lib/style/tokens.css +90 -4
- 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
|
|
2
|
+
export type TextInputPanelProps = {
|
|
3
3
|
callback: (txt: string) => void;
|
|
4
4
|
name?: string;
|
|
5
5
|
txt?: string;
|
|
6
|
-
}
|
|
7
|
-
export declare function
|
|
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 }:
|
|
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
|
|
25
|
+
export type FileInputPanelProps = {
|
|
16
26
|
callback: (file: File | null) => void;
|
|
17
27
|
name?: string;
|
|
18
|
-
}
|
|
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
|
|
10
|
+
export function useTextInputPanel({ callback, txt = "" }) {
|
|
11
11
|
const txtName = useRef(txt);
|
|
12
|
-
return
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
32
|
+
export function useFileInputPanel({ callback }) {
|
|
23
33
|
const file = useRef(null);
|
|
24
|
-
return
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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,417 @@ 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 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
|
|
41
|
-
|
|
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(
|
|
44
|
-
return () => document.removeEventListener(
|
|
45
|
-
}, [open]);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
}
|