wenay-react2 1.0.39 → 1.0.41

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 (54) hide show
  1. package/doc/EXAMPLE_USAGE.md +915 -0
  2. package/doc/PROJECT_FUNCTIONALITY.md +401 -0
  3. package/doc/PROJECT_RULES.md +80 -0
  4. package/doc/WENAY_REACT2_RENAMES.md +177 -0
  5. package/doc/changes/v1.0.35.md +26 -0
  6. package/doc/changes/v1.0.37.md +13 -0
  7. package/doc/changes/v1.0.38.md +48 -0
  8. package/doc/changes/v1.0.39.md +35 -0
  9. package/doc/changes/v1.0.40.md +15 -0
  10. package/doc/changes/v1.0.41.md +13 -0
  11. package/doc/progress/README.md +14 -0
  12. package/doc/progress/hook-controller-opportunities.md +367 -0
  13. package/doc/progress/hook-extraction-audit.md +195 -0
  14. package/doc/progress/public-surface-normalization.md +368 -0
  15. package/doc/progress/style-system-normalization.md +121 -0
  16. package/doc/target/README.md +33 -0
  17. package/doc/target/my.md +109 -0
  18. package/doc/wenay-react2-1.0.8.tgz +0 -0
  19. package/doc/wenay-react2-rare.md +702 -0
  20. package/doc/wenay-react2.md +475 -0
  21. package/lib/common/src/components/Dnd/FloatingWindow.d.ts +29 -12
  22. package/lib/common/src/components/Dnd/FloatingWindow.js +100 -91
  23. package/lib/common/src/components/Menu/RightMenu.d.ts +28 -1
  24. package/lib/common/src/components/Menu/RightMenu.js +62 -35
  25. package/lib/common/src/components/Modal/LeftModal.js +5 -4
  26. package/lib/common/src/components/Modal/Modal.js +8 -7
  27. package/lib/common/src/components/ParamsEditor.d.ts +15 -0
  28. package/lib/common/src/components/ParamsEditor.js +45 -18
  29. package/lib/common/src/components/Settings/SettingsDialog.d.ts +68 -6
  30. package/lib/common/src/components/Settings/SettingsDialog.js +61 -12
  31. package/lib/common/src/components/Toolbar/Toolbar.js +64 -19
  32. package/lib/common/src/components/UiSlot/UiSlot.js +5 -4
  33. package/lib/common/src/grid/columnState/columnState.js +8 -6
  34. package/lib/common/src/hooks/useKeyboard.js +4 -3
  35. package/lib/common/src/logs/logs.d.ts +40 -133
  36. package/lib/common/src/logs/logs.js +96 -66
  37. package/lib/common/src/logs/logsContext.d.ts +26 -0
  38. package/lib/common/src/logs/logsContext.js +25 -20
  39. package/lib/common/src/logs/logsController.d.ts +88 -0
  40. package/lib/common/src/logs/logsController.js +54 -0
  41. package/lib/common/src/logs/miniLogs.d.ts +69 -3
  42. package/lib/common/src/logs/miniLogs.js +70 -15
  43. package/lib/common/src/menu/menu.d.ts +15 -3
  44. package/lib/common/src/menu/menu.js +81 -17
  45. package/lib/common/src/menu/menuMouse.d.ts +18 -0
  46. package/lib/common/src/menu/menuMouse.js +43 -1
  47. package/lib/common/src/styles/tokens.d.ts +7 -6
  48. package/lib/common/src/styles/tokens.js +8 -5
  49. package/lib/common/src/utils/memoryStore.d.ts +1 -10
  50. package/lib/common/src/utils/searchHistory.js +4 -3
  51. package/lib/style/menuRight.css +29 -23
  52. package/lib/style/style.css +15 -9
  53. package/lib/style/tokens.css +8 -4
  54. package/package.json +50 -49
@@ -1,13 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
3
3
  import { Rnd } from "react-rnd";
4
- import { renderBy, updateBy } from "../../../updateBy";
4
+ import { createUpdateApi } from "../../../updateBy";
5
5
  import { ObservableMap } from "../../utils/observableMap";
6
6
  // Map of all popup window sizes; observable - memoryCache marks itself dirty on its mutations
7
7
  export const floatingWindowMap = new ObservableMap();
8
8
  // limit={{x:{min:0}, y:{min:0}}}
9
9
  let k = 0;
10
10
  const openWindows = { ar: [] };
11
+ const openWindowsApi = createUpdateApi(openWindows);
11
12
  // Freezes the subtree until update changes (intentionally ignores render closure changes) -
12
13
  // the previous useMemo-in-callback semantics, but without calling a hook from an arbitrary place
13
14
  const MemoChild = React.memo(({ update, render }) => render(update), (prev, next) => prev.update === next.update);
@@ -17,25 +18,15 @@ export const FloatingWindow = (a) => {
17
18
  const ff = (update) => _jsx(MemoChild, { update: isFunc ? update : 0, render: renderChild });
18
19
  return _jsx(FloatingWindowBase, { ...a, children: ff });
19
20
  };
20
- /**
21
- * Wrapper component around react-rnd.
22
- * Provides dragging and resizing, an optional header, and a close button.
23
- */
24
- export function FloatingWindowBase({ children, keyForSave: ks, position, size, overflow = true, zIndex = 9, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, sizeByWindow = true }) {
25
- // NOTE: no implicit limit for onCLickClose windows. `limit` is parent-relative,
26
- // so {y:{min:0}} pinned windows to their DOM parent's top: a window opened from a
27
- // centered wrapper (ModalProvider / ModalWrapper at top:50%) could not be dragged
28
- // above mid-screen. Keeping the window on screen is already handled by the
29
- // viewport clamp below (header rect vs viewport in useLayoutEffect).
21
+ export function useFloatingWindowController({ keyForSave: ks, position, size, zIndex = 9, onUpdate, limit, sizeByWindow = true } = {}) {
30
22
  const positionDef = { x: 0, y: 0, ...(position ?? {}) };
31
23
  const sizeDef = { height: 0, width: 0, ...(size ?? {}) };
32
- // If there is a key, store position and size data in the map
33
24
  let map;
34
25
  if (ks) {
35
26
  map = floatingWindowMap.get(ks) ?? floatingWindowMap.set(ks, { size: sizeDef, position: positionDef }).get(ks);
36
27
  }
37
- position = map?.position ?? positionDef;
38
- size = map?.size ?? sizeDef;
28
+ const savedPosition = map?.position ?? positionDef;
29
+ const savedSize = map?.size ?? sizeDef;
39
30
  const id2 = useRef({ k: k++ });
40
31
  const id = id2.current;
41
32
  const [zIndexX, setZIndexX] = useState(0);
@@ -43,32 +34,24 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
43
34
  const lastT = useRef(null);
44
35
  const [a, setA] = useState(false);
45
36
  const [b, setB] = useState(false);
46
- const [x, setX] = useState(position.x);
47
- const [y, setY] = useState(position.y);
48
- const [width, setWidth] = useState(size.width);
49
- const [height, setHeight] = useState(size.height);
37
+ const [x, setX] = useState(savedPosition.x);
38
+ const [y, setY] = useState(savedPosition.y);
39
+ const [width, setWidth] = useState(savedSize.width);
40
+ const [height, setHeight] = useState(savedSize.height);
50
41
  const [update, setUpdate] = useState(0);
51
42
  const zindex = useRef(zIndexX);
52
43
  zindex.current = zIndexX;
53
- // Update the window zIndex if it was brought to the top
54
- updateBy(openWindows, () => {
44
+ openWindowsApi.use(() => {
55
45
  const z = openWindows.ar.findIndex((v) => v.k === id.k);
56
46
  if (z >= 0 && z !== zindex.current) {
57
47
  setZIndexX(z);
58
48
  }
59
49
  });
60
- // limit via ref: an inline object in props must not resubscribe document listeners on every render
61
50
  const limitRef = useRef(limit);
62
51
  useLayoutEffect(() => { limitRef.current = limit; });
63
- /**
64
- * Hook for mouse dragging (a) and touch-device dragging (b).
65
- * Remove subscriptions on unmount and when dependencies change (a/b).
66
- */
67
52
  useEffect(() => {
68
- // Mouse
69
53
  const mouseMoveHandler = (e) => {
70
54
  e.stopPropagation();
71
- // mousedown always sets lastC before this subscription exists
72
55
  if (lastC.current == null)
73
56
  return;
74
57
  const data = lastC.current;
@@ -98,12 +81,9 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
98
81
  document.removeEventListener("mousemove", mouseMoveHandler);
99
82
  lastC.current = null;
100
83
  setA(false);
101
- // drag end commits geometry mutated in place - invisible to the map, so announce
102
- // it; a no-move click also lands here, the save-side diff turns that into a no-op
103
84
  if (ks)
104
85
  floatingWindowMap.touch(ks);
105
86
  };
106
- // Touch
107
87
  const touchMoveHandler = (e) => {
108
88
  const data = lastT.current;
109
89
  if (!data)
@@ -150,17 +130,14 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
150
130
  floatingWindowMap.touch(ks);
151
131
  }
152
132
  };
153
- // If mouse dragging mode is active
154
133
  if (a) {
155
134
  document.addEventListener("mousemove", mouseMoveHandler);
156
135
  document.addEventListener("mouseup", mouseUpHandler);
157
136
  }
158
- // If touch-event dragging mode is active
159
137
  if (b) {
160
138
  document.addEventListener("touchmove", touchMoveHandler);
161
139
  document.addEventListener("touchend", touchEndHandler);
162
140
  }
163
- // Return the cleanup function for unmount and a/b changes:
164
141
  return () => {
165
142
  document.removeEventListener("mousemove", mouseMoveHandler);
166
143
  document.removeEventListener("mouseup", mouseUpHandler);
@@ -168,30 +145,22 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
168
145
  document.removeEventListener("touchend", touchEndHandler);
169
146
  };
170
147
  }, [a, b]);
171
- // On the first render, add the window to the array; remove it on unmount
172
148
  useEffect(() => {
173
149
  openWindows.ar.push(id);
174
- renderBy(openWindows);
150
+ openWindowsApi.render();
175
151
  return () => {
176
152
  const z = openWindows.ar.findIndex((v) => v.k === id.k);
177
153
  if (z >= 0) {
178
154
  openWindows.ar.splice(z, 1);
179
- renderBy(openWindows);
155
+ openWindowsApi.render();
180
156
  }
181
157
  };
182
158
  }, []);
183
- // Update coordinates and size in the map if keyForSave is set
184
- if (size) {
185
- size.height = height;
186
- size.width = width;
187
- }
188
- if (position) {
189
- position.x = x;
190
- position.y = y;
191
- }
159
+ savedSize.height = height;
160
+ savedSize.width = width;
161
+ savedPosition.x = x;
162
+ savedPosition.y = y;
192
163
  const headerRef = useRef(null);
193
- // clamp to the viewport in a layout effect: an inline ref-callback ran twice per render
194
- // (null + element) with a forced reflow from getBoundingClientRect on each call
195
164
  useLayoutEffect(() => {
196
165
  const el = headerRef.current;
197
166
  if (!el || !sizeByWindow)
@@ -208,39 +177,88 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
208
177
  setHeight(window.innerHeight);
209
178
  }
210
179
  }, [x, y, width, height, sizeByWindow]);
211
- const headerD = (_jsx("div", { ref: headerRef, className: "wenayWndHeader", onTouchStart: (e) => {
212
- const t = e.changedTouches[0];
213
- if (t) {
214
- lastT.current = {
215
- x: x - t.clientX,
216
- y: y - t.clientY,
217
- id: t.identifier
218
- };
219
- }
220
- setB(true);
221
- }, onMouseDown: (e) => {
222
- lastC.current = {
223
- x: x - e.clientX,
224
- y: y - e.clientY
180
+ const onHeaderTouchStart = (e) => {
181
+ const t = e.changedTouches[0];
182
+ if (t) {
183
+ lastT.current = {
184
+ x: x - t.clientX,
185
+ y: y - t.clientY,
186
+ id: t.identifier
225
187
  };
226
- setA(true);
227
- }, children: header ?? _jsx("div", { className: "wenayWndHeaderDef" }) }));
188
+ }
189
+ setB(true);
190
+ };
191
+ const onHeaderMouseDown = (e) => {
192
+ lastC.current = {
193
+ x: x - e.clientX,
194
+ y: y - e.clientY
195
+ };
196
+ setA(true);
197
+ };
198
+ const onWindowMouseDown = () => {
199
+ const z = openWindows.ar.findIndex((v) => v === id);
200
+ if (z !== openWindows.ar.length - 1 || zindex.current !== z) {
201
+ const buf = openWindows.ar[z];
202
+ openWindows.ar.splice(z, 1);
203
+ openWindows.ar.push(buf);
204
+ openWindowsApi.render();
205
+ }
206
+ };
207
+ const onResizeStop = (e, dir, elementRef, delta, { x: nx, y: ny }) => {
208
+ setX(nx);
209
+ setY(ny);
210
+ setHeight(elementRef.offsetHeight);
211
+ setWidth(elementRef.offsetWidth);
212
+ setUpdate(update + 1);
213
+ if (ks)
214
+ floatingWindowMap.touch(ks);
215
+ };
216
+ const onResize = (e, dir, elementRef, delta, pos) => {
217
+ onUpdate?.({ e, dir, elementRef, delta, position: pos });
218
+ };
219
+ const windowZIndex = zIndexX * 2 + zIndex;
220
+ return {
221
+ position: { x, y },
222
+ size: { width, height },
223
+ update,
224
+ stackIndex: zIndexX,
225
+ zIndex: windowZIndex,
226
+ overlayZIndex: windowZIndex + 1,
227
+ dragging: a || b,
228
+ headerRef,
229
+ onHeaderTouchStart,
230
+ onHeaderMouseDown,
231
+ onWindowMouseDown,
232
+ onResize,
233
+ onResizeStop,
234
+ };
235
+ }
236
+ /**
237
+ * Wrapper component around react-rnd.
238
+ * Provides dragging and resizing, an optional header, and a close button.
239
+ */
240
+ export function FloatingWindowBase({ children, keyForSave: ks, position, size, overflow = true, zIndex = 9, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, sizeByWindow = true }) {
241
+ // NOTE: no implicit limit for onCLickClose windows. `limit` is parent-relative,
242
+ // so {y:{min:0}} pinned windows to their DOM parent's top: a window opened from a
243
+ // centered wrapper (ModalProvider / ModalWrapper at top:50%) could not be dragged
244
+ // above mid-screen. Keeping the window on screen is already handled by the
245
+ // viewport clamp below (header rect vs viewport in useLayoutEffect).
246
+ const controller = useFloatingWindowController({
247
+ keyForSave: ks,
248
+ position,
249
+ size,
250
+ zIndex,
251
+ onUpdate,
252
+ disableDragging,
253
+ limit,
254
+ sizeByWindow,
255
+ });
256
+ const headerD = (_jsx("div", { ref: controller.headerRef, className: "wenayWndHeader", onTouchStart: controller.onHeaderTouchStart, onMouseDown: controller.onHeaderMouseDown, children: header ?? _jsx("div", { className: "wenayWndHeaderDef" }) }));
228
257
  return (_jsx(Rnd, { disableDragging: true, style: {
229
- zIndex: zIndexX * 2 + zIndex
230
- }, className: className, onResizeStop: (e, dir, elementRef, delta, { x: nx, y: ny }) => {
231
- setX(nx);
232
- setY(ny);
233
- // actual element size: `+height + delta.height` gave NaN for string sizes like "100%"
234
- setHeight(elementRef.offsetHeight);
235
- setWidth(elementRef.offsetWidth);
236
- setUpdate(update + 1);
237
- if (ks)
238
- floatingWindowMap.touch(ks);
239
- }, onResize: (e, dir, elementRef, delta, pos) => {
240
- onUpdate?.({ e, dir, elementRef, delta, position: pos });
241
- }, position: { x, y }, size: { width, height }, default: {
242
- ...position,
243
- ...size
258
+ zIndex: controller.zIndex
259
+ }, className: className, onResizeStop: controller.onResizeStop, onResize: controller.onResize, position: controller.position, size: controller.size, default: {
260
+ ...controller.position,
261
+ ...controller.size
244
262
  }, children: _jsxs("div", { className: "wenayWnd", style: {
245
263
  width: "100%",
246
264
  height: "100%",
@@ -248,20 +266,11 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
248
266
  flexDirection: "column",
249
267
  position: "relative",
250
268
  flex: "auto"
251
- }, onMouseDown: () => {
252
- // Bring the window to the top
253
- const z = openWindows.ar.findIndex((v) => v === id);
254
- if (z !== openWindows.ar.length - 1 || zindex.current !== z) {
255
- const buf = openWindows.ar[z];
256
- openWindows.ar.splice(z, 1);
257
- openWindows.ar.push(buf);
258
- renderBy(openWindows);
259
- }
260
- }, children: [moveOnlyHeader || header ? headerD : null, _jsxs("div", { className: "maxSize", style: { overflow: overflow ? "auto" : undefined }, children: [(a || b) && (_jsx("div", { className: "maxSize", style: {
269
+ }, onMouseDown: controller.onWindowMouseDown, children: [moveOnlyHeader || header ? headerD : null, _jsxs("div", { className: "maxSize", style: { overflow: overflow ? "auto" : undefined }, children: [controller.dragging && (_jsx("div", { className: "maxSize", style: {
261
270
  position: "absolute",
262
- zIndex: zIndexX * 2 + zIndex + 1
263
- } })), typeof children === "function" ? children(update) : children] }), onCLickClose && (_jsx("div", { className: "wenayCloseBtn wenayWndClose", title: "Close", style: {
264
- zIndex: zIndexX * 2 + zIndex + 1
271
+ zIndex: controller.overlayZIndex
272
+ } })), typeof children === "function" ? children(controller.update) : children] }), onCLickClose && (_jsx("div", { className: "wenayCloseBtn wenayWndClose", title: "Close", style: {
273
+ zIndex: controller.overlayZIndex
265
274
  }, onClick: onCLickClose, 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" }) }) }, "323"))] }) }));
266
275
  }
267
276
  /**
@@ -1,4 +1,5 @@
1
1
  import React, { JSX } from 'react';
2
+ import { useDraggable } from "../../hooks/useDraggable";
2
3
  import { type MenuRightPosition, type MenuRightVerticalPosition } from "./RightMenuStore";
3
4
  export type MenuElement = {
4
5
  label: string;
@@ -32,7 +33,33 @@ export type DropdownMenuProps = {
32
33
  keyForSave?: string;
33
34
  };
34
35
  export type MenuRightRenderProps = Omit<DropdownMenuProps, 'elements'>;
35
- export declare function DropdownMenu({ elements, style, styles, classNames, trigger, position: initialPosition, verticalPosition: initialVerticalPosition, keyForSave }: DropdownMenuProps): import("react/jsx-runtime").JSX.Element;
36
+ export type UseRightMenuControllerOptions = Pick<DropdownMenuProps, 'elements' | 'style' | 'styles' | 'position' | 'verticalPosition' | 'keyForSave'>;
37
+ export type RightMenuController = {
38
+ elements: MenuElement[];
39
+ isOpen: boolean;
40
+ isFixed: boolean;
41
+ select: number | null;
42
+ position: MenuRightPosition;
43
+ isTop: boolean;
44
+ containerRef: React.RefObject<HTMLDivElement | null>;
45
+ dragProps: ReturnType<typeof useDraggable>['dragProps'];
46
+ triggerState: MenuRightTriggerState;
47
+ containerStyle: React.CSSProperties;
48
+ flyoutStyle: React.CSSProperties;
49
+ submenuRender: React.ReactElement;
50
+ hasSubmenu: boolean;
51
+ open(): void;
52
+ close(): void;
53
+ toggleFixed(): void;
54
+ setOpen(open: boolean): void;
55
+ selectItem(item: MenuElement, index: number): void;
56
+ contentMouseEnter(): void;
57
+ contentMouseLeave(): Promise<void>;
58
+ submenuMouseEnter(): void;
59
+ submenuMouseLeave(): Promise<void>;
60
+ };
61
+ export declare function useRightMenuController({ elements, style, styles, position: initialPosition, verticalPosition: initialVerticalPosition, keyForSave }: UseRightMenuControllerOptions): RightMenuController;
62
+ export declare function DropdownMenu({ elements, style, styles, classNames, trigger, position, verticalPosition, keyForSave }: DropdownMenuProps): import("react/jsx-runtime").JSX.Element;
36
63
  export declare function createRightMenuController(): {
37
64
  set(array: MenuElement[]): void;
38
65
  delete(array: MenuElement[]): void;
@@ -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;