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.
Files changed (38) hide show
  1. package/lib/common/src/components/Dnd/FloatingWindow.d.ts +29 -12
  2. package/lib/common/src/components/Dnd/FloatingWindow.js +100 -91
  3. package/lib/common/src/components/Input.d.ts +25 -6
  4. package/lib/common/src/components/Input.js +27 -8
  5. package/lib/common/src/components/Menu/RightMenu.d.ts +28 -1
  6. package/lib/common/src/components/Menu/RightMenu.js +62 -35
  7. package/lib/common/src/components/Modal/LeftModal.js +5 -4
  8. package/lib/common/src/components/Modal/Modal.js +8 -7
  9. package/lib/common/src/components/ParamsEditor.d.ts +15 -0
  10. package/lib/common/src/components/ParamsEditor.js +45 -18
  11. package/lib/common/src/components/Settings/SettingsDialog.d.ts +68 -6
  12. package/lib/common/src/components/Settings/SettingsDialog.js +73 -13
  13. package/lib/common/src/components/Toolbar/Toolbar.js +64 -19
  14. package/lib/common/src/components/UiSlot/UiSlot.js +5 -4
  15. package/lib/common/src/grid/columnState/ColumnDots.d.ts +1 -1
  16. package/lib/common/src/grid/columnState/ColumnDots.js +1 -1
  17. package/lib/common/src/grid/columnState/ColumnsMenu.js +10 -18
  18. package/lib/common/src/grid/columnState/columnState.js +8 -6
  19. package/lib/common/src/hooks/useKeyboard.js +4 -3
  20. package/lib/common/src/logs/logs.d.ts +13 -133
  21. package/lib/common/src/logs/logs.js +20 -39
  22. package/lib/common/src/logs/logsContext.d.ts +26 -0
  23. package/lib/common/src/logs/logsContext.js +25 -20
  24. package/lib/common/src/logs/logsController.d.ts +88 -0
  25. package/lib/common/src/logs/logsController.js +54 -0
  26. package/lib/common/src/logs/miniLogs.d.ts +69 -3
  27. package/lib/common/src/logs/miniLogs.js +70 -15
  28. package/lib/common/src/menu/menu.d.ts +15 -3
  29. package/lib/common/src/menu/menu.js +81 -17
  30. package/lib/common/src/menu/menuMouse.d.ts +35 -0
  31. package/lib/common/src/menu/menuMouse.js +113 -4
  32. package/lib/common/src/styles/tokens.d.ts +79 -1
  33. package/lib/common/src/styles/tokens.js +79 -1
  34. package/lib/common/src/utils/memoryStore.d.ts +1 -10
  35. package/lib/common/src/utils/searchHistory.js +4 -3
  36. package/lib/style/style.css +122 -50
  37. package/lib/style/tokens.css +76 -2
  38. 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 DropdownMenu({ elements, style, styles, classNames = {}, trigger = '☰', position: initialPosition = 'right', verticalPosition: initialVerticalPosition = 'top', keyForSave }) {
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 jsxRender = useMemo(() => _jsx(jsx.Render, {}), [jsx]);
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
- // Click and hover handlers
86
- const handleClickOutside = useCallback(() => {
84
+ const close = useCallback(() => {
87
85
  setIsOpen(false);
88
86
  }, []);
89
- const handleToggle = useCallback(() => {
87
+ const toggleFixed = useCallback(() => {
90
88
  setIsFixed((prev) => !prev);
91
89
  setIsOpen((prev) => !prev);
92
90
  }, []);
93
- const handleSelect = useCallback((item, index) => {
91
+ const selectItem = useCallback((item, index) => {
94
92
  jsx.set(item.subMenuContent);
95
93
  setSelect(index);
96
94
  }, [jsx]);
97
- const handleContentMouseEnter = useCallback(() => {
95
+ const contentMouseEnter = useCallback(() => {
98
96
  data.current.m1 = true;
99
97
  }, []);
100
- const handleContentMouseLeave = useCallback(async () => {
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 handleSubmenuMouseEnter = useCallback(() => {
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 handleSubmenuMouseLeave = useCallback(async () => {
112
+ const submenuMouseLeave = useCallback(async () => {
112
113
  data.current.m2 = false;
113
- await sleepAsync(50);
114
- if (!data.current.m1 && !data.current.m2) {
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
- return (_jsxs(OutsideClickArea, { ref: containerRef, outsideClick: handleClickOutside, className: cx(classNames.container ?? 'menu-container', isFixed ? (classNames.activeContainer ?? 'activeM') : undefined), style: containerStyle, onMouseEnter: () => !isFixed && setIsOpen(true), onMouseLeave: () => !isFixed && setIsOpen(false), children: [_jsx("div", { ...dragProps, className: classNames.trigger ?? "menu-button", style: styles?.trigger, onClick: handleToggle, children: renderTrigger(trigger, { isOpen, isFixed }) }), dop] }));
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 { renderBy, updateBy } from "../../../updateBy";
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
- renderBy(menuStore);
246
+ menuStoreApi.render();
246
247
  }, [menu, menuKey]);
247
- updateBy(menuStore);
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() { renderBy(menuStore); },
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 { renderBy, updateBy } from "../../../updateBy";
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
- renderBy(data);
35
+ dataApi.render();
36
36
  },
37
37
  set JSX(jsx) {
38
38
  _jsx = jsx;
39
- renderBy(data);
39
+ dataApi.render();
40
40
  },
41
41
  get JSX() { return _jsx; },
42
42
  Render() {
43
- updateBy(data);
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
- renderBy(data);
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
- renderBy(data);
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
- updateBy(data);
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 [_, setUpdate] = useState(0);
178
- function Refresh() { setUpdate(e => ++e); }
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
- data.onChange(myParams.current);
220
+ controller.notifyChange();
190
221
  }
191
222
  function refreshIndicatorDelayed() {
192
- if (timeoutId.current != null)
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
- data.onExpand?.(myParams.current);
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
- /** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
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
- }): import("react/jsx-runtime").JSX.Element;
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 { renderBy, updateBy } from "../../../updateBy";
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
- renderBy(registry);
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
- renderBy(registry);
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
- /** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
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
- updateBy(registry);
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
- settingsSearchHistory.clear();
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
  }