wenay-react2 1.0.41 → 1.0.42

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 (58) hide show
  1. package/doc/EXAMPLE_USAGE.md +58 -2
  2. package/doc/PROJECT_FUNCTIONALITY.md +5 -1
  3. package/doc/changes/v1.0.41.md +23 -0
  4. package/doc/changes/v1.0.42.md +26 -0
  5. package/doc/progress/architecture-fix-queue.md +74 -0
  6. package/doc/progress/style-system-normalization.md +5 -5
  7. package/doc/target/my.md +39 -67
  8. package/doc/wenay-react2-rare.md +118 -13
  9. package/doc/wenay-react2.md +75 -11
  10. package/lib/common/api.d.ts +1 -0
  11. package/lib/common/api.js +8 -4
  12. package/lib/common/src/components/Dnd/DragArea.d.ts +5 -0
  13. package/lib/common/src/components/Dnd/DragArea.js +5 -1
  14. package/lib/common/src/components/Dnd/FloatingWindow.d.ts +9 -5
  15. package/lib/common/src/components/Dnd/FloatingWindow.js +39 -97
  16. package/lib/common/src/components/Dnd/Resizable.d.ts +3 -7
  17. package/lib/common/src/components/Dnd/Resizable.js +4 -3
  18. package/lib/common/src/components/Menu/RightMenu.js +8 -4
  19. package/lib/common/src/components/Menu/RightMenuStore.d.ts +2 -2
  20. package/lib/common/src/components/Menu/RightMenuStore.js +5 -3
  21. package/lib/common/src/components/Modal/LeftModal.js +4 -2
  22. package/lib/common/src/components/Modal/ModalContextProvider.js +5 -16
  23. package/lib/common/src/components/MyResizeObserver.d.ts +24 -0
  24. package/lib/common/src/components/MyResizeObserver.js +49 -0
  25. package/lib/common/src/components/Overlay.d.ts +24 -0
  26. package/lib/common/src/components/Overlay.js +28 -0
  27. package/lib/common/src/components/Settings/SettingsDialog.js +21 -22
  28. package/lib/common/src/components/Toolbar/Toolbar.d.ts +1 -0
  29. package/lib/common/src/components/Toolbar/Toolbar.js +16 -23
  30. package/lib/common/src/grid/columnState/ColumnsMenu.js +4 -13
  31. package/lib/common/src/grid/columnState/columnGrid.d.ts +103 -0
  32. package/lib/common/src/grid/columnState/columnGrid.js +231 -0
  33. package/lib/common/src/grid/columnState/columnState.js +10 -9
  34. package/lib/common/src/grid/columnState/index.js +3 -0
  35. package/lib/common/src/hooks/useDraggable.d.ts +8 -0
  36. package/lib/common/src/hooks/useDraggable.js +13 -4
  37. package/lib/common/src/hooks/useOutside.d.ts +4 -1
  38. package/lib/common/src/hooks/useOutside.js +2 -1
  39. package/lib/common/src/logs/logs.d.ts +96 -5
  40. package/lib/common/src/logs/logs.js +147 -108
  41. package/lib/common/src/logs/logsController.d.ts +3 -0
  42. package/lib/common/src/utils/cache.d.ts +12 -0
  43. package/lib/common/src/utils/cache.js +0 -0
  44. package/lib/common/src/utils/fixedOrder.d.ts +15 -0
  45. package/lib/common/src/utils/fixedOrder.js +30 -0
  46. package/lib/common/src/utils/index.d.ts +2 -0
  47. package/lib/common/src/utils/index.js +2 -0
  48. package/lib/common/src/utils/memoryStore.d.ts +3 -6
  49. package/lib/common/src/utils/memoryStore.js +1 -3
  50. package/lib/common/src/utils/persistedMaps.d.ts +16 -0
  51. package/lib/common/src/utils/persistedMaps.js +5 -0
  52. package/lib/common/src/utils/searchHistory.js +7 -6
  53. package/lib/common/src/utils/structEqual.d.ts +12 -0
  54. package/lib/common/src/utils/structEqual.js +40 -0
  55. package/lib/common/updateBy.d.ts +3 -0
  56. package/lib/common/updateBy.js +57 -22
  57. package/lib/style/style.css +58 -1
  58. package/package.json +2 -2
@@ -2,9 +2,11 @@ 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
4
  import { createUpdateApi } from "../../../updateBy";
5
- import { ObservableMap } from "../../utils/observableMap";
6
- // Map of all popup window sizes; observable - memoryCache marks itself dirty on its mutations
7
- export const floatingWindowMap = new ObservableMap();
5
+ import { floatingWindowMap } from "../../utils/persistedMaps";
6
+ import { useDraggableApi } from "../../hooks/useDraggable";
7
+ // Map of all popup window sizes; declared in utils/persistedMaps (memoryCache registry must not
8
+ // import the component layer) and re-exported here so the public surface is unchanged
9
+ export { floatingWindowMap };
8
10
  // limit={{x:{min:0}, y:{min:0}}}
9
11
  let k = 0;
10
12
  const openWindows = { ar: [] };
@@ -237,8 +239,9 @@ export function useFloatingWindowController({ keyForSave: ks, position, size, zI
237
239
  * Wrapper component around react-rnd.
238
240
  * Provides dragging and resizing, an optional header, and a close button.
239
241
  */
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
+ export function FloatingWindowBase({ children, keyForSave: ks, position, size, overflow = true, zIndex = 9, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, onClickClose, sizeByWindow = true }) {
243
+ const clickClose = onClickClose ?? onCLickClose;
244
+ // NOTE: no implicit limit for onClickClose windows. `limit` is parent-relative,
242
245
  // so {y:{min:0}} pinned windows to their DOM parent's top: a window opened from a
243
246
  // centered wrapper (ModalProvider / ModalWrapper at top:50%) could not be dragged
244
247
  // above mid-screen. Keeping the window on screen is already handled by the
@@ -269,9 +272,9 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
269
272
  }, 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: {
270
273
  position: "absolute",
271
274
  zIndex: controller.overlayZIndex
272
- } })), typeof children === "function" ? children(controller.update) : children] }), onCLickClose && (_jsx("div", { className: "wenayCloseBtn wenayWndClose", title: "Close", style: {
275
+ } })), typeof children === "function" ? children(controller.update) : children] }), clickClose && (_jsx("div", { className: "wenayCloseBtn wenayWndClose", title: "Close", style: {
273
276
  zIndex: controller.overlayZIndex
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"))] }) }));
277
+ }, onClick: clickClose, 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"))] }) }));
275
278
  }
276
279
  /**
277
280
  * Wrapper component that lets a nested element be dragged
@@ -280,111 +283,50 @@ export function FloatingWindowBase({ children, keyForSave: ks, position, size, o
280
283
  * Function only as a hook for parameter changes during movement, although it has its own component (for offset counting).
281
284
  * Returns the distance traveled when moving the child element.
282
285
  */
283
- export function DragBox({ children, onX, onY, x = 0, y = 0, right = false, last, dragging, onStart, onStop }) {
284
- const offsetMouse = useRef({ x: 0, y: 0 });
285
- const offsetTouch = useRef(null);
286
- const [draggingMouse, setDraggingMouse] = useState(false);
287
- const [draggingTouch, setDraggingTouch] = useState(false);
286
+ export function DragBox({ children, onX, onY, x = 0, y = 0, right = false, last, dragging: _dragging, // accepted for compatibility, was never read
287
+ onStart, onStop }) {
288
+ // Thin adapter over useDraggableApi (A7): same observable contract as the old
289
+ // bespoke loop - immediate start, per-tick imperative onX/onY with the delta from
290
+ // the press point, NO re-render per move tick, posRef keeps the last delta after
291
+ // release (reset happens on the next gesture start inside the hook).
288
292
  const posRef = useRef(last?.current ?? { x, y });
289
- const wasDragging = useRef(false);
290
293
  const callbacksRef = useRef({ onX, onY, onStart, onStop });
291
294
  callbacksRef.current = { onX, onY, onStart, onStop };
295
+ const api = useDraggableApi({
296
+ holdMs: 0,
297
+ trackState: false,
298
+ onDragStart() {
299
+ // per-gesture reset - without it repeated drags accumulated offset
300
+ posRef.current.x = 0;
301
+ posRef.current.y = 0;
302
+ callbacksRef.current.onStart?.();
303
+ },
304
+ onMove(p) {
305
+ // mutate in place: `last` shares this object (see layout effect below)
306
+ posRef.current.x = p.x;
307
+ posRef.current.y = p.y;
308
+ callbacksRef.current.onX?.(p.x);
309
+ callbacksRef.current.onY?.(p.y);
310
+ },
311
+ onDragEnd(final) {
312
+ posRef.current.x = final.x;
313
+ posRef.current.y = final.y;
314
+ callbacksRef.current.onStop?.();
315
+ },
316
+ });
292
317
  useLayoutEffect(() => {
293
318
  posRef.current.x = x;
294
319
  posRef.current.y = y;
295
320
  }, [x, y]);
296
- useEffect(() => {
297
- if (!draggingMouse && !draggingTouch) {
298
- // onStop only after a real drag - previously it also fired on initial mount
299
- if (wasDragging.current) {
300
- wasDragging.current = false;
301
- callbacksRef.current.onStop?.();
302
- }
303
- return;
304
- }
305
- wasDragging.current = true;
306
- if (draggingMouse) {
307
- const handleMouseMove = (e) => {
308
- const newX = e.clientX + offsetMouse.current.x;
309
- const newY = e.clientY + offsetMouse.current.y;
310
- posRef.current = { x: newX, y: newY };
311
- callbacksRef.current.onX?.(newX);
312
- callbacksRef.current.onY?.(newY);
313
- };
314
- const handleMouseUp = () => {
315
- offsetMouse.current.x = 0;
316
- offsetMouse.current.y = 0;
317
- document.removeEventListener("mousemove", handleMouseMove);
318
- document.removeEventListener("mouseup", handleMouseUp);
319
- setDraggingMouse(false);
320
- };
321
- document.addEventListener("mousemove", handleMouseMove);
322
- document.addEventListener("mouseup", handleMouseUp);
323
- callbacksRef.current.onStart?.();
324
- return () => {
325
- document.removeEventListener("mousemove", handleMouseMove);
326
- document.removeEventListener("mouseup", handleMouseUp);
327
- };
328
- }
329
- if (draggingTouch) {
330
- const handleTouchMove = (e) => {
331
- if (!offsetTouch.current)
332
- return;
333
- const theTouch = Array.from(e.changedTouches).find((t) => t.identifier === offsetTouch.current?.id);
334
- if (!theTouch)
335
- return;
336
- const newX = theTouch.clientX + offsetTouch.current.x;
337
- const newY = theTouch.clientY + offsetTouch.current.y;
338
- posRef.current = { x: newX, y: newY };
339
- callbacksRef.current.onX?.(newX);
340
- callbacksRef.current.onY?.(newY);
341
- };
342
- const handleTouchEnd = (e) => {
343
- if (!offsetTouch.current)
344
- return;
345
- const ended = Array.from(e.changedTouches).find((t) => t.identifier === offsetTouch.current?.id);
346
- if (ended) {
347
- offsetTouch.current = null;
348
- document.removeEventListener("touchmove", handleTouchMove);
349
- document.removeEventListener("touchend", handleTouchEnd);
350
- setDraggingTouch(false);
351
- }
352
- };
353
- document.addEventListener("touchmove", handleTouchMove);
354
- document.addEventListener("touchend", handleTouchEnd);
355
- callbacksRef.current.onStart?.();
356
- return () => {
357
- document.removeEventListener("touchmove", handleTouchMove);
358
- document.removeEventListener("touchend", handleTouchEnd);
359
- };
360
- }
361
- }, [draggingMouse, draggingTouch]);
362
321
  useLayoutEffect(() => {
363
322
  if (last) {
364
323
  last.current = posRef.current;
365
324
  }
366
325
  });
367
- const handleMouseDown = (e) => {
368
- e.preventDefault();
369
- posRef.current.x = 0;
370
- posRef.current.y = 0;
371
- offsetMouse.current = { x: -e.clientX, y: -e.clientY };
372
- setDraggingMouse(true);
373
- };
374
- const handleTouchStart = (e) => {
375
- const t = e.changedTouches[0];
376
- if (!t)
377
- return;
378
- // same per-gesture reset as mouse - without it repeated touch drags accumulated offset
379
- posRef.current.x = 0;
380
- posRef.current.y = 0;
381
- offsetTouch.current = { x: -t.clientX, y: -t.clientY, id: t.identifier };
382
- setDraggingTouch(true);
383
- };
384
326
  return (_jsx("div", { style: {
385
327
  position: "absolute",
386
328
  left: right ? undefined : 0,
387
329
  right: right ? 0 : undefined,
388
330
  top: 0
389
- }, onMouseDown: handleMouseDown, onTouchStart: handleTouchStart, children: children }));
331
+ }, ...api.dragProps, children: children }));
390
332
  }
@@ -1,10 +1,7 @@
1
1
  import { ResizableProps } from "re-resizable";
2
- import { ObservableMap } from "../../utils/observableMap";
3
- type tSaveMap = {
4
- height?: number | string;
5
- width?: number | string;
6
- };
7
- export declare const mapResiReact: ObservableMap<string, tSaveMap>;
2
+ import { mapResiReact, type ResizableSavedSize } from "../../utils/persistedMaps";
3
+ type tSaveMap = ResizableSavedSize;
4
+ export { mapResiReact };
8
5
  type t3 = Pick<ResizableProps, "style" | "enable" | "onResize" | "children" | "size" | "maxWidth" | "maxHeight" | "minWidth" | "minHeight">;
9
6
  export declare function FResizableReact({ style, onResize, enable, children, keyForSave, onResizeStop, size, minWidth, minHeight, maxWidth, maxHeight, moveWith, moveHeight }: t3 & {
10
7
  keyForSave?: string;
@@ -13,4 +10,3 @@ export declare function FResizableReact({ style, onResize, enable, children, key
13
10
  moveWith?: boolean;
14
11
  moveHeight?: boolean;
15
12
  }): import("react/jsx-runtime").JSX.Element;
16
- export {};
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Resizable } from "re-resizable";
3
- import { ObservableMap } from "../../utils/observableMap";
4
- // Memory for all column sizes; observable - memoryCache marks itself dirty on its mutations
5
- export const mapResiReact = new ObservableMap();
3
+ import { mapResiReact } from "../../utils/persistedMaps";
4
+ // Memory for all column sizes; declared in utils/persistedMaps (memoryCache registry must not
5
+ // import the component layer) and re-exported here so the public surface is unchanged
6
+ export { mapResiReact };
6
7
  export function FResizableReact({ style, onResize, enable, children, keyForSave, onResizeStop, size = { height: 50, width: 50 }, minWidth, minHeight, maxWidth = "100%", maxHeight = "100%", moveWith = true, moveHeight = true }) {
7
8
  let obj = size;
8
9
  if (keyForSave) {
@@ -125,20 +125,24 @@ export function useRightMenuController({ elements, style, styles, position: init
125
125
  ...styles?.container,
126
126
  display: 'flex'
127
127
  };
128
+ // runs during render: guard for DOM-less environments (SSR/tests) - Infinity makes
129
+ // the clamp a no-op there; on the client the behavior is unchanged
130
+ const vw = typeof window != 'undefined' ? window.innerWidth : Infinity;
131
+ const vh = typeof window != 'undefined' ? window.innerHeight : Infinity;
128
132
  if (position === 'left') {
129
- computedStyle.left = Math.max(0, Math.min(computedX, window.innerWidth - 50));
133
+ computedStyle.left = Math.max(0, Math.min(computedX, vw - 50));
130
134
  computedStyle.right = 'auto';
131
135
  }
132
136
  else {
133
- computedStyle.right = Math.max(0, Math.min(computedX, window.innerWidth - 50));
137
+ computedStyle.right = Math.max(0, Math.min(computedX, vw - 50));
134
138
  computedStyle.left = 'auto';
135
139
  }
136
140
  if (isTop) {
137
- computedStyle.top = Math.max(0, Math.min(computedY, window.innerHeight - 50));
141
+ computedStyle.top = Math.max(0, Math.min(computedY, vh - 50));
138
142
  computedStyle.bottom = 'auto';
139
143
  }
140
144
  else {
141
- computedStyle.bottom = Math.max(0, Math.min(computedY, window.innerHeight - 50));
145
+ computedStyle.bottom = Math.max(0, Math.min(computedY, vh - 50));
142
146
  computedStyle.top = 'auto';
143
147
  }
144
148
  return computedStyle;
@@ -8,5 +8,5 @@ export type MenuRightSavedState = {
8
8
  y: number;
9
9
  };
10
10
  };
11
- import { ObservableMap } from "../../utils/observableMap";
12
- export declare const mapRightMenu: ObservableMap<string, MenuRightSavedState>;
11
+ import { mapRightMenu } from "../../utils/persistedMaps";
12
+ export { mapRightMenu };
@@ -1,3 +1,5 @@
1
- import { ObservableMap } from "../../utils/observableMap";
2
- // observable - memoryCache marks itself dirty on its mutations (drag end re-set()s the state)
3
- export const mapRightMenu = new ObservableMap();
1
+ import { mapRightMenu } from "../../utils/persistedMaps";
2
+ // observable - memoryCache marks itself dirty on its mutations (drag end re-set()s the state);
3
+ // declared in utils/persistedMaps (memoryCache registry must not import the component layer)
4
+ // and re-exported here so the public surface is unchanged
5
+ export { mapRightMenu };
@@ -5,7 +5,8 @@ import { createUpdateApi } from "../../../updateBy";
5
5
  import { createModalElementStore } from "./Modal";
6
6
  import { DragBox } from "../Dnd/FloatingWindow";
7
7
  function useViewport() {
8
- const [width, setWidth] = useState(window.innerWidth);
8
+ // lazy initializer + guard: window is absent in DOM-less environments (SSR/tests)
9
+ const [width, setWidth] = useState(() => typeof window != 'undefined' ? window.innerWidth : 0);
9
10
  useEffect(() => {
10
11
  const handleResize = () => setWidth(window.innerWidth);
11
12
  window.addEventListener('resize', handleResize);
@@ -191,7 +192,8 @@ export function getApiLeftMenu() {
191
192
  }, children: text ?? "" }));
192
193
  };
193
194
  const createDefaultElement = ({ color, children, textB }) => {
194
- const viewportWidth = window.innerWidth;
195
+ // guard: ApiLeftMenu is created at import time, so this must not touch window on a server
196
+ const viewportWidth = typeof window != 'undefined' ? window.innerWidth : 0;
195
197
  return (_jsxs("div", { className: "blur", style: {
196
198
  minHeight: "100vh",
197
199
  width: viewportWidth > 800 ? "100%" : "calc(100vw - 50px)",
@@ -1,8 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useMemo, useState } from 'react';
3
- import { createPortal } from 'react-dom';
4
- import { OutsideClickArea } from "../../hooks/useOutside";
2
+ import { createContext, useContext, useMemo, useState } from 'react';
5
3
  import { tokens } from "../../styles/tokens";
4
+ import { Overlay } from "../Overlay";
6
5
  const noopModal = Object.assign(() => { }, { show() { }, open() { }, close() { }, replace() { }, set() { } });
7
6
  const ModalContext = createContext(noopModal);
8
7
  export const ModalProvider = ({ children, closeOnOutsideClick = true, closeOnEscape = true }) => {
@@ -16,21 +15,11 @@ export const ModalProvider = ({ children, closeOnOutsideClick = true, closeOnEsc
16
15
  api.set = api.replace;
17
16
  return api;
18
17
  }, []);
19
- useEffect(() => {
20
- if (!modal || !closeOnEscape)
21
- return;
22
- const onKeyDown = (e) => {
23
- if (e.key == 'Escape')
24
- setModal(null);
25
- };
26
- document.addEventListener('keydown', onKeyDown);
27
- return () => document.removeEventListener('keydown', onKeyDown);
28
- }, [modal, closeOnEscape]);
29
- return (_jsxs(ModalContext.Provider, { value: modalApi, children: [children, modal && createPortal(_jsx("div", { style: {
18
+ return (_jsxs(ModalContext.Provider, { value: modalApi, children: [children, modal && (_jsx(Overlay, { scrimStyle: {
30
19
  position: 'fixed', inset: 0, zIndex: tokens.zIndex.modal,
31
20
  display: 'flex', alignItems: 'center', justifyContent: 'center',
32
21
  backgroundColor: 'var(--dlg-scrim, rgba(0, 0, 0, 0.5))'
33
- }, children: _jsx(OutsideClickArea, { outsideClick: () => { if (closeOnOutsideClick)
34
- setModal(null); }, status: true, children: modal }) }), document.body)] }));
22
+ }, onEscape: closeOnEscape ? () => setModal(null) : undefined, onOutsideClick: () => { if (closeOnOutsideClick)
23
+ setModal(null); }, children: modal }))] }));
35
24
  };
36
25
  export const useModal = () => useContext(ModalContext);
@@ -6,5 +6,29 @@ export declare class CResizeObserver {
6
6
  add(element: Element, onResize: () => void): ObserveID;
7
7
  delete(id: ObserveID): void;
8
8
  }
9
+ /** Subscribe an element to the shared resize observer via the returned callback ref.
10
+ * `onResize` goes through a ref - a new function identity neither resubscribes nor is missed.
11
+ * The native observer fires once right after observe, so the first measurement is not skipped.
12
+ * `setResizeableElement` / `removeResizeableElement` below stay untouched - this is the
13
+ * hook-shaped entry over the same singleton. */
14
+ export declare function useResizeObserver<T extends Element = HTMLElement>(onResize: () => void): {
15
+ /** Callback ref - attach to the element to observe. */
16
+ ref: (el: T | null) => void;
17
+ /** The currently observed element (null while detached). */
18
+ element: () => T | null;
19
+ };
20
+ /** "I want the size -> I get the value/method": observed element's width/height as state
21
+ * (rounded, equality-guarded - a no-op resize does not re-render) plus a live `getSize()`
22
+ * getter for measurements that must not wait for a render. */
23
+ export declare function useElementSize<T extends Element = HTMLElement>(): {
24
+ ref: (el: T | null) => void;
25
+ element: () => T | null;
26
+ width: number;
27
+ height: number;
28
+ getSize: () => {
29
+ width: number;
30
+ height: number;
31
+ };
32
+ };
9
33
  export declare function setResizeableElement(el: HTMLElement): HTMLElement | undefined;
10
34
  export declare function removeResizeableElement(el: HTMLElement): void;
@@ -1,3 +1,4 @@
1
+ import { useCallback, useRef, useState } from "react";
1
2
  // Class for tracking element size changes
2
3
  export class CResizeObserver {
3
4
  #idMap = new WeakMap();
@@ -40,6 +41,54 @@ export class CResizeObserver {
40
41
  }
41
42
  }
42
43
  const global_resizeObserver = new CResizeObserver();
44
+ /** Subscribe an element to the shared resize observer via the returned callback ref.
45
+ * `onResize` goes through a ref - a new function identity neither resubscribes nor is missed.
46
+ * The native observer fires once right after observe, so the first measurement is not skipped.
47
+ * `setResizeableElement` / `removeResizeableElement` below stay untouched - this is the
48
+ * hook-shaped entry over the same singleton. */
49
+ export function useResizeObserver(onResize) {
50
+ const cbRef = useRef(onResize);
51
+ cbRef.current = onResize;
52
+ const elRef = useRef(null);
53
+ const idRef = useRef(null);
54
+ const ref = useCallback((el) => {
55
+ if (idRef.current) {
56
+ global_resizeObserver.delete(idRef.current);
57
+ idRef.current = null;
58
+ }
59
+ elRef.current = el;
60
+ if (el)
61
+ idRef.current = global_resizeObserver.add(el, function emitResize() { cbRef.current(); });
62
+ }, []);
63
+ return {
64
+ /** Callback ref - attach to the element to observe. */
65
+ ref,
66
+ /** The currently observed element (null while detached). */
67
+ element: () => elRef.current,
68
+ };
69
+ }
70
+ /** "I want the size -> I get the value/method": observed element's width/height as state
71
+ * (rounded, equality-guarded - a no-op resize does not re-render) plus a live `getSize()`
72
+ * getter for measurements that must not wait for a render. */
73
+ export function useElementSize() {
74
+ const [size, setSize] = useState({ width: 0, height: 0 });
75
+ const obs = useResizeObserver(function readSize() {
76
+ const el = obs.element();
77
+ if (!el)
78
+ return;
79
+ const r = el.getBoundingClientRect();
80
+ const next = { width: Math.round(r.width), height: Math.round(r.height) };
81
+ setSize(prev => prev.width == next.width && prev.height == next.height ? prev : next);
82
+ });
83
+ const getSize = () => {
84
+ const el = obs.element();
85
+ if (!el)
86
+ return { width: 0, height: 0 };
87
+ const r = el.getBoundingClientRect();
88
+ return { width: r.width, height: r.height };
89
+ };
90
+ return { ref: obs.ref, element: obs.element, width: size.width, height: size.height, getSize };
91
+ }
43
92
  const resizeableElementMap = new WeakMap();
44
93
  function getWidth(el) {
45
94
  return Math.ceil(el.clientWidth || el.getBoundingClientRect().width);
@@ -0,0 +1,24 @@
1
+ import React, { ReactNode } from 'react';
2
+ export type OverlayProps = {
3
+ children: ReactNode;
4
+ /** Scrim look: a CSS class (SettingsDialog: `wenayDlgScrim`) and/or inline style
5
+ * (ModalProvider: flex-centered token scrim). The scrim div gets both verbatim. */
6
+ scrimClassName?: string;
7
+ scrimStyle?: React.CSSProperties;
8
+ /** Forwarded to the OutsideClickArea that wraps children (e.g. `wenayDlgOutside`). */
9
+ outsideClassName?: string;
10
+ outsideStatus?: boolean;
11
+ /** Attach a document-level Escape handler while mounted. Omit when the host owns
12
+ * its own keydown logic (SettingsDialog's two-stage Escape stays in its controller). */
13
+ onEscape?: () => void;
14
+ onOutsideClick?: () => void;
15
+ container?: Element;
16
+ };
17
+ /** INTERNAL (A9): the one portal+scrim+outside-click+Escape composition for the
18
+ * scrim-based modal systems - ModalProvider and SettingsDialog adapt to it.
19
+ * Deliberately NOT exported from the public barrels: apps keep using
20
+ * ModalProvider/useModal/SettingsDialog. The render-slot stores
21
+ * (createModalElementStore) and the LeftModal drawer are not overlays and stay
22
+ * separate. Keeping all scrim/portal DOM in this one leaf is also the seam for a
23
+ * future react-native view layer (headless state stays in the hosts). */
24
+ export declare function Overlay({ children, scrimClassName, scrimStyle, outsideClassName, outsideStatus, onEscape, onOutsideClick, container, }: OverlayProps): React.ReactPortal;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from 'react';
3
+ import { createPortal } from 'react-dom';
4
+ import { OutsideClickArea } from '../hooks/useOutside';
5
+ /** INTERNAL (A9): the one portal+scrim+outside-click+Escape composition for the
6
+ * scrim-based modal systems - ModalProvider and SettingsDialog adapt to it.
7
+ * Deliberately NOT exported from the public barrels: apps keep using
8
+ * ModalProvider/useModal/SettingsDialog. The render-slot stores
9
+ * (createModalElementStore) and the LeftModal drawer are not overlays and stay
10
+ * separate. Keeping all scrim/portal DOM in this one leaf is also the seam for a
11
+ * future react-native view layer (headless state stays in the hosts). */
12
+ export function Overlay({ children, scrimClassName, scrimStyle, outsideClassName, outsideStatus = true, onEscape, onOutsideClick, container, }) {
13
+ // callbacks through refs: inline closures must not resubscribe the document listener
14
+ const callbacksRef = useRef({ onEscape, onOutsideClick });
15
+ callbacksRef.current = { onEscape, onOutsideClick };
16
+ const hasEscape = !!onEscape;
17
+ useEffect(() => {
18
+ if (!hasEscape)
19
+ return;
20
+ const onKeyDown = (e) => {
21
+ if (e.key == 'Escape')
22
+ callbacksRef.current.onEscape?.();
23
+ };
24
+ document.addEventListener('keydown', onKeyDown);
25
+ return () => document.removeEventListener('keydown', onKeyDown);
26
+ }, [hasEscape]);
27
+ return createPortal(_jsx("div", { className: scrimClassName, style: scrimStyle, children: _jsx(OutsideClickArea, { outsideClick: () => callbacksRef.current.onOutsideClick?.(), status: outsideStatus, className: outsideClassName, children: children }) }), container ?? document.body);
28
+ }
@@ -1,9 +1,8 @@
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
- import { createPortal } from "react-dom";
4
3
  import { createUpdateApi, renderBy } from "../../../updateBy";
5
- import { OutsideClickArea } from "../../hooks/useOutside";
6
4
  import { FloatingWindowBase } from "../Dnd/FloatingWindow";
5
+ import { Overlay } from "../Overlay";
7
6
  import { createSearchHistory } from "../../utils/searchHistory";
8
7
  import { memoryGetOrCreate, memoryMarkDirty } from "../../utils/memoryStore";
9
8
  const settingsDialogSize = { width: 820, height: 560 };
@@ -477,24 +476,24 @@ export function SettingsDialog(props) {
477
476
  toggleExpanded(key);
478
477
  } }), _jsx("span", { className: "wenayDlgTreeLabel", children: renderHighlightedLabel(node.section.name, searchTerms) })] }), hasChildren && isOpen && node.children.map(renderTreeNode)] }, key);
479
478
  }
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 => {
481
- setSearch(e.currentTarget.value);
482
- setHistoryOpen(searchHistory.length != 0);
483
- }, onKeyDown: e => {
484
- if (e.key == "Enter")
485
- commitSearch();
486
- if (e.key == "ArrowDown" && searchHistory[0]) {
487
- e.preventDefault();
488
- useHistoryItem(searchHistory[0]);
489
- }
490
- } }), searchHistory.length != 0 && _jsx("button", { type: "button", className: "wenayDlgSearchHistoryBtn", title: "Search history", "aria-label": "Search history", "aria-expanded": historyOpen, onClick: () => {
491
- setHistoryOpen(v => !v);
492
- searchInputRef.current?.focus();
493
- }, children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: _jsx("path", { d: "M7 2a5 5 0 1 1-4.4 2.6M2 2v3h3M7 4v3l2 1" }) }) }), search != "" && _jsx("button", { type: "button", className: "wenayDlgSearchClear", title: "Clear search", "aria-label": "Clear search", onClick: () => {
494
- setSearch("");
495
- setHistoryOpen(searchHistory.length != 0);
496
- searchInputRef.current?.focus();
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: () => {
498
- clearHistory();
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)] });
479
+ 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 && (_jsx(Overlay, { scrimClassName: "wenayDlgScrim", outsideClassName: "wenayDlgOutside", outsideStatus: open, onOutsideClick: () => setOpen(false), 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 => {
480
+ setSearch(e.currentTarget.value);
481
+ setHistoryOpen(searchHistory.length != 0);
482
+ }, onKeyDown: e => {
483
+ if (e.key == "Enter")
484
+ commitSearch();
485
+ if (e.key == "ArrowDown" && searchHistory[0]) {
486
+ e.preventDefault();
487
+ useHistoryItem(searchHistory[0]);
488
+ }
489
+ } }), searchHistory.length != 0 && _jsx("button", { type: "button", className: "wenayDlgSearchHistoryBtn", title: "Search history", "aria-label": "Search history", "aria-expanded": historyOpen, onClick: () => {
490
+ setHistoryOpen(v => !v);
491
+ searchInputRef.current?.focus();
492
+ }, children: _jsx("svg", { width: "14", height: "14", viewBox: "0 0 14 14", "aria-hidden": "true", children: _jsx("path", { d: "M7 2a5 5 0 1 1-4.4 2.6M2 2v3h3M7 4v3l2 1" }) }) }), search != "" && _jsx("button", { type: "button", className: "wenayDlgSearchClear", title: "Clear search", "aria-label": "Clear search", onClick: () => {
493
+ setSearch("");
494
+ setHistoryOpen(searchHistory.length != 0);
495
+ searchInputRef.current?.focus();
496
+ }, 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: () => {
497
+ clearHistory();
498
+ }, 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() })] }) }) }))] });
500
499
  }
@@ -133,5 +133,6 @@ export declare function createToolbar(opts: {
133
133
  showReset: (on: boolean) => void;
134
134
  reset: () => void;
135
135
  onChange: import("wenay-common2").ListenApi<[ToolbarConfig]>;
136
+ dispose: () => void;
136
137
  };
137
138
  };
@@ -3,6 +3,7 @@ import { useLayoutEffect, useRef, useState } from 'react';
3
3
  import { listen as createListen } from 'wenay-common2';
4
4
  import { createUpdateApi } from '../../../updateBy';
5
5
  import { memoryGetOrCreate, memoryMarkDirty } from '../../utils/memoryStore';
6
+ import { pinFixedOrder, movedOrderWithFixed } from '../../utils/fixedOrder';
6
7
  import { OutsideClickArea } from '../../hooks/useOutside';
7
8
  import { useReorder } from '../../hooks/useReorder';
8
9
  // Module singleton (closure + updateBy subscription, no React context) - same
@@ -114,8 +115,9 @@ export function createToolbar(opts) {
114
115
  const ext = opts.source;
115
116
  const sourceMode = ext ? (opts.sourceMode ?? 'orderVisible') : 'orderVisible';
116
117
  // outside edits of the external config (grid drags...) must reach the
117
- // toolbar's own subscribers too; module-lifetime subscription by design
118
- ext?.onChange?.(() => emitChange(normalize()));
118
+ // toolbar's own subscribers too; lives until dispose() (repeated factories over
119
+ // one key used to leak a permanent listener per call - HMR, remounts)
120
+ const offSource = ext?.onChange?.(() => emitChange(normalize()));
119
121
  function sameOrder(a, b) {
120
122
  return a.length == b.length && a.every((k, i) => k == b[i]);
121
123
  }
@@ -142,14 +144,11 @@ export function createToolbar(opts) {
142
144
  const rawOrder = ext && sourceMode == 'order'
143
145
  ? mergeSourceOrder(Array.isArray(st.order) ? st.order : [], Array.isArray(extRaw?.order) ? extRaw.order : [], sourceKeys)
144
146
  : Array.isArray(raw.order) ? raw.order : [];
145
- const order = rawOrder.filter(k => known.has(k) && !opts.items.find(i => i.key == k)?.fixed);
147
+ const prelim = rawOrder.filter(k => known.has(k) && !opts.items.find(i => i.key == k)?.fixed);
146
148
  for (const it of opts.items)
147
- if (!it.fixed && order.indexOf(it.key) == -1)
148
- order.push(it.key);
149
- opts.items.forEach(function pinFixed(it, i) {
150
- if (it.fixed)
151
- order.splice(Math.min(i, order.length), 0, it.key);
152
- });
149
+ if (!it.fixed && prelim.indexOf(it.key) == -1)
150
+ prelim.push(it.key);
151
+ const order = pinFixedOrder(prelim, opts.items);
153
152
  const rawVisible = raw.visible && typeof raw.visible == 'object' ? raw.visible : {};
154
153
  const visible = {};
155
154
  for (const it of opts.items)
@@ -310,20 +309,9 @@ export function createToolbar(opts) {
310
309
  const base = p.className ?? 'wenaySegBtn';
311
310
  const activeCls = p.activeClassName ?? 'wenaySegBtnActive';
312
311
  const byKey = new Map(opts.items.map(i => [i.key, i]));
313
- /** Simulated commit: splice + the same fixed pinning as normalize(). */
312
+ /** Simulated commit: splice + the same fixed pinning as normalize() (shared utils/fixedOrder). */
314
313
  function movedOrder(order, key, to) {
315
- const next = order.slice();
316
- const from = next.indexOf(key);
317
- if (from == -1)
318
- return next;
319
- next.splice(from, 1);
320
- next.splice(Math.max(0, Math.min(next.length, to)), 0, key);
321
- const res = next.filter(k => !byKey.get(k)?.fixed);
322
- opts.items.forEach(function pinFixed(it, i) {
323
- if (it.fixed)
324
- res.splice(Math.min(i, res.length), 0, it.key);
325
- });
326
- return res;
314
+ return movedOrderWithFixed(order, key, to, opts.items);
327
315
  }
328
316
  const reorder = useReorder({
329
317
  order: cfg.order,
@@ -354,9 +342,14 @@ export function createToolbar(opts) {
354
342
  }, onTouchStart: it.fixed ? undefined : r.props.onTouchStart, children: [_jsx("input", { type: 'checkbox', disabled: it.fixed, checked: cfg.visible[k] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [k]: !(cfg.visible[k] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: toolbarItemIcon(it) }), _jsx("span", { className: 'wenayTbRowTitle', children: it.title }), !it.fixed && _jsx("div", { className: 'wenayTbHandle', tabIndex: 0, title: 'Drag or arrow keys to reorder', onKeyDown: e => onHandleKey(k, e), children: "\u283F" })] }, k);
355
343
  }) }), _jsxs("div", { className: 'wenayTbRow wenayTbRowMeta', children: [_jsx("input", { type: 'checkbox', checked: cfg.visible[SETTINGS_KEY] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [SETTINGS_KEY]: !(cfg.visible[SETTINGS_KEY] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: settingsIcon() }), _jsx("span", { className: 'wenayTbRowTitle', children: settingsTitle() })] }), opts.resetItem !== false && _jsxs("div", { className: 'wenayTbRow wenayTbRowMeta', children: [_jsx("input", { type: 'checkbox', checked: cfg.visible[RESET_KEY] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [RESET_KEY]: !(cfg.visible[RESET_KEY] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: resetIcon() }), _jsx("span", { className: 'wenayTbRowTitle', children: resetTitle() }), _jsx("button", { type: 'button', className: 'wenayTbMetaAction', title: resetTitle(), onClick: () => reset(), children: resetIcon() })] })] });
356
344
  }
345
+ /** Release the external-source subscription. The persisted config itself stays -
346
+ * dispose() is about listener lifetime (HMR, remounts), not about state. */
347
+ function dispose() {
348
+ offSource?.();
349
+ }
357
350
  return {
358
351
  Bar,
359
352
  Settings,
360
- api: { useConfig, useItems, getConfig, setConfig, setOrder, show, setDensity, showSettings, showReset, reset, onChange },
353
+ api: { useConfig, useItems, getConfig, setConfig, setOrder, show, setDensity, showSettings, showReset, reset, onChange, dispose },
361
354
  };
362
355
  }