wenay-react2 1.0.38 → 1.0.40
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/lib/common/src/components/Dnd/FloatingWindow.d.ts +29 -12
- package/lib/common/src/components/Dnd/FloatingWindow.js +100 -91
- package/lib/common/src/components/Input.d.ts +25 -6
- package/lib/common/src/components/Input.js +27 -8
- package/lib/common/src/components/Menu/RightMenu.d.ts +28 -1
- package/lib/common/src/components/Menu/RightMenu.js +62 -35
- package/lib/common/src/components/Modal/LeftModal.js +5 -4
- package/lib/common/src/components/Modal/Modal.js +8 -7
- package/lib/common/src/components/ParamsEditor.d.ts +15 -0
- package/lib/common/src/components/ParamsEditor.js +45 -18
- package/lib/common/src/components/Settings/SettingsDialog.d.ts +68 -6
- package/lib/common/src/components/Settings/SettingsDialog.js +73 -13
- package/lib/common/src/components/Toolbar/Toolbar.js +64 -19
- package/lib/common/src/components/UiSlot/UiSlot.js +5 -4
- package/lib/common/src/grid/columnState/ColumnDots.d.ts +1 -1
- package/lib/common/src/grid/columnState/ColumnDots.js +1 -1
- package/lib/common/src/grid/columnState/ColumnsMenu.js +10 -18
- package/lib/common/src/grid/columnState/columnState.js +8 -6
- package/lib/common/src/hooks/useKeyboard.js +4 -3
- package/lib/common/src/logs/logs.d.ts +13 -133
- package/lib/common/src/logs/logs.js +20 -39
- package/lib/common/src/logs/logsContext.d.ts +26 -0
- package/lib/common/src/logs/logsContext.js +25 -20
- package/lib/common/src/logs/logsController.d.ts +88 -0
- package/lib/common/src/logs/logsController.js +54 -0
- package/lib/common/src/logs/miniLogs.d.ts +69 -3
- package/lib/common/src/logs/miniLogs.js +70 -15
- package/lib/common/src/menu/menu.d.ts +15 -3
- package/lib/common/src/menu/menu.js +81 -17
- package/lib/common/src/menu/menuMouse.d.ts +35 -0
- package/lib/common/src/menu/menuMouse.js +113 -4
- package/lib/common/src/styles/tokens.d.ts +79 -1
- package/lib/common/src/styles/tokens.js +79 -1
- package/lib/common/src/utils/memoryStore.d.ts +1 -10
- package/lib/common/src/utils/searchHistory.js +4 -3
- package/lib/style/style.css +122 -50
- package/lib/style/tokens.css +76 -2
- package/package.json +49 -49
|
@@ -12,7 +12,7 @@ function cx(...parts) {
|
|
|
12
12
|
function renderTrigger(trigger, state) {
|
|
13
13
|
return typeof trigger == 'function' ? trigger(state) : trigger;
|
|
14
14
|
}
|
|
15
|
-
export function
|
|
15
|
+
export function useRightMenuController({ elements, style, styles, position: initialPosition = 'right', verticalPosition: initialVerticalPosition = 'top', keyForSave }) {
|
|
16
16
|
const [initialState] = useState(() => {
|
|
17
17
|
const fallback = {
|
|
18
18
|
position: initialPosition,
|
|
@@ -31,9 +31,8 @@ export function DropdownMenu({ elements, style, styles, classNames = {}, trigger
|
|
|
31
31
|
const [isFixed, setIsFixed] = useState(false);
|
|
32
32
|
const [select, setSelect] = useState(null);
|
|
33
33
|
const data = useRef({ m1: false, m2: false });
|
|
34
|
-
// Get modal JSX functions
|
|
35
34
|
const jsx = useMemo(createModalRenderStore, []);
|
|
36
|
-
const
|
|
35
|
+
const submenuRender = useMemo(() => _jsx(jsx.Render, {}), [jsx]);
|
|
37
36
|
const [position, setPosition] = useState(initialState.position);
|
|
38
37
|
const [isTop, setIsTop] = useState(initialState.verticalPosition === 'top');
|
|
39
38
|
const positionLast = useRef({ ...initialState.offset });
|
|
@@ -82,67 +81,50 @@ export function DropdownMenu({ elements, style, styles, classNames = {}, trigger
|
|
|
82
81
|
}
|
|
83
82
|
}, [isTop, keyForSave, position]);
|
|
84
83
|
const { position: pos, dragProps } = useDraggable(0, 0, 50, handleDragEnd, () => { });
|
|
85
|
-
|
|
86
|
-
const handleClickOutside = useCallback(() => {
|
|
84
|
+
const close = useCallback(() => {
|
|
87
85
|
setIsOpen(false);
|
|
88
86
|
}, []);
|
|
89
|
-
const
|
|
87
|
+
const toggleFixed = useCallback(() => {
|
|
90
88
|
setIsFixed((prev) => !prev);
|
|
91
89
|
setIsOpen((prev) => !prev);
|
|
92
90
|
}, []);
|
|
93
|
-
const
|
|
91
|
+
const selectItem = useCallback((item, index) => {
|
|
94
92
|
jsx.set(item.subMenuContent);
|
|
95
93
|
setSelect(index);
|
|
96
94
|
}, [jsx]);
|
|
97
|
-
const
|
|
95
|
+
const contentMouseEnter = useCallback(() => {
|
|
98
96
|
data.current.m1 = true;
|
|
99
97
|
}, []);
|
|
100
|
-
const
|
|
101
|
-
data.current.m1 = false;
|
|
98
|
+
const clearSelectIfIdle = useCallback(async () => {
|
|
102
99
|
await sleepAsync(50);
|
|
103
100
|
if (!data.current.m1 && !data.current.m2) {
|
|
104
101
|
jsx.set(null);
|
|
105
102
|
setSelect(null);
|
|
106
103
|
}
|
|
107
104
|
}, [jsx]);
|
|
108
|
-
const
|
|
105
|
+
const contentMouseLeave = useCallback(async () => {
|
|
106
|
+
data.current.m1 = false;
|
|
107
|
+
await clearSelectIfIdle();
|
|
108
|
+
}, [clearSelectIfIdle]);
|
|
109
|
+
const submenuMouseEnter = useCallback(() => {
|
|
109
110
|
data.current.m2 = true;
|
|
110
111
|
}, []);
|
|
111
|
-
const
|
|
112
|
+
const submenuMouseLeave = useCallback(async () => {
|
|
112
113
|
data.current.m2 = false;
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
jsx.set(null);
|
|
116
|
-
setSelect(null);
|
|
117
|
-
}
|
|
118
|
-
}, [jsx]);
|
|
119
|
-
// Render dropdown menu (dop)
|
|
120
|
-
const dop = (isFixed || isOpen) && (_jsxs("div", { onMouseEnter: handleContentMouseEnter, onMouseLeave: handleContentMouseLeave, className: cx(classNames.flyout ?? 'dropdown-content2', !isTop ? (classNames.flyoutUp ?? 'dropdown-up') : undefined), style: {
|
|
121
|
-
display: 'flex',
|
|
122
|
-
[position]: 0,
|
|
123
|
-
right: position === 'left' ? 'auto' : 0,
|
|
124
|
-
flexDirection: position === 'left' ? 'row' : 'row-reverse',
|
|
125
|
-
...styles?.flyout
|
|
126
|
-
}, children: [_jsx("div", { className: classNames.list ?? "dropdown-content", style: styles?.list, onMouseEnter: handleContentMouseEnter, onMouseLeave: handleContentMouseLeave, children: elements.map((item, index) => (_jsx("div", { className: cx(classNames.item ?? 'menu-item', select === index ? (classNames.itemActive ?? 'force-hover') : undefined), style: styles?.item, onMouseEnter: () => handleSelect(item, index), onClick: () => handleSelect(item, index), children: item.label }, item.label))) }), _jsx("div", { onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: handleSubmenuMouseLeave, children: jsx.JSX ? (
|
|
127
|
-
// clamp to the viewport: with hard 40vw/70vh the submenu ran
|
|
128
|
-
// off screen when the menu sat near a bottom/side edge
|
|
129
|
-
_jsx("div", { className: classNames.submenu ?? "submenu", style: { width: '40vw', minHeight: '70vh',
|
|
130
|
-
maxWidth: '90vw', maxHeight: '85vh', overflowY: 'auto', ...styles?.submenu }, children: jsxRender })) : null })] }));
|
|
131
|
-
// Calculate offsets
|
|
114
|
+
await clearSelectIfIdle();
|
|
115
|
+
}, [clearSelectIfIdle]);
|
|
132
116
|
const computedX = position === 'left'
|
|
133
117
|
? positionLast.current.x + pos.x
|
|
134
118
|
: positionLast.current.x - pos.x;
|
|
135
119
|
const computedY = isTop
|
|
136
120
|
? positionLast.current.y + pos.y
|
|
137
121
|
: positionLast.current.y - pos.y;
|
|
138
|
-
// Calculate menu container styles
|
|
139
122
|
const containerStyle = useMemo(() => {
|
|
140
123
|
const computedStyle = {
|
|
141
124
|
...style,
|
|
142
125
|
...styles?.container,
|
|
143
126
|
display: 'flex'
|
|
144
127
|
};
|
|
145
|
-
// Horizontal positioning
|
|
146
128
|
if (position === 'left') {
|
|
147
129
|
computedStyle.left = Math.max(0, Math.min(computedX, window.innerWidth - 50));
|
|
148
130
|
computedStyle.right = 'auto';
|
|
@@ -151,7 +133,6 @@ export function DropdownMenu({ elements, style, styles, classNames = {}, trigger
|
|
|
151
133
|
computedStyle.right = Math.max(0, Math.min(computedX, window.innerWidth - 50));
|
|
152
134
|
computedStyle.left = 'auto';
|
|
153
135
|
}
|
|
154
|
-
// Vertical positioning
|
|
155
136
|
if (isTop) {
|
|
156
137
|
computedStyle.top = Math.max(0, Math.min(computedY, window.innerHeight - 50));
|
|
157
138
|
computedStyle.bottom = 'auto';
|
|
@@ -162,7 +143,53 @@ export function DropdownMenu({ elements, style, styles, classNames = {}, trigger
|
|
|
162
143
|
}
|
|
163
144
|
return computedStyle;
|
|
164
145
|
}, [style, styles?.container, position, isTop, computedX, computedY]);
|
|
165
|
-
|
|
146
|
+
const flyoutStyle = useMemo(() => ({
|
|
147
|
+
display: 'flex',
|
|
148
|
+
[position]: 0,
|
|
149
|
+
right: position === 'left' ? 'auto' : 0,
|
|
150
|
+
flexDirection: position === 'left' ? 'row' : 'row-reverse',
|
|
151
|
+
...styles?.flyout
|
|
152
|
+
}), [position, styles?.flyout]);
|
|
153
|
+
return {
|
|
154
|
+
elements,
|
|
155
|
+
isOpen,
|
|
156
|
+
isFixed,
|
|
157
|
+
select,
|
|
158
|
+
position,
|
|
159
|
+
isTop,
|
|
160
|
+
containerRef,
|
|
161
|
+
dragProps,
|
|
162
|
+
triggerState: { isOpen, isFixed },
|
|
163
|
+
containerStyle,
|
|
164
|
+
flyoutStyle,
|
|
165
|
+
submenuRender,
|
|
166
|
+
hasSubmenu: !!jsx.JSX,
|
|
167
|
+
open: () => setIsOpen(true),
|
|
168
|
+
close,
|
|
169
|
+
toggleFixed,
|
|
170
|
+
setOpen: setIsOpen,
|
|
171
|
+
selectItem,
|
|
172
|
+
contentMouseEnter,
|
|
173
|
+
contentMouseLeave,
|
|
174
|
+
submenuMouseEnter,
|
|
175
|
+
submenuMouseLeave,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
export function DropdownMenu({ elements, style, styles, classNames = {}, trigger = '☰', position, verticalPosition, keyForSave }) {
|
|
179
|
+
const controller = useRightMenuController({
|
|
180
|
+
elements,
|
|
181
|
+
style,
|
|
182
|
+
styles,
|
|
183
|
+
position,
|
|
184
|
+
verticalPosition,
|
|
185
|
+
keyForSave,
|
|
186
|
+
});
|
|
187
|
+
const dop = (controller.isFixed || controller.isOpen) && (_jsxs("div", { onMouseEnter: controller.contentMouseEnter, onMouseLeave: controller.contentMouseLeave, className: cx(classNames.flyout ?? 'dropdown-content2', !controller.isTop ? (classNames.flyoutUp ?? 'dropdown-up') : undefined), style: controller.flyoutStyle, children: [_jsx("div", { className: classNames.list ?? "dropdown-content", style: styles?.list, onMouseEnter: controller.contentMouseEnter, onMouseLeave: controller.contentMouseLeave, children: elements.map((item, index) => (_jsx("div", { className: cx(classNames.item ?? 'menu-item', controller.select === index ? (classNames.itemActive ?? 'force-hover') : undefined), style: styles?.item, onMouseEnter: () => controller.selectItem(item, index), onClick: () => controller.selectItem(item, index), children: item.label }, item.label))) }), _jsx("div", { onMouseEnter: controller.submenuMouseEnter, onMouseLeave: controller.submenuMouseLeave, children: controller.hasSubmenu ? (
|
|
188
|
+
// clamp to the viewport: with hard 40vw/70vh the submenu ran
|
|
189
|
+
// off screen when the menu sat near a bottom/side edge
|
|
190
|
+
_jsx("div", { className: classNames.submenu ?? "submenu", style: { width: '40vw', minHeight: '70vh',
|
|
191
|
+
maxWidth: '90vw', maxHeight: '85vh', overflowY: 'auto', ...styles?.submenu }, children: controller.submenuRender })) : null })] }));
|
|
192
|
+
return (_jsxs(OutsideClickArea, { ref: controller.containerRef, outsideClick: controller.close, className: cx(classNames.container ?? 'menu-container', controller.isFixed ? (classNames.activeContainer ?? 'activeM') : undefined), style: controller.containerStyle, onMouseEnter: () => !controller.isFixed && controller.setOpen(true), onMouseLeave: () => !controller.isFixed && controller.setOpen(false), children: [_jsx("div", { ...controller.dragProps, className: classNames.trigger ?? "menu-button", style: styles?.trigger, onClick: controller.toggleFixed, children: renderTrigger(trigger, controller.triggerState) }), dop] }));
|
|
166
193
|
}
|
|
167
194
|
export function createRightMenuController() {
|
|
168
195
|
const elements = [];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { colorGenerator2, sleepAsync } from "wenay-common2";
|
|
4
|
-
import {
|
|
4
|
+
import { createUpdateApi } from "../../../updateBy";
|
|
5
5
|
import { createModalElementStore } from "./Modal";
|
|
6
6
|
import { DragBox } from "../Dnd/FloatingWindow";
|
|
7
7
|
function useViewport() {
|
|
@@ -206,6 +206,7 @@ export function getApiLeftMenu() {
|
|
|
206
206
|
}, children: textB })), children()] }));
|
|
207
207
|
};
|
|
208
208
|
const menuStore = new Map();
|
|
209
|
+
const menuStoreApi = createUpdateApi(menuStore);
|
|
209
210
|
const setMenu = (items, key = "base") => {
|
|
210
211
|
const colorGen = colorGenerator2({ min: 0, max: 90 });
|
|
211
212
|
// exclude this key's old items, otherwise a repeated setMenu shifts default colors
|
|
@@ -242,15 +243,15 @@ export function getApiLeftMenu() {
|
|
|
242
243
|
return;
|
|
243
244
|
lastSet.current = { menu, menuKey };
|
|
244
245
|
setMenu(menu, menuKey);
|
|
245
|
-
|
|
246
|
+
menuStoreApi.render();
|
|
246
247
|
}, [menu, menuKey]);
|
|
247
|
-
|
|
248
|
+
menuStoreApi.use();
|
|
248
249
|
return (_jsxs("div", { className: "maxSize", style: { position: "absolute", zIndex: zIndex0 }, children: [_jsx(modal.Render, {}), _jsx(LeftMenuComponent, { zIndex: zIndex, api: () => { }, menu: getAllMenuItems() })] }));
|
|
249
250
|
}
|
|
250
251
|
const modal = createModalElementStore();
|
|
251
252
|
return {
|
|
252
253
|
modal,
|
|
253
|
-
renderBy() {
|
|
254
|
+
renderBy() { menuStoreApi.render(); },
|
|
254
255
|
getMenu: () => menuStore,
|
|
255
256
|
setMenu: setMenu,
|
|
256
257
|
Modal2: Modal2
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx_1 } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { createUpdateApi } from "../../../updateBy";
|
|
3
3
|
import { TextInputModal } from "../Input";
|
|
4
4
|
function setModalTarget(target, jsx) {
|
|
5
5
|
if (typeof target == "function")
|
|
@@ -32,21 +32,21 @@ function createJsxStore(renderItem) {
|
|
|
32
32
|
const data = {
|
|
33
33
|
set(jsx) {
|
|
34
34
|
_jsx = jsx;
|
|
35
|
-
|
|
35
|
+
dataApi.render();
|
|
36
36
|
},
|
|
37
37
|
set JSX(jsx) {
|
|
38
38
|
_jsx = jsx;
|
|
39
|
-
|
|
39
|
+
dataApi.render();
|
|
40
40
|
},
|
|
41
41
|
get JSX() { return _jsx; },
|
|
42
42
|
Render() {
|
|
43
|
-
|
|
43
|
+
dataApi.use();
|
|
44
44
|
return _jsx && renderItem(_jsx);
|
|
45
45
|
},
|
|
46
46
|
addJSX(jsx) {
|
|
47
47
|
if (check(jsx) == -1) {
|
|
48
48
|
_jsxArr.push({ jsx, key: key++ });
|
|
49
|
-
|
|
49
|
+
dataApi.render();
|
|
50
50
|
}
|
|
51
51
|
return jsx;
|
|
52
52
|
},
|
|
@@ -54,15 +54,16 @@ function createJsxStore(renderItem) {
|
|
|
54
54
|
const c = check(jsx);
|
|
55
55
|
if (c != -1) {
|
|
56
56
|
_jsxArr.splice(c, 1);
|
|
57
|
-
|
|
57
|
+
dataApi.render();
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
get arrJSX() { return _jsxArr.map(e => e.jsx && _jsx_1("div", { children: renderItem(e.jsx) }, e.key)); },
|
|
61
61
|
RenderArr() {
|
|
62
|
-
|
|
62
|
+
dataApi.use();
|
|
63
63
|
return data.arrJSX;
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
|
+
const dataApi = createUpdateApi(data);
|
|
66
67
|
return data;
|
|
67
68
|
}
|
|
68
69
|
/** Low-level imperative JSX storage based on updateBy/renderBy. Prefer `ModalProvider`/`useModal` for app modals. */
|
|
@@ -1,4 +1,19 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import { Params } from "wenay-common2";
|
|
3
|
+
export type ParamsEditorControllerOptions<TParams extends Params.IParamsExpandableReadonly = Params.IParamsExpandableReadonly> = {
|
|
4
|
+
params: TParams;
|
|
5
|
+
onChange: (params: TParams) => void;
|
|
6
|
+
onExpand?: (params: TParams) => void;
|
|
7
|
+
};
|
|
8
|
+
export type ParamsEditorController<TParams extends Params.IParamsExpandableReadonly = Params.IParamsExpandableReadonly> = {
|
|
9
|
+
paramsRef: React.MutableRefObject<TParams>;
|
|
10
|
+
params: TParams;
|
|
11
|
+
refresh(): void;
|
|
12
|
+
notifyChange(): void;
|
|
13
|
+
notifyChangeDelayed(): void;
|
|
14
|
+
notifyExpand(): void;
|
|
15
|
+
};
|
|
16
|
+
export declare function useParamsEditorController<TParams extends Params.IParamsExpandableReadonly = Params.IParamsExpandableReadonly>({ params, onChange, onExpand }: ParamsEditorControllerOptions<TParams>): ParamsEditorController<TParams>;
|
|
2
17
|
export declare function ParamsEditor<TParams extends Params.IParamsExpandableReadonly = Params.IParamsExpandableReadonly>(data: {
|
|
3
18
|
params: TParams;
|
|
4
19
|
expandStatus?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { deepCloneMutable, isDate, TF, timeLocalToStr_yyyymmdd, timeLocalToStr_yyyymmdd_hhmm, timeLocalToStr_yyyymmdd_hhmmss, timeLocalToStr_yyyymmdd_hhmmss_ms } from "wenay-common2";
|
|
4
4
|
import { setResizeableElement, removeResizeableElement } from "./MyResizeObserver";
|
|
5
5
|
import { ParamRow, ParamToggleLabel } from "./Parameters";
|
|
@@ -163,6 +163,44 @@ function InputListArray(set, values, range, rangeLabels) {
|
|
|
163
163
|
return _jsx("option", { value: String(a), label: rangeLabels?.at(i) ?? String(a), children: a }, String(a));
|
|
164
164
|
}) });
|
|
165
165
|
}
|
|
166
|
+
export function useParamsEditorController({ params, onChange, onExpand }) {
|
|
167
|
+
const [, setUpdate] = useState(0);
|
|
168
|
+
const cloned = useMemo(() => deepCloneMutable(params), [params]);
|
|
169
|
+
const paramsRef = useRef(cloned);
|
|
170
|
+
if (paramsRef.current !== cloned)
|
|
171
|
+
paramsRef.current = cloned;
|
|
172
|
+
const callbacks = useRef({ onChange, onExpand });
|
|
173
|
+
callbacks.current = { onChange, onExpand };
|
|
174
|
+
const timeoutId = useRef(null);
|
|
175
|
+
useEffect(() => () => {
|
|
176
|
+
if (timeoutId.current != null)
|
|
177
|
+
clearTimeout(timeoutId.current);
|
|
178
|
+
}, []);
|
|
179
|
+
const refresh = useCallback(() => {
|
|
180
|
+
setUpdate(e => e + 1);
|
|
181
|
+
}, []);
|
|
182
|
+
const notifyChange = useCallback(() => {
|
|
183
|
+
callbacks.current.onChange(paramsRef.current);
|
|
184
|
+
}, []);
|
|
185
|
+
const notifyChangeDelayed = useCallback(() => {
|
|
186
|
+
if (timeoutId.current != null)
|
|
187
|
+
clearTimeout(timeoutId.current);
|
|
188
|
+
timeoutId.current = setTimeout(() => {
|
|
189
|
+
notifyChange();
|
|
190
|
+
}, 200);
|
|
191
|
+
}, [notifyChange]);
|
|
192
|
+
const notifyExpand = useCallback(() => {
|
|
193
|
+
callbacks.current.onExpand?.(paramsRef.current);
|
|
194
|
+
}, []);
|
|
195
|
+
return {
|
|
196
|
+
paramsRef,
|
|
197
|
+
params: paramsRef.current,
|
|
198
|
+
refresh,
|
|
199
|
+
notifyChange,
|
|
200
|
+
notifyChangeDelayed,
|
|
201
|
+
notifyExpand,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
166
204
|
export function ParamsEditor(data) {
|
|
167
205
|
// callbacks live in a ref: useMemo below intentionally freezes the element until
|
|
168
206
|
// params identity changes, and would otherwise freeze a stale onChange/onExpand with it
|
|
@@ -174,26 +212,15 @@ export function ParamsEditor(data) {
|
|
|
174
212
|
return result;
|
|
175
213
|
}
|
|
176
214
|
function ParamsEditorBase(data) {
|
|
177
|
-
const
|
|
178
|
-
|
|
215
|
+
const controller = useParamsEditorController(data);
|
|
216
|
+
const myParams = controller.paramsRef;
|
|
217
|
+
function Refresh() { controller.refresh(); }
|
|
179
218
|
const styleColorDisable = "rgb(146,146,146)";
|
|
180
|
-
const p = useMemo(() => deepCloneMutable(data.params), [data.params]);
|
|
181
|
-
const myParams = useRef(p);
|
|
182
|
-
// sync during render: the effect+Refresh variant committed a frame of stale params first
|
|
183
|
-
if (myParams.current !== p)
|
|
184
|
-
myParams.current = p;
|
|
185
|
-
const timeoutId = useRef(null);
|
|
186
|
-
useEffect(() => () => { if (timeoutId.current != null)
|
|
187
|
-
clearTimeout(timeoutId.current); }, []);
|
|
188
219
|
function refreshIndicator() {
|
|
189
|
-
|
|
220
|
+
controller.notifyChange();
|
|
190
221
|
}
|
|
191
222
|
function refreshIndicatorDelayed() {
|
|
192
|
-
|
|
193
|
-
clearTimeout(timeoutId.current);
|
|
194
|
-
timeoutId.current = setTimeout(() => {
|
|
195
|
-
refreshIndicator();
|
|
196
|
-
}, 200);
|
|
223
|
+
controller.notifyChangeDelayed();
|
|
197
224
|
}
|
|
198
225
|
function Param(//<T extends boolean|number|string|const_Date>(
|
|
199
226
|
set, val, type, range, labels) {
|
|
@@ -218,7 +245,7 @@ function ParamsEditorBase(data) {
|
|
|
218
245
|
refreshIndicator(); // process without delay
|
|
219
246
|
}
|
|
220
247
|
function onExpandA(param, flag) {
|
|
221
|
-
|
|
248
|
+
controller.notifyExpand();
|
|
222
249
|
}
|
|
223
250
|
function ListParams(obj, parentEnabled = true, expandLevel = 0) {
|
|
224
251
|
return Object.entries(obj).map(([key, param]) => {
|
|
@@ -13,20 +13,82 @@ export type SettingsSection = {
|
|
|
13
13
|
/** Semantic aliases: commands, domain terms, translated labels, etc. */
|
|
14
14
|
keywords?: readonly string[];
|
|
15
15
|
};
|
|
16
|
+
type SettingsTreeNode = {
|
|
17
|
+
section: SettingsSection;
|
|
18
|
+
children: SettingsTreeNode[];
|
|
19
|
+
parent?: SettingsTreeNode;
|
|
20
|
+
depth: number;
|
|
21
|
+
};
|
|
22
|
+
type SettingsTree = {
|
|
23
|
+
roots: SettingsTreeNode[];
|
|
24
|
+
ordered: SettingsTreeNode[];
|
|
25
|
+
byKey: Map<string, SettingsTreeNode>;
|
|
26
|
+
};
|
|
27
|
+
type SettingsTreeFilter = {
|
|
28
|
+
visibleKeys: Set<string>;
|
|
29
|
+
matchedKeys: Set<string>;
|
|
30
|
+
expandedKeys: Set<string>;
|
|
31
|
+
};
|
|
32
|
+
type SettingsSearchTerm = {
|
|
33
|
+
variants: string[];
|
|
34
|
+
};
|
|
16
35
|
/** Register an external section. Re-register with the same key replaces the previous one.
|
|
17
36
|
* The returned function removes exactly this registration (a no-op if it was replaced). */
|
|
18
37
|
export declare function registerSettingsSection(s: SettingsSection): () => void;
|
|
19
38
|
/** Current external sections (static props sections are not included). */
|
|
20
39
|
export declare function getSettingsSections(): readonly SettingsSection[];
|
|
21
|
-
|
|
22
|
-
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
23
|
-
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
24
|
-
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
25
|
-
export declare function SettingsDialog(props: {
|
|
40
|
+
export type SettingsDialogProps = {
|
|
26
41
|
trigger?: React.ReactNode;
|
|
27
42
|
sections?: SettingsSection[];
|
|
28
43
|
defaultSection?: string;
|
|
29
44
|
/** Section buttons: apps pass their own .chip / .chipActive; defaults are minimal library styles */
|
|
30
45
|
sectionClassName?: string;
|
|
31
46
|
sectionActiveClassName?: string;
|
|
32
|
-
}
|
|
47
|
+
};
|
|
48
|
+
export type SettingsDialogTreeToolState = "collapsed" | "expanded" | "branch";
|
|
49
|
+
export type SettingsDialogController = {
|
|
50
|
+
open: boolean;
|
|
51
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
52
|
+
active?: string;
|
|
53
|
+
setActive: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
54
|
+
search: string;
|
|
55
|
+
setSearch: React.Dispatch<React.SetStateAction<string>>;
|
|
56
|
+
expanded: Set<string>;
|
|
57
|
+
historyOpen: boolean;
|
|
58
|
+
setHistoryOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
59
|
+
navWidth: number;
|
|
60
|
+
navResizing: boolean;
|
|
61
|
+
searchInputRef: React.RefObject<HTMLInputElement | null>;
|
|
62
|
+
searchBoxRef: React.RefObject<HTMLDivElement | null>;
|
|
63
|
+
tree: SettingsTree;
|
|
64
|
+
filtered: SettingsTreeFilter;
|
|
65
|
+
current?: SettingsSection;
|
|
66
|
+
currentNode?: SettingsTreeNode;
|
|
67
|
+
firstVisibleNode?: SettingsTreeNode;
|
|
68
|
+
searchTerms: SettingsSearchTerm[];
|
|
69
|
+
searchHistory: readonly string[];
|
|
70
|
+
base: string;
|
|
71
|
+
activeCls: string;
|
|
72
|
+
treeToolsVisible: boolean;
|
|
73
|
+
treeToolState: SettingsDialogTreeToolState;
|
|
74
|
+
treeToolTitle: string;
|
|
75
|
+
openDialog(): void;
|
|
76
|
+
closeDialog(): void;
|
|
77
|
+
onTriggerKeyDown(e: React.KeyboardEvent<HTMLSpanElement>): void;
|
|
78
|
+
toggleExpanded(key: string): void;
|
|
79
|
+
commitSearch(value?: string): void;
|
|
80
|
+
useHistoryItem(value: string): void;
|
|
81
|
+
clearHistory(): void;
|
|
82
|
+
commitNavWidth(value: number): void;
|
|
83
|
+
beginNavResize(e: React.PointerEvent<HTMLDivElement>): void;
|
|
84
|
+
onNavResizeKeyDown(e: React.KeyboardEvent<HTMLDivElement>): void;
|
|
85
|
+
closeHistoryWhenSearchFocusLeaves(e: React.FocusEvent<HTMLDivElement>): void;
|
|
86
|
+
cycleTreeTool(): void;
|
|
87
|
+
};
|
|
88
|
+
export declare function useSettingsDialogController(props: SettingsDialogProps): SettingsDialogController;
|
|
89
|
+
/** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
|
|
90
|
+
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
91
|
+
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
92
|
+
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
93
|
+
export declare function SettingsDialog(props: SettingsDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
94
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import {
|
|
4
|
+
import { createUpdateApi, renderBy } from "../../../updateBy";
|
|
5
5
|
import { OutsideClickArea } from "../../hooks/useOutside";
|
|
6
6
|
import { FloatingWindowBase } from "../Dnd/FloatingWindow";
|
|
7
7
|
import { createSearchHistory } from "../../utils/searchHistory";
|
|
@@ -12,6 +12,7 @@ const settingsDialogNav = { min: 160, def: 220, max: 360 };
|
|
|
12
12
|
// Module singleton (closure + updateBy subscription, no React context): any module
|
|
13
13
|
// registers a section on mount and removes it on unmount; the dialog re-renders on changes.
|
|
14
14
|
const registry = { list: [] };
|
|
15
|
+
const registryApi = createUpdateApi(registry);
|
|
15
16
|
const settingsSearchHistory = createSearchHistory({ key: "SettingsDialog.searchHistory", max: 8 });
|
|
16
17
|
const settingsDialogLayout = memoryGetOrCreate("SettingsDialog.layout", { navWidth: settingsDialogNav.def });
|
|
17
18
|
/** Register an external section. Re-register with the same key replaces the previous one.
|
|
@@ -22,12 +23,12 @@ export function registerSettingsSection(s) {
|
|
|
22
23
|
registry.list.push(s);
|
|
23
24
|
else
|
|
24
25
|
registry.list.splice(i, 1, s);
|
|
25
|
-
|
|
26
|
+
registryApi.render();
|
|
26
27
|
return () => {
|
|
27
28
|
const j = registry.list.indexOf(s);
|
|
28
29
|
if (j != -1) {
|
|
29
30
|
registry.list.splice(j, 1);
|
|
30
|
-
|
|
31
|
+
registryApi.render();
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
}
|
|
@@ -220,11 +221,7 @@ function filterSettingsTree(roots, terms) {
|
|
|
220
221
|
roots.forEach(visit);
|
|
221
222
|
return { visibleKeys, matchedKeys, expandedKeys };
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
225
|
-
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
226
|
-
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
227
|
-
export function SettingsDialog(props) {
|
|
224
|
+
export function useSettingsDialogController(props) {
|
|
228
225
|
const [open, setOpen] = useState(false);
|
|
229
226
|
const [active, setActive] = useState(props.defaultSection);
|
|
230
227
|
const [search, setSearch] = useState("");
|
|
@@ -233,9 +230,10 @@ export function SettingsDialog(props) {
|
|
|
233
230
|
const [navWidth, setNavWidth] = useState(() => clampSettingsNavWidth(settingsDialogLayout.navWidth));
|
|
234
231
|
const [navResizing, setNavResizing] = useState(false);
|
|
235
232
|
const searchInputRef = useRef(null);
|
|
233
|
+
const searchBoxRef = useRef(null);
|
|
236
234
|
const navResizeRef = useRef(null);
|
|
237
235
|
const navWidthRef = useRef(navWidth);
|
|
238
|
-
|
|
236
|
+
registryApi.use();
|
|
239
237
|
const searchHistory = settingsSearchHistory.use();
|
|
240
238
|
const sections = [...(props.sections ?? []), ...registry.list];
|
|
241
239
|
const tree = buildSettingsTree(sections);
|
|
@@ -309,6 +307,9 @@ export function SettingsDialog(props) {
|
|
|
309
307
|
setHistoryOpen(false);
|
|
310
308
|
setOpen(true);
|
|
311
309
|
}
|
|
310
|
+
function closeDialog() {
|
|
311
|
+
setOpen(false);
|
|
312
|
+
}
|
|
312
313
|
function onTriggerKeyDown(e) {
|
|
313
314
|
if (e.key != "Enter" && e.key != " ")
|
|
314
315
|
return;
|
|
@@ -379,6 +380,21 @@ export function SettingsDialog(props) {
|
|
|
379
380
|
setHistoryOpen(false);
|
|
380
381
|
searchInputRef.current?.focus();
|
|
381
382
|
}
|
|
383
|
+
function clearHistory() {
|
|
384
|
+
settingsSearchHistory.clear();
|
|
385
|
+
setHistoryOpen(false);
|
|
386
|
+
searchInputRef.current?.focus();
|
|
387
|
+
}
|
|
388
|
+
function closeHistoryWhenSearchFocusLeaves(e) {
|
|
389
|
+
const next = e.relatedTarget;
|
|
390
|
+
if (next && e.currentTarget.contains(next))
|
|
391
|
+
return;
|
|
392
|
+
window.setTimeout(() => {
|
|
393
|
+
const box = searchBoxRef.current;
|
|
394
|
+
if (box == null || !box.contains(document.activeElement))
|
|
395
|
+
setHistoryOpen(false);
|
|
396
|
+
}, 0);
|
|
397
|
+
}
|
|
382
398
|
function cycleTreeTool() {
|
|
383
399
|
if (treeToolState == "expanded")
|
|
384
400
|
collapseOutsideCurrent();
|
|
@@ -387,6 +403,52 @@ export function SettingsDialog(props) {
|
|
|
387
403
|
else
|
|
388
404
|
expandAll();
|
|
389
405
|
}
|
|
406
|
+
return {
|
|
407
|
+
open,
|
|
408
|
+
setOpen,
|
|
409
|
+
active,
|
|
410
|
+
setActive,
|
|
411
|
+
search,
|
|
412
|
+
setSearch,
|
|
413
|
+
expanded,
|
|
414
|
+
historyOpen,
|
|
415
|
+
setHistoryOpen,
|
|
416
|
+
navWidth,
|
|
417
|
+
navResizing,
|
|
418
|
+
searchInputRef,
|
|
419
|
+
searchBoxRef,
|
|
420
|
+
tree,
|
|
421
|
+
filtered,
|
|
422
|
+
current,
|
|
423
|
+
currentNode,
|
|
424
|
+
firstVisibleNode,
|
|
425
|
+
searchTerms,
|
|
426
|
+
searchHistory,
|
|
427
|
+
base,
|
|
428
|
+
activeCls,
|
|
429
|
+
treeToolsVisible,
|
|
430
|
+
treeToolState,
|
|
431
|
+
treeToolTitle,
|
|
432
|
+
openDialog,
|
|
433
|
+
closeDialog,
|
|
434
|
+
onTriggerKeyDown,
|
|
435
|
+
toggleExpanded,
|
|
436
|
+
commitSearch,
|
|
437
|
+
useHistoryItem,
|
|
438
|
+
clearHistory,
|
|
439
|
+
commitNavWidth,
|
|
440
|
+
beginNavResize,
|
|
441
|
+
onNavResizeKeyDown,
|
|
442
|
+
closeHistoryWhenSearchFocusLeaves,
|
|
443
|
+
cycleTreeTool,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
|
|
447
|
+
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
448
|
+
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
449
|
+
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
450
|
+
export function SettingsDialog(props) {
|
|
451
|
+
const { open, setOpen, setActive, search, setSearch, expanded, historyOpen, setHistoryOpen, navWidth, navResizing, searchInputRef, searchBoxRef, tree, filtered, current, firstVisibleNode, searchTerms, searchHistory, base, activeCls, treeToolsVisible, treeToolState, treeToolTitle, openDialog, onTriggerKeyDown, toggleExpanded, commitSearch, useHistoryItem, clearHistory, commitNavWidth, beginNavResize, onNavResizeKeyDown, closeHistoryWhenSearchFocusLeaves, cycleTreeTool, } = useSettingsDialogController(props);
|
|
390
452
|
function renderTreeNode(node) {
|
|
391
453
|
if (!filtered.visibleKeys.has(node.section.key))
|
|
392
454
|
return null;
|
|
@@ -415,7 +477,7 @@ export function SettingsDialog(props) {
|
|
|
415
477
|
toggleExpanded(key);
|
|
416
478
|
} }), _jsx("span", { className: "wenayDlgTreeLabel", children: renderHighlightedLabel(node.section.name, searchTerms) })] }), hasChildren && isOpen && node.children.map(renderTreeNode)] }, key);
|
|
417
479
|
}
|
|
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 => {
|
|
480
|
+
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 => {
|
|
419
481
|
setSearch(e.currentTarget.value);
|
|
420
482
|
setHistoryOpen(searchHistory.length != 0);
|
|
421
483
|
}, onKeyDown: e => {
|
|
@@ -433,8 +495,6 @@ export function SettingsDialog(props) {
|
|
|
433
495
|
setHistoryOpen(searchHistory.length != 0);
|
|
434
496
|
searchInputRef.current?.focus();
|
|
435
497
|
}, 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
|
-
|
|
437
|
-
setHistoryOpen(false);
|
|
438
|
-
searchInputRef.current?.focus();
|
|
498
|
+
clearHistory();
|
|
439
499
|
}, 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)] });
|
|
440
500
|
}
|