wenay-react2 1.0.28 → 1.0.30

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 (32) hide show
  1. package/lib/common/api.d.ts +2 -0
  2. package/lib/common/api.js +3 -0
  3. package/lib/common/src/components/Dnd/RNDFunc3.d.ts +2 -1
  4. package/lib/common/src/components/Dnd/RNDFunc3.js +11 -2
  5. package/lib/common/src/components/Dnd/Resizable.d.ts +2 -1
  6. package/lib/common/src/components/Dnd/Resizable.js +6 -2
  7. package/lib/common/src/components/Menu/RightMenu.js +1 -0
  8. package/lib/common/src/components/Menu/RightMenuStore.d.ts +2 -1
  9. package/lib/common/src/components/Menu/RightMenuStore.js +3 -1
  10. package/lib/common/src/components/Settings/SettingsDialog.d.ts +22 -0
  11. package/lib/common/src/components/Settings/SettingsDialog.js +55 -0
  12. package/lib/common/src/components/Settings/index.d.ts +1 -0
  13. package/lib/common/src/components/Settings/index.js +1 -0
  14. package/lib/common/src/components/UiSlot/UiSlot.d.ts +25 -0
  15. package/lib/common/src/components/UiSlot/UiSlot.js +35 -0
  16. package/lib/common/src/components/UiSlot/index.d.ts +1 -0
  17. package/lib/common/src/components/UiSlot/index.js +1 -0
  18. package/lib/common/src/styles/tokens.d.ts +13 -0
  19. package/lib/common/src/styles/tokens.js +13 -0
  20. package/lib/common/src/utils/cache.d.ts +17 -0
  21. package/lib/common/src/utils/cache.js +0 -0
  22. package/lib/common/src/utils/callbackHub.d.ts +10 -0
  23. package/lib/common/src/utils/callbackHub.js +23 -0
  24. package/lib/common/src/utils/index.d.ts +2 -0
  25. package/lib/common/src/utils/index.js +2 -0
  26. package/lib/common/src/utils/mapMemory.d.ts +17 -4
  27. package/lib/common/src/utils/mapMemory.js +21 -1
  28. package/lib/common/src/utils/observableMap.d.ts +17 -0
  29. package/lib/common/src/utils/observableMap.js +40 -0
  30. package/lib/style/style.css +69 -0
  31. package/lib/style/tokens.css +10 -0
  32. package/package.json +1 -1
@@ -25,6 +25,8 @@ export * from "./src/components/ParametersEngine";
25
25
  export * from "./src/components/Input";
26
26
  export * from "./src/components/Modal";
27
27
  export * from "./src/components/Menu";
28
+ export * from "./src/components/Settings";
29
+ export * from "./src/components/UiSlot";
28
30
  export { MenuBase, TimeNum, MenuElement as LegacyMenuElement } from "./src/menu/menu";
29
31
  export type { tMenuReact, tMenuReactStrictly } from "./src/menu/menu";
30
32
  export * from "./src/menu/menuMouse";
package/lib/common/api.js CHANGED
@@ -37,6 +37,9 @@ export * from "./src/components/Input";
37
37
  export * from "./src/components/Modal";
38
38
  // 11. Menu - depends on hooks, Dnd, and Modal
39
39
  export * from "./src/components/Menu";
40
+ // 11b. Settings dialog + section registry; UI slot with configurable placement
41
+ export * from "./src/components/Settings";
42
+ export * from "./src/components/UiSlot";
40
43
  // 12. MENU - depends on components
41
44
  export { MenuBase, TimeNum, MenuElement as LegacyMenuElement } from "./src/menu/menu";
42
45
  export * from "./src/menu/menuMouse";
@@ -1,4 +1,5 @@
1
1
  import React, { ReactNode } from "react";
2
+ import { ObservableMap } from "../../utils/observableMap";
2
3
  type tPosition = {
3
4
  x: number;
4
5
  y: number;
@@ -46,7 +47,7 @@ type tDivRndBase = {
46
47
  children: React.ReactElement | ((update: number) => React.ReactElement);
47
48
  className?: string;
48
49
  };
49
- export declare const ExRNDMap3: Map<string, tRND>;
50
+ export declare const ExRNDMap3: ObservableMap<string, tRND>;
50
51
  export declare const DivRnd3: typeof DivRndBase3;
51
52
  /**
52
53
  * Wrapper component around react-rnd.
@@ -2,8 +2,9 @@ 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 { renderBy, updateBy } from "../../../updateBy";
5
- // Map of all popup window sizes
6
- export const ExRNDMap3 = new Map();
5
+ import { ObservableMap } from "../../utils/observableMap";
6
+ // Map of all popup window sizes; observable - Cash marks itself dirty on its mutations
7
+ export const ExRNDMap3 = new ObservableMap();
7
8
  // limit={{x:{min:0}, y:{min:0}}}
8
9
  let k = 0;
9
10
  const openWindows = { ar: [] };
@@ -97,6 +98,10 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
97
98
  document.removeEventListener("mousemove", mouseMoveHandler);
98
99
  lastC.current = null;
99
100
  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
+ if (ks)
104
+ ExRNDMap3.touch(ks);
100
105
  };
101
106
  // Touch
102
107
  const touchMoveHandler = (e) => {
@@ -141,6 +146,8 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
141
146
  document.removeEventListener("touchend", touchEndHandler);
142
147
  document.removeEventListener("touchmove", touchMoveHandler);
143
148
  setB(false);
149
+ if (ks)
150
+ ExRNDMap3.touch(ks);
144
151
  }
145
152
  };
146
153
  // If mouse dragging mode is active
@@ -227,6 +234,8 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
227
234
  setHeight(elementRef.offsetHeight);
228
235
  setWidth(elementRef.offsetWidth);
229
236
  setUpdate(update + 1);
237
+ if (ks)
238
+ ExRNDMap3.touch(ks);
230
239
  }, onResize: (e, dir, elementRef, delta, pos) => {
231
240
  onUpdate?.({ e, dir, elementRef, delta, position: pos });
232
241
  }, position: { x, y }, size: { width, height }, default: {
@@ -1,9 +1,10 @@
1
1
  import { ResizableProps } from "re-resizable";
2
+ import { ObservableMap } from "../../utils/observableMap";
2
3
  type tSaveMap = {
3
4
  height?: number | string;
4
5
  width?: number | string;
5
6
  };
6
- export declare const mapResiReact: Map<string, tSaveMap>;
7
+ export declare const mapResiReact: ObservableMap<string, tSaveMap>;
7
8
  type t3 = Pick<ResizableProps, "style" | "enable" | "onResize" | "children" | "size" | "maxWidth" | "maxHeight" | "minWidth" | "minHeight">;
8
9
  export declare function FResizableReact({ style, onResize, enable, children, keyForSave, onResizeStop, size, minWidth, minHeight, maxWidth, maxHeight, moveWith, moveHeight }: t3 & {
9
10
  keyForSave?: string;
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Resizable } from "re-resizable";
3
- // Memory for all column sizes
4
- export const mapResiReact = new Map();
3
+ import { ObservableMap } from "../../utils/observableMap";
4
+ // Memory for all column sizes; observable - Cash marks itself dirty on its mutations
5
+ export const mapResiReact = new ObservableMap();
5
6
  export function FResizableReact({ style, onResize, enable, children, keyForSave, onResizeStop, size = { height: 50, width: 50 }, minWidth, minHeight, maxWidth = "100%", maxHeight = "100%", moveWith = true, moveHeight = true }) {
6
7
  let obj = size;
7
8
  if (keyForSave) {
@@ -30,6 +31,9 @@ export function FResizableReact({ style, onResize, enable, children, keyForSave,
30
31
  obj.height = elementRef.style.height;
31
32
  }
32
33
  // onResize?.(size)
34
+ // obj is mutated in place - invisible to the map, so announce it
35
+ if (keyForSave)
36
+ mapResiReact.touch(keyForSave);
33
37
  onResizeStop?.(obj);
34
38
  // this.Refresh()
35
39
  }, size: obj, defaultSize: obj, maxWidth: maxWidth, maxHeight: maxHeight, minWidth: minWidth, minHeight: minHeight, children: children });
@@ -67,6 +67,7 @@ export function DropdownMenu({ elements, style, position: initialPosition = 'rig
67
67
  setPosition(nextPosition);
68
68
  setIsTop(toTop);
69
69
  if (keyForSave) {
70
+ // mapRightMenu is observable: set() itself marks the cache dirty
70
71
  mapRightMenu.set(keyForSave, {
71
72
  position: nextPosition,
72
73
  position2: toTop ? 'top' : 'bottom',
@@ -8,4 +8,5 @@ export type MenuRightSavedState = {
8
8
  y: number;
9
9
  };
10
10
  };
11
- export declare const mapRightMenu: Map<string, MenuRightSavedState>;
11
+ import { ObservableMap } from "../../utils/observableMap";
12
+ export declare const mapRightMenu: ObservableMap<string, MenuRightSavedState>;
@@ -1 +1,3 @@
1
- export const mapRightMenu = new Map();
1
+ import { ObservableMap } from "../../utils/observableMap";
2
+ // observable - Cash marks itself dirty on its mutations (drag end re-set()s the state)
3
+ export const mapRightMenu = new ObservableMap();
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ export type tSettingsSection = {
3
+ key: string;
4
+ name: string;
5
+ render: () => React.ReactNode;
6
+ };
7
+ /** Register an external section. Re-register with the same key replaces the previous one.
8
+ * The returned function removes exactly this registration (a no-op if it was replaced). */
9
+ export declare function registerSettingsSection(s: tSettingsSection): () => void;
10
+ /** Current external sections (static props sections are not included). */
11
+ export declare function getSettingsSections(): readonly tSettingsSection[];
12
+ /** Centered settings dialog: sections column on the left, active section content on the right.
13
+ * Sections = props.sections (first) + everything from registerSettingsSection.
14
+ * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
15
+ export declare function SettingsDialog(props: {
16
+ trigger: React.ReactNode;
17
+ sections?: tSettingsSection[];
18
+ defaultSection?: string;
19
+ /** Section buttons: apps pass their own .chip / .chipActive; defaults are minimal library styles */
20
+ sectionClassName?: string;
21
+ sectionActiveClassName?: string;
22
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { renderBy, updateBy } from "../../../updateBy";
5
+ // Module singleton (closure + updateBy subscription, no React context): any module
6
+ // registers a section on mount and removes it on unmount; the dialog re-renders on changes.
7
+ const registry = { list: [] };
8
+ /** Register an external section. Re-register with the same key replaces the previous one.
9
+ * The returned function removes exactly this registration (a no-op if it was replaced). */
10
+ export function registerSettingsSection(s) {
11
+ const i = registry.list.findIndex(e => e.key == s.key);
12
+ if (i == -1)
13
+ registry.list.push(s);
14
+ else
15
+ registry.list.splice(i, 1, s);
16
+ renderBy(registry);
17
+ return () => {
18
+ const j = registry.list.indexOf(s);
19
+ if (j != -1) {
20
+ registry.list.splice(j, 1);
21
+ renderBy(registry);
22
+ }
23
+ };
24
+ }
25
+ /** Current external sections (static props sections are not included). */
26
+ export function getSettingsSections() {
27
+ return registry.list;
28
+ }
29
+ /** Centered settings dialog: sections column on the left, active section content on the right.
30
+ * Sections = props.sections (first) + everything from registerSettingsSection.
31
+ * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
32
+ export function SettingsDialog(props) {
33
+ const [open, setOpen] = useState(false);
34
+ const [active, setActive] = useState(props.defaultSection);
35
+ updateBy(registry);
36
+ useEffect(() => {
37
+ if (!open)
38
+ return;
39
+ const onKeyDown = (e) => {
40
+ if (e.key == 'Escape')
41
+ setOpen(false);
42
+ };
43
+ document.addEventListener('keydown', onKeyDown);
44
+ return () => document.removeEventListener('keydown', onKeyDown);
45
+ }, [open]);
46
+ const sections = [...(props.sections ?? []), ...registry.list];
47
+ // fall back to the first section when active was never set or its section unmounted
48
+ const current = sections.find(s => s.key == active) ?? sections[0];
49
+ const base = props.sectionClassName ?? "wenayDlgSection";
50
+ const activeCls = props.sectionActiveClassName ?? "wenayDlgSectionActive";
51
+ return _jsxs(_Fragment, { children: [_jsx("span", { onClick: () => setOpen(true), style: { display: "inline-block", cursor: "pointer" }, children: props.trigger }), open && createPortal(_jsx("div", { className: "wenayDlgScrim", onClick: e => {
52
+ if (e.target == e.currentTarget)
53
+ setOpen(false);
54
+ }, children: _jsxs("div", { className: "wenayDlg", children: [_jsx("div", { className: "wenayDlgNav", children: sections.map(s => (_jsx("div", { className: s.key == current?.key ? `${base} ${activeCls}` : base, onClick: () => setActive(s.key), children: s.name }, s.key))) }), _jsx("div", { className: "wenayDlgContent", children: current?.render() }), _jsx("div", { className: "wenayCloseBtn wenayDlgClose", title: "Close", onClick: () => setOpen(false), 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" }) }) })] }) }), document.body)] });
55
+ }
@@ -0,0 +1 @@
1
+ export * from './SettingsDialog';
@@ -0,0 +1 @@
1
+ export * from './SettingsDialog';
@@ -0,0 +1,25 @@
1
+ import React from "react";
2
+ /** One UI block shown in exactly one of several mount points; the point is a persisted setting.
3
+ * Persistence rides the existing staticProps -> Cash mechanics: staticProps is observable,
4
+ * setPlace announces its in-place mutation (staticMarkDirty), the app decides when to save
5
+ * via Cash.onDirty - same as window state (ExRNDMap3 etc.).
6
+ * Mount points render <Slot place="..."> themselves and decide nothing: the slot compares
7
+ * the persisted place with its own and renders children only on a match. */
8
+ export declare function createUiSlot<Places extends string>(opts: {
9
+ key: string;
10
+ places: {
11
+ [P in Places]: string;
12
+ };
13
+ def: NoInfer<Places>;
14
+ }): {
15
+ Slot: (p: {
16
+ place: Places;
17
+ children: React.ReactNode;
18
+ }) => React.JSX.Element | null;
19
+ PlacementSetting: (p?: {
20
+ className?: string;
21
+ activeClassName?: string;
22
+ }) => import("react/jsx-runtime").JSX.Element;
23
+ getPlace: () => Places;
24
+ setPlace: (p: Places) => void;
25
+ };
@@ -0,0 +1,35 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderBy, updateBy } from "../../../updateBy";
3
+ import { staticGetAdd, staticMarkDirty } from "../../utils/mapMemory";
4
+ /** One UI block shown in exactly one of several mount points; the point is a persisted setting.
5
+ * Persistence rides the existing staticProps -> Cash mechanics: staticProps is observable,
6
+ * setPlace announces its in-place mutation (staticMarkDirty), the app decides when to save
7
+ * via Cash.onDirty - same as window state (ExRNDMap3 etc.).
8
+ * Mount points render <Slot place="..."> themselves and decide nothing: the slot compares
9
+ * the persisted place with its own and renders children only on a match. */
10
+ export function createUiSlot(opts) {
11
+ const st = staticGetAdd(opts.key, { place: opts.def });
12
+ // a stored place may no longer exist after an app update - fall back to def
13
+ const isPlace = (p) => typeof p == "string" && p in opts.places;
14
+ const getPlace = () => isPlace(st.place) ? st.place : opts.def;
15
+ const setPlace = (p) => {
16
+ if (st.place == p)
17
+ return;
18
+ st.place = p;
19
+ renderBy(st);
20
+ staticMarkDirty(opts.key);
21
+ };
22
+ function Slot(p) {
23
+ updateBy(st);
24
+ return getPlace() == p.place ? _jsx(_Fragment, { children: p.children }) : null;
25
+ }
26
+ /** Segmented row over opts.places; apps pass their own .chip / .chipActive classes */
27
+ function PlacementSetting(p = {}) {
28
+ updateBy(st);
29
+ const cur = getPlace();
30
+ const base = p.className ?? "wenaySegBtn";
31
+ const activeCls = p.activeClassName ?? "wenaySegBtnActive";
32
+ return _jsx("div", { style: { display: "inline-flex", gap: 4, flexWrap: "wrap" }, children: Object.keys(opts.places).map(key => (_jsx("div", { className: key == cur ? `${base} ${activeCls}` : base, onClick: () => setPlace(key), children: opts.places[key] }, key))) });
33
+ }
34
+ return { Slot, PlacementSetting, getPlace, setPlace };
35
+ }
@@ -0,0 +1 @@
1
+ export * from './UiSlot';
@@ -0,0 +1 @@
1
+ export * from './UiSlot';
@@ -44,6 +44,19 @@ export declare const tokens: {
44
44
  readonly closeHover: "#fff";
45
45
  readonly closeHoverBg: "#e5484d";
46
46
  };
47
+ /** SettingsDialog chrome (--dlg-*). Dark defaults; apps re-skin via :root[data-theme]. */
48
+ readonly dlg: {
49
+ readonly scrim: "rgba(0, 0, 0, 0.5)";
50
+ /** var(--color-bg-dark) in CSS */
51
+ readonly bg: "#131821";
52
+ /** var(--color-border-common) in CSS */
53
+ readonly border: "1px solid rgb(50, 62, 71)";
54
+ readonly radius: "10px";
55
+ readonly shadow: "0 12px 40px rgba(0, 0, 0, 0.5)";
56
+ /** var(--color-bg-light) in CSS */
57
+ readonly navBg: "#17202e";
58
+ readonly navWidth: "168px";
59
+ };
47
60
  readonly font: {
48
61
  readonly family: "Roboto";
49
62
  readonly sizeBase: "12px";
@@ -46,6 +46,19 @@ export const tokens = {
46
46
  closeHover: '#fff',
47
47
  closeHoverBg: '#e5484d',
48
48
  },
49
+ /** SettingsDialog chrome (--dlg-*). Dark defaults; apps re-skin via :root[data-theme]. */
50
+ dlg: {
51
+ scrim: 'rgba(0, 0, 0, 0.5)',
52
+ /** var(--color-bg-dark) in CSS */
53
+ bg: '#131821',
54
+ /** var(--color-border-common) in CSS */
55
+ border: '1px solid rgb(50, 62, 71)',
56
+ radius: '10px',
57
+ shadow: '0 12px 40px rgba(0, 0, 0, 0.5)',
58
+ /** var(--color-bg-light) in CSS */
59
+ navBg: '#17202e',
60
+ navWidth: '168px',
61
+ },
49
62
  font: {
50
63
  family: 'Roboto',
51
64
  sizeBase: '12px',
@@ -1,3 +1,4 @@
1
+ export type tDirtyListener = (scope?: string, key?: string) => void;
1
2
  export interface IServerSaveBasePromise {
2
3
  set(key: string, value: object): Promise<boolean>;
3
4
  get<T extends (object)>(key: string): Promise<T | null>;
@@ -20,12 +21,28 @@ export declare const ObjectStringToDate: (obj: unknown) => void;
20
21
  export declare function CacheFuncMapBase(arr: [k: string, v: Map<string, unknown>][], Save: IServerSaveBasePromise): {
21
22
  load(): Promise<void>;
22
23
  save(): Promise<void>;
24
+ saveDebounced(delay?: number): void;
25
+ flush(): Promise<void>;
23
26
  clear(): Promise<void>;
27
+ /** Manual announcement for state the observable maps cannot see (plain Map entries). */
28
+ markDirty: (scope?: string, key?: string) => void;
29
+ /** Subscribe to this instance's dirty events (coalesced, async); returns unsubscribe. */
30
+ onDirty(cb: tDirtyListener): () => void;
31
+ /** Cheap hint (e.g. a beforeunload guard); the save diff is the source of truth. */
32
+ isDirty(): boolean;
24
33
  getArr: [k: string, v: Map<string, unknown>][];
25
34
  };
26
35
  export declare function CacheFuncMap(arr: [k: string, v: Map<string, unknown>][]): {
27
36
  load(): Promise<void>;
28
37
  save(): Promise<void>;
38
+ saveDebounced(delay?: number): void;
39
+ flush(): Promise<void>;
29
40
  clear(): Promise<void>;
41
+ /** Manual announcement for state the observable maps cannot see (plain Map entries). */
42
+ markDirty: (scope?: string, key?: string) => void;
43
+ /** Subscribe to this instance's dirty events (coalesced, async); returns unsubscribe. */
44
+ onDirty(cb: tDirtyListener): () => void;
45
+ /** Cheap hint (e.g. a beforeunload guard); the save diff is the source of truth. */
46
+ isDirty(): boolean;
30
47
  getArr: [k: string, v: Map<string, unknown>][];
31
48
  };
Binary file
@@ -0,0 +1,10 @@
1
+ /** Multiplexer for a single-slot callback API (onX(cb | null)): many subscribers share
2
+ * one slot instead of silently overwriting each other.
3
+ * bind(emit) is called lazily, ONCE, on the first on() - not at creation time, because
4
+ * the slot may still be taken by app initialization. */
5
+ export declare function createCallbackHub<Args extends any[]>(bind: (emit: (...a: Args) => void) => void): {
6
+ /** Subscribe; the returned function unsubscribes without touching other subscribers.
7
+ * The callback is registered BEFORE bind runs, so it also catches synchronous emits. */
8
+ on(cb: (...a: Args) => void): () => void;
9
+ count: () => number;
10
+ };
@@ -0,0 +1,23 @@
1
+ /** Multiplexer for a single-slot callback API (onX(cb | null)): many subscribers share
2
+ * one slot instead of silently overwriting each other.
3
+ * bind(emit) is called lazily, ONCE, on the first on() - not at creation time, because
4
+ * the slot may still be taken by app initialization. */
5
+ export function createCallbackHub(bind) {
6
+ const subs = new Set();
7
+ let bound = false;
8
+ // copy before iterating: an unsubscribe from inside a callback must not skip the rest
9
+ const emit = (...a) => { [...subs].forEach(cb => cb(...a)); };
10
+ return {
11
+ /** Subscribe; the returned function unsubscribes without touching other subscribers.
12
+ * The callback is registered BEFORE bind runs, so it also catches synchronous emits. */
13
+ on(cb) {
14
+ subs.add(cb);
15
+ if (!bound) {
16
+ bound = true;
17
+ bind(emit);
18
+ }
19
+ return () => { subs.delete(cb); };
20
+ },
21
+ count: () => subs.size,
22
+ };
23
+ }
@@ -1,6 +1,8 @@
1
1
  export * from './applyTransactionAsyncUpdate';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './callbackHub';
4
5
  export * from './inputAutoStep';
5
6
  export * from './mapMemory';
7
+ export * from './observableMap';
6
8
  export * from './pageVisibilityContext';
@@ -1,6 +1,8 @@
1
1
  export * from './applyTransactionAsyncUpdate';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './callbackHub';
4
5
  export * from './inputAutoStep';
5
6
  export * from './mapMemory';
7
+ export * from './observableMap';
6
8
  export * from './pageVisibilityContext';
@@ -1,3 +1,4 @@
1
+ import { ObservableMap } from "./observableMap";
1
2
  export declare function staticSet(key: any, data: object): void;
2
3
  export declare function staticGet(key: any): object | undefined;
3
4
  export declare function deepMergeWithMap(target: any, source: any, visited?: Map<any, any>): any;
@@ -6,15 +7,27 @@ export declare function staticGetAdd<T extends object>(key: any, def: T, options
6
7
  deepAutoMerge?: boolean;
7
8
  reversDeep?: boolean;
8
9
  }): T;
10
+ /** Announce a direct in-place mutation of an object taken from staticGetAdd - such
11
+ * mutations are invisible to map methods. staticSet/staticGetAdd need no announcement:
12
+ * their set() calls are observed by Cash automatically. */
13
+ export declare function staticMarkDirty(key: any): void;
14
+ /** App-facing change of a persisted staticProps entry in one call:
15
+ * mutate + rerender subscribers + mark the cache dirty. No-op if the key is absent. */
16
+ export declare function staticUpdate<T extends object>(key: any, mutate: (cur: T) => void): T | undefined;
9
17
  export declare function staticGetById<T extends object>(key: any, def: T, id: string | number): T;
10
18
  export declare const Cash: {
11
19
  load(): Promise<void>;
12
20
  save(): Promise<void>;
21
+ saveDebounced(delay?: number): void;
22
+ flush(): Promise<void>;
13
23
  clear(): Promise<void>;
24
+ markDirty: (scope?: string, key?: string) => void;
25
+ onDirty(cb: import("./cache").tDirtyListener): () => void;
26
+ isDirty(): boolean;
14
27
  getArr: [k: string, v: Map<string, unknown>][];
15
28
  };
16
29
  export declare const MemoryMap: {
17
- rnd: Map<string, {
30
+ rnd: ObservableMap<string, {
18
31
  position: {
19
32
  x: number;
20
33
  y: number;
@@ -24,10 +37,10 @@ export declare const MemoryMap: {
24
37
  width: number | string;
25
38
  };
26
39
  }>;
27
- resize: Map<string, {
40
+ resize: ObservableMap<string, {
28
41
  height?: number | string;
29
42
  width?: number | string;
30
43
  }>;
31
- rightMenu: Map<string, import("../components/Menu/RightMenuStore").MenuRightSavedState>;
32
- other: Map<string, object>;
44
+ rightMenu: ObservableMap<string, import("../components/Menu/RightMenuStore").MenuRightSavedState>;
45
+ other: ObservableMap<string, object>;
33
46
  };
@@ -1,9 +1,12 @@
1
1
  import { deepClone } from "wenay-common2";
2
+ import { renderBy } from "../../updateBy";
2
3
  import { ExRNDMap3 } from "../components/Dnd/RNDFunc3";
3
4
  import { mapResiReact } from "../components/Dnd/Resizable";
4
5
  import { mapRightMenu } from "../components/Menu/RightMenuStore";
5
6
  import { CacheFuncMap } from "./cache";
6
- const staticProps = new Map();
7
+ import { ObservableMap } from "./observableMap";
8
+ // observable - Cash marks itself dirty on its mutations
9
+ const staticProps = new ObservableMap();
7
10
  export function staticSet(key, data) {
8
11
  if (!staticProps.has(key))
9
12
  staticProps.set(key, data);
@@ -52,6 +55,23 @@ export function staticGetAdd(key, def, options = { reversDeep: false }) {
52
55
  const t = (staticProps.has(key) ? staticProps.get(key) : staticProps.set(key, def).get(key));
53
56
  return t; // Object.assign(def, t) // t //
54
57
  }
58
+ /** Announce a direct in-place mutation of an object taken from staticGetAdd - such
59
+ * mutations are invisible to map methods. staticSet/staticGetAdd need no announcement:
60
+ * their set() calls are observed by Cash automatically. */
61
+ export function staticMarkDirty(key) {
62
+ staticProps.touch(typeof key == "string" ? key : undefined);
63
+ }
64
+ /** App-facing change of a persisted staticProps entry in one call:
65
+ * mutate + rerender subscribers + mark the cache dirty. No-op if the key is absent. */
66
+ export function staticUpdate(key, mutate) {
67
+ const cur = staticProps.get(key);
68
+ if (cur === undefined)
69
+ return undefined;
70
+ mutate(cur);
71
+ renderBy(cur);
72
+ staticMarkDirty(key);
73
+ return cur;
74
+ }
55
75
  export function staticGetById(key, def, id) {
56
76
  const stored = staticProps.get(key);
57
77
  if (!stored || stored.__id !== id) {
@@ -0,0 +1,17 @@
1
+ /** Map that announces its own mutations. The persisted-state maps (ExRNDMap3, mapResiReact,
2
+ * mapRightMenu, staticProps) are ObservableMap instances, so the dirty signal originates in
3
+ * the data layer itself: set/delete/clear emit automatically, touch(key) announces an
4
+ * in-place mutation of a stored object (invisible to map methods). CacheFuncMapBase
5
+ * subscribes to the maps it owns - mutation sites never talk to the cache directly. */
6
+ export type tMapChangeListener<K> = (key?: K) => void;
7
+ export declare class ObservableMap<K, V> extends Map<K, V> {
8
+ private listeners?;
9
+ /** Subscribe to mutations (set/delete/clear/touch); returns unsubscribe. */
10
+ onChange(cb: tMapChangeListener<K>): () => void;
11
+ /** Announce an in-place mutation of a stored value. */
12
+ touch(key?: K): void;
13
+ private emit;
14
+ set(key: K, value: V): this;
15
+ delete(key: K): boolean;
16
+ clear(): void;
17
+ }
@@ -0,0 +1,40 @@
1
+ /** Map that announces its own mutations. The persisted-state maps (ExRNDMap3, mapResiReact,
2
+ * mapRightMenu, staticProps) are ObservableMap instances, so the dirty signal originates in
3
+ * the data layer itself: set/delete/clear emit automatically, touch(key) announces an
4
+ * in-place mutation of a stored object (invisible to map methods). CacheFuncMapBase
5
+ * subscribes to the maps it owns - mutation sites never talk to the cache directly. */
6
+ export class ObservableMap extends Map {
7
+ // no initializer: Map's constructor calls this.set() before class fields are assigned
8
+ listeners;
9
+ /** Subscribe to mutations (set/delete/clear/touch); returns unsubscribe. */
10
+ onChange(cb) {
11
+ (this.listeners ??= new Set()).add(cb);
12
+ return () => { this.listeners?.delete(cb); };
13
+ }
14
+ /** Announce an in-place mutation of a stored value. */
15
+ touch(key) { this.emit(key); }
16
+ emit(key) {
17
+ if (!this.listeners?.size)
18
+ return;
19
+ // copy before iterating: an unsubscribe from inside a callback must not skip the rest
20
+ for (const cb of [...this.listeners])
21
+ cb(key);
22
+ }
23
+ set(key, value) {
24
+ super.set(key, value);
25
+ this.emit(key);
26
+ return this;
27
+ }
28
+ delete(key) {
29
+ const existed = super.delete(key);
30
+ if (existed)
31
+ this.emit(key);
32
+ return existed;
33
+ }
34
+ clear() {
35
+ const hadEntries = this.size > 0;
36
+ super.clear();
37
+ if (hadEntries)
38
+ this.emit();
39
+ }
40
+ }
@@ -385,3 +385,72 @@ body {
385
385
  top: var(--wnd-close-top, -12px);
386
386
  right: var(--wnd-close-right, -12px);
387
387
  }
388
+
389
+ /* Settings dialog (SettingsDialog). Themed via --dlg-* (tokens.css), dark defaults. */
390
+ .wenayDlgScrim {
391
+ position: fixed;
392
+ inset: 0;
393
+ z-index: var(--wenay-z-modal, 9999);
394
+ background: var(--dlg-scrim, rgba(0, 0, 0, 0.5));
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ }
399
+ .wenayDlg {
400
+ position: relative;
401
+ display: flex;
402
+ width: 640px;
403
+ height: 420px;
404
+ max-width: 92vw;
405
+ max-height: 82vh;
406
+ background: var(--dlg-bg, var(--color-bg-dark));
407
+ border: var(--dlg-border, 1px solid var(--color-border-common));
408
+ border-radius: var(--dlg-radius, 10px);
409
+ box-shadow: var(--dlg-shadow, 0 12px 40px rgba(0, 0, 0, 0.5));
410
+ color: var(--color-text-base);
411
+ overflow: hidden;
412
+ }
413
+ .wenayDlgNav {
414
+ width: var(--dlg-nav-width, 168px);
415
+ flex: none;
416
+ background: var(--dlg-nav-bg, var(--color-bg-light));
417
+ padding: 10px 8px;
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 4px;
421
+ overflow-y: auto;
422
+ }
423
+ .wenayDlgContent {
424
+ position: relative;
425
+ flex: 1;
426
+ min-width: 0;
427
+ padding: 14px 40px 14px 16px;
428
+ overflow: auto;
429
+ }
430
+ /* Positioning of the close button in the dialog's top-right corner (over the content column).
431
+ A direct child of .wenayDlg, NOT of the scrollable content, so it does not scroll away. */
432
+ .wenayDlgClose {
433
+ position: absolute;
434
+ top: 8px;
435
+ right: 8px;
436
+ }
437
+ /* Minimal default section/segment buttons; apps override via sectionClassName /
438
+ sectionActiveClassName (SettingsDialog) and className / activeClassName (PlacementSetting) */
439
+ .wenayDlgSection,
440
+ .wenaySegBtn {
441
+ padding: 6px 10px;
442
+ border-radius: 6px;
443
+ cursor: pointer;
444
+ user-select: none;
445
+ text-align: left;
446
+ color: inherit;
447
+ }
448
+ .wenayDlgSection:hover,
449
+ .wenaySegBtn:hover {
450
+ background: rgba(255, 255, 255, 0.08);
451
+ }
452
+ .wenayDlgSectionActive,
453
+ .wenaySegBtnActive {
454
+ background: rgba(255, 255, 255, 0.16);
455
+ color: #fff;
456
+ }
@@ -62,6 +62,16 @@
62
62
  --wnd-close-hover: #fff;
63
63
  --wnd-close-hover-bg: #e5484d;
64
64
 
65
+ /* Settings dialog (SettingsDialog). Dark defaults close to current apps;
66
+ apps re-skin via :root[data-theme='...'] overrides, same contract as --wnd-*. */
67
+ --dlg-scrim: rgba(0, 0, 0, 0.5);
68
+ --dlg-bg: var(--color-bg-dark);
69
+ --dlg-border: 1px solid var(--color-border-common);
70
+ --dlg-radius: 10px;
71
+ --dlg-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
72
+ --dlg-nav-bg: var(--color-bg-light);
73
+ --dlg-nav-width: 168px;
74
+
65
75
  /* Layers */
66
76
  --wenay-z-modal: 9999;
67
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-react2",
3
- "version": "1.0.28",
3
+ "version": "1.0.30",
4
4
  "description": "Common react",
5
5
  "strict": true,
6
6
  "main": "dist/index.js",