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
@@ -19,32 +19,50 @@ const datumMiniConst = {
19
19
  const settingLogs = { params: Params.GetSimpleParams(getSettingLogs()) };
20
20
  const logGridDefaultColDef = { ...colDefCentered, wrapText: true };
21
21
  // varMin - minimum importance
22
- export function getLogsApi(setting) {
23
- const datum = memoryGetOrCreate("settingLogs", settingLogs);
22
+ // Each call builds its OWN state (fresh map, fresh mini feed, own settings - persisted under
23
+ // setting.settingsKey when provided). The global logsApi below injects the legacy module-level
24
+ // state explicitly, so existing consumers of datumConst/datumMiniConst/"settingLogs" keep
25
+ // seeing the same objects as before.
26
+ export function getLogsApi(setting, sharedState) {
27
+ const state = sharedState ?? createLogsControllerState({
28
+ settings: setting.settingsKey
29
+ ? memoryGetOrCreate(setting.settingsKey, { params: Params.GetSimpleParams(getSettingLogs()) })
30
+ : undefined,
31
+ });
24
32
  const controller = createLogsController({
25
33
  options: setting,
26
- state: createLogsControllerState({
27
- full: datumConst,
28
- mini: datumMiniConst,
29
- settings: datum,
30
- }),
31
- onFullChange: () => renderBy(datumConst),
32
- onMiniChange: () => renderBy(datumMiniConst),
33
- onSettingsChange: () => renderBy(datum),
34
+ state,
35
+ onFullChange: () => renderBy(state.full),
36
+ onMiniChange: () => renderBy(state.mini),
37
+ onSettingsChange: () => renderBy(state.settings),
34
38
  });
39
+ // legacy module state -> keep the exact same component identities as before;
40
+ // custom instances get views bound to THEIR state via closures
41
+ const usesLegacyState = state.full === datumConst
42
+ && state.mini === datumMiniConst;
43
+ const Setting = usesLegacyState ? InputSettingLogs
44
+ : (props) => _jsx(InputSettingLogs, { ...props, settings: state.settings });
45
+ const Message = usesLegacyState ? MessageEventLogs
46
+ : (props) => _jsx(MessageEventLogs, { ...props, settings: state.settings, mini: state.mini });
47
+ const Page = usesLegacyState ? PageLogs
48
+ : (props) => _jsx(PageLogs, { ...props, state: state });
35
49
  return {
36
50
  addLogs: controller.addLogs,
37
51
  params: controller.params,
38
52
  React: {
39
- Setting: InputSettingLogs,
40
- Message: MessageEventLogs,
41
- PageLogs: PageLogs
53
+ Setting: Setting,
54
+ Message: Message,
55
+ PageLogs: Page
42
56
  }
43
57
  };
44
58
  }
45
- export const logsApi = getLogsApi({ limitPer: 500 });
46
- function InputSettingLogs({}) {
47
- const datum = memoryGetOrCreate("settingLogs", settingLogs);
59
+ export const logsApi = getLogsApi({ limitPer: 500 }, {
60
+ full: datumConst,
61
+ mini: datumMiniConst,
62
+ settings: memoryGetOrCreate("settingLogs", settingLogs),
63
+ });
64
+ function InputSettingLogs({ settings }) {
65
+ const datum = settings ?? memoryGetOrCreate("settingLogs", settingLogs);
48
66
  return _jsx(ParamsEditor
49
67
  // @ts-ignore
50
68
  , {
@@ -54,104 +72,122 @@ function InputSettingLogs({}) {
54
72
  renderBy(datum);
55
73
  } });
56
74
  }
57
- export function PageLogs({ update }) {
58
- const datumFull = datumConst;
59
- const rowData = [...datumFull.map.values()].flat();
60
- const datum = datumMiniConst;
61
- const setting = memoryGetOrCreate("settingLogs", settingLogs);
75
+ /** Controller for the full-page logs table: owns the ag-grid imperative surface that
76
+ * `PageLogs` used to drive inline. The importance filter lives in ONE method
77
+ * (`applyImportanceFilter`) - it used to be duplicated between the settings effect and
78
+ * `onGridReady`. Rows keep the original identity semantics: the grid receives the mount-time
79
+ * snapshot once, later entries arrive as `applyTransactionAsync` copies of the mini feed. */
80
+ export function useLogsPageTable(state) {
81
+ const setting = state?.settings ?? memoryGetOrCreate("settingLogs", settingLogs);
82
+ const full = state?.full ?? datumConst;
83
+ const mini = state?.mini ?? datumMiniConst;
62
84
  const apiGrid = useRef(null);
63
- useEffect(() => {
64
- apiGrid.current?.api.sizeColumnsToFit();
65
- }, [update]);
66
- updateBy(setting, () => {
67
- if (setting.params.minVarLogs) {
68
- apiGrid.current?.api.setFilterModel({
85
+ // mount-time snapshot: the live grid is fed by transactions, not re-renders
86
+ const [rowData] = useState(() => [...full.map.values()].flat());
87
+ const getApi = useCallback(() => apiGrid.current, []);
88
+ const fit = useCallback(() => { apiGrid.current?.api.sizeColumnsToFit(); }, []);
89
+ const applyImportanceFilter = useCallback((min) => {
90
+ const api = apiGrid.current?.api;
91
+ if (!api)
92
+ return;
93
+ if (min) {
94
+ api.setFilterModel({
69
95
  var: {
70
96
  filterType: 'number',
71
97
  type: 'greaterThanOrEqual',
72
- filter: setting.params.minVarLogs
98
+ filter: min
73
99
  }
74
100
  });
75
101
  }
76
102
  else {
77
- apiGrid.current?.api.destroyFilter("var");
103
+ api.destroyFilter("var");
78
104
  }
105
+ }, []);
106
+ const appendRow = useCallback((row) => {
107
+ apiGrid.current?.api.applyTransactionAsync({ add: [row] });
108
+ }, []);
109
+ // settings change -> single filter method (no re-render: updateBy with a callback)
110
+ updateBy(setting, () => {
111
+ applyImportanceFilter(setting.params.minVarLogs);
79
112
  });
80
- updateBy(datum, () => {
81
- const data = datum.last[0];
82
- if (data) { // data.time timeLocalToStr_yyyymmdd_hhmmss_ms
83
- apiGrid.current?.api.applyTransactionAsync({
84
- add: [{ ...data }]
85
- });
86
- }
113
+ // new mini-feed entry -> async append, copy like before (row identity per event)
114
+ updateBy(mini, () => {
115
+ const data = mini.last[0];
116
+ if (data)
117
+ appendRow({ ...data });
87
118
  });
88
- const Main = useCallback(() => {
89
- const columns = [
90
- {
91
- field: "time",
92
- sort: "desc",
93
- width: 50,
94
- valueFormatter: (e) => e.value ? timeLocalToStr_hhmmss(e.value) : e.value
95
- },
96
- {
97
- field: "id",
98
- width: 20,
99
- },
100
- {
101
- field: "var",
102
- width: 50,
103
- },
104
- {
105
- field: "type1",
106
- width: 50,
107
- },
108
- {
109
- field: "type2",
110
- width: 50,
111
- },
112
- {
113
- field: "type3",
114
- width: 50,
115
- },
116
- {
117
- field: "txt",
118
- wrapText: true,
119
- autoHeight: true,
120
- width: 350
121
- },
122
- {
123
- field: "address",
124
- width: 150,
125
- },
126
- ];
127
- return _jsx("div", { className: "maxSize", children: _jsx(AgGridTable
128
- // className = "ag-theme-alpine-dark ag-theme-alpine2" // ag-theme-alpine-dark3
129
- , {
130
- // className = "ag-theme-alpine-dark ag-theme-alpine2" // ag-theme-alpine-dark3
131
- suppressCellFocus: true, onGridReady: (a) => {
132
- apiGrid.current = a; //as GridReadyEvent<tColum>
133
- apiGrid.current.api.sizeColumnsToFit();
134
- if (setting.params.minVarLogs) {
135
- apiGrid.current.api.setFilterModel({
136
- var: {
137
- filterType: 'number',
138
- type: 'greaterThanOrEqual',
139
- filter: setting.params.minVarLogs
140
- }
141
- });
142
- }
143
- }, onSortChanged: (e) => {
144
- }, defaultColDef: logGridDefaultColDef, headerHeight: 30, rowHeight: 26, autoSizePadding: 1, rowData: rowData, columnDefs: columns, onCellMouseDown: (e) => {
145
- if (e.event instanceof MouseEvent && e.event.button == 2) {
146
- contextMenu.openAt(e.event, [
147
- {
148
- name: "copy", actionKey: "logs.copyCell", onClick: () => { copyToClipboard(e.value); }
149
- }
150
- ]);
119
+ const onGridReady = useCallback((a) => {
120
+ apiGrid.current = a;
121
+ fit();
122
+ // fresh grid has no filter - only apply when the setting asks for one
123
+ if (setting.params.minVarLogs)
124
+ applyImportanceFilter(setting.params.minVarLogs);
125
+ }, [applyImportanceFilter, fit, setting]);
126
+ const columnDefs = useMemo(() => [
127
+ {
128
+ field: "time",
129
+ sort: "desc",
130
+ width: 50,
131
+ valueFormatter: (e) => e.value ? timeLocalToStr_hhmmss(e.value) : e.value
132
+ },
133
+ {
134
+ field: "id",
135
+ width: 20,
136
+ },
137
+ {
138
+ field: "var",
139
+ width: 50,
140
+ },
141
+ {
142
+ field: "type1",
143
+ width: 50,
144
+ },
145
+ {
146
+ field: "type2",
147
+ width: 50,
148
+ },
149
+ {
150
+ field: "type3",
151
+ width: 50,
152
+ },
153
+ {
154
+ field: "txt",
155
+ wrapText: true,
156
+ autoHeight: true,
157
+ width: 350
158
+ },
159
+ {
160
+ field: "address",
161
+ width: 150,
162
+ },
163
+ ], []);
164
+ const gridProps = useMemo(() => ({
165
+ suppressCellFocus: true,
166
+ onGridReady,
167
+ defaultColDef: logGridDefaultColDef,
168
+ headerHeight: 30,
169
+ rowHeight: 26,
170
+ autoSizePadding: 1,
171
+ rowData,
172
+ columnDefs,
173
+ onCellMouseDown: (e) => {
174
+ if (e.event instanceof MouseEvent && e.event.button == 2) {
175
+ contextMenu.openAt(e.event, [
176
+ {
177
+ name: "copy", actionKey: "logs.copyCell", onClick: () => { copyToClipboard(e.value); }
151
178
  }
152
- } }) });
153
- }, [true]);
154
- return _jsx(Main, {});
179
+ ]);
180
+ }
181
+ },
182
+ }), [columnDefs, onGridReady, rowData]);
183
+ return { getApi, fit, applyImportanceFilter, appendRow, onGridReady, columnDefs, gridProps };
184
+ }
185
+ export function PageLogs({ update, state }) {
186
+ const table = useLogsPageTable(state);
187
+ useEffect(() => {
188
+ table.fit();
189
+ }, [update]);
190
+ return _jsx("div", { className: "maxSize", children: _jsx(AgGridTable, { ...table.gridProps }) });
155
191
  }
156
192
  export function MessageEventLogCard({ logs }) {
157
193
  return _jsxs("div", { className: "testAnime", style: { width: "200px", color: logStyleTokens.text, height: "auto", marginTop: "10px", borderRight: `5px solid ${logStyleTokens.accent}`, background: logSeverityBackground(logs.var) }, children: [_jsx("p", { style: { textAlign: "center", fontSize: "10px", marginBottom: "1px" }, children: "notification" }), _jsx("hr", { style: {
@@ -164,7 +200,8 @@ export function MessageEventLogCard({ logs }) {
164
200
  } }), _jsx("div", { style: { textAlign: "right", marginRight: "10px", height: "auto", overflowWrap: "break-word", textOverflow: "ellipsis" }, children: typeof logs.txt == "object" ? JSON.stringify(logs.txt) : logs.txt }), _jsx("p", { style: { float: "inline-end", textAlign: "right", marginRight: "10px" }, children: (new Date(logs.time)).toLocaleDateString() })] });
165
201
  }
166
202
  export function useMessageEventLogsController(options = {}) {
167
- const setting = memoryGetOrCreate("settingLogs", settingLogs);
203
+ const setting = options.settings ?? memoryGetOrCreate("settingLogs", settingLogs);
204
+ const mini = options.mini ?? datumMiniConst;
168
205
  const maxVisible = Math.max(1, options.maxVisible ?? 10);
169
206
  const [notifications, setNotifications] = useState([]);
170
207
  const counterRef = useRef(0);
@@ -185,18 +222,20 @@ export function useMessageEventLogsController(options = {}) {
185
222
  timersRef.current.set(key, timer);
186
223
  }, [setting]);
187
224
  const onMiniChange = useCallback(() => {
188
- const last = datumMiniConst.last[0];
225
+ const last = mini.last[0];
189
226
  if (!last || last === lastLogRef.current)
190
227
  return;
191
228
  lastLogRef.current = last;
192
229
  addNotification(last);
193
- }, [addNotification]);
194
- updateBy(datumMiniConst, onMiniChange);
230
+ }, [addNotification, mini]);
231
+ updateBy(mini, onMiniChange);
195
232
  useEffect(() => () => {
196
233
  timersRef.current.forEach(clearTimeout);
197
234
  timersRef.current.clear();
198
235
  }, []);
199
236
  const setShow = useCallback((value) => {
237
+ // LogsSettingsState.params is readonly at the type level; keep the legacy in-place mutation
238
+ ;
200
239
  setting.params.show = value;
201
240
  renderBy(setting);
202
241
  }, [setting]);
@@ -236,8 +275,8 @@ export function MessageEventLogsView({ controller, zIndex, className, style }) {
236
275
  };
237
276
  return _jsxs("div", { className: className, style: { maxHeight: "50vh", position: "absolute", right: "1px", zIndex, ...style }, children: [_jsx("button", { type: "button", "aria-label": toggleTitle, title: toggleTitle, onClick: controller.toggleShow, style: toggleStyle, children: controller.show ? "\u00d7" : "log" }), _jsx("div", { children: controller.show ? controller.visibleNotifications.map(e => (_jsx("div", { className: "example-exit", children: _jsx(MessageEventLogCard, { logs: e.logs }) }, e.key))) : null })] });
238
277
  }
239
- export function MessageEventLogs({ zIndex }) {
240
- const controller = useMessageEventLogsController();
278
+ export function MessageEventLogs({ zIndex, settings, mini }) {
279
+ const controller = useMessageEventLogsController({ settings, mini });
241
280
  return _jsx(MessageEventLogsView, { controller: controller, zIndex: zIndex });
242
281
  }
243
282
  const defPageBase = {
@@ -12,6 +12,9 @@ export type LogsApiOptions = {
12
12
  limit?: number;
13
13
  limitPer: number;
14
14
  varMin?: number;
15
+ /** memoryGetOrCreate key for this instance's settings; without it a fresh
16
+ * (non-persisted) settings object is used. The global logsApi keeps "settingLogs". */
17
+ settingsKey?: string;
15
18
  };
16
19
  export declare const getSettingLogs: () => {
17
20
  minVarLogs: {
@@ -46,3 +46,15 @@ export declare function createCacheMap(arr: [k: string, v: Map<string, unknown>]
46
46
  isDirty(): boolean;
47
47
  getArr: [k: string, v: Map<string, unknown>][];
48
48
  };
49
+ export type CacheMap = ReturnType<typeof createCacheMapWithStorage>;
50
+ /** The app-side persistence contract as one hook: load() on mount, then subscribe the dirty
51
+ * channel to saveDebounced(delay). The app still owns the write policy - this only wires the
52
+ * documented default (`doc/EXAMPLE_USAGE.md`). Returns pass-through methods for the rare
53
+ * imperative needs; `reload` is an explicit alias of `load` (a second load() merges storage
54
+ * on top of current maps - it is not a reset). */
55
+ export declare function useCacheMapPersistence(cache: CacheMap, delay?: number): {
56
+ isDirty: () => boolean;
57
+ flush: () => Promise<void>;
58
+ save: () => Promise<void>;
59
+ reload: () => Promise<void>;
60
+ };
Binary file
@@ -0,0 +1,15 @@
1
+ /** Shared "fixed entries pin back to their descriptor index" invariant.
2
+ *
3
+ * columnState.normalize(), ColumnsMenu.movedOrder(), Toolbar.normalize() and
4
+ * Toolbar.Settings.movedOrder() used to inline byte-identical copies of this idiom, with
5
+ * comments demanding they stay in sync (the drag preview must land exactly where the commit
6
+ * does). One implementation makes that invariant structural instead of disciplinary. */
7
+ export type FixedOrderDescriptor = {
8
+ key: string;
9
+ fixed?: boolean;
10
+ };
11
+ /** Drop fixed keys from `order`, then pin every fixed descriptor back at its descriptor index. */
12
+ export declare function pinFixedOrder(order: readonly string[], descriptors: readonly FixedOrderDescriptor[]): string[];
13
+ /** The shared drag preview: splice-move `key` to index `to`, then re-pin fixed entries.
14
+ * Same result as committing the move through normalize(). */
15
+ export declare function movedOrderWithFixed(order: readonly string[], key: string, to: number, descriptors: readonly FixedOrderDescriptor[]): string[];
@@ -0,0 +1,30 @@
1
+ /** Shared "fixed entries pin back to their descriptor index" invariant.
2
+ *
3
+ * columnState.normalize(), ColumnsMenu.movedOrder(), Toolbar.normalize() and
4
+ * Toolbar.Settings.movedOrder() used to inline byte-identical copies of this idiom, with
5
+ * comments demanding they stay in sync (the drag preview must land exactly where the commit
6
+ * does). One implementation makes that invariant structural instead of disciplinary. */
7
+ /** Drop fixed keys from `order`, then pin every fixed descriptor back at its descriptor index. */
8
+ export function pinFixedOrder(order, descriptors) {
9
+ const fixed = new Set();
10
+ for (const d of descriptors)
11
+ if (d.fixed)
12
+ fixed.add(d.key);
13
+ const res = order.filter(k => !fixed.has(k));
14
+ descriptors.forEach(function pinFixed(d, i) {
15
+ if (d.fixed)
16
+ res.splice(Math.min(i, res.length), 0, d.key);
17
+ });
18
+ return res;
19
+ }
20
+ /** The shared drag preview: splice-move `key` to index `to`, then re-pin fixed entries.
21
+ * Same result as committing the move through normalize(). */
22
+ export function movedOrderWithFixed(order, key, to, descriptors) {
23
+ const next = order.slice();
24
+ const from = next.indexOf(key);
25
+ if (from == -1)
26
+ return next;
27
+ next.splice(from, 1);
28
+ next.splice(Math.max(0, Math.min(next.length, to)), 0, key);
29
+ return pinFixedOrder(next, descriptors);
30
+ }
@@ -1,9 +1,11 @@
1
1
  export * from './gridRows';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './fixedOrder';
4
5
  export * from './callbackHub';
5
6
  export * from './inputAutoStep';
6
7
  export * from './memoryStore';
7
8
  export * from './observableMap';
8
9
  export * from './pageVisibilityContext';
9
10
  export * from './searchHistory';
11
+ export * from './structEqual';
@@ -1,9 +1,11 @@
1
1
  export * from './gridRows';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './fixedOrder';
4
5
  export * from './callbackHub';
5
6
  export * from './inputAutoStep';
6
7
  export * from './memoryStore';
7
8
  export * from './observableMap';
8
9
  export * from './pageVisibilityContext';
9
10
  export * from './searchHistory';
11
+ export * from './structEqual';
@@ -27,11 +27,8 @@ export declare const memoryCache: {
27
27
  getArr: [k: string, v: Map<string, unknown>][];
28
28
  };
29
29
  export declare const memoryMaps: {
30
- rnd: ObservableMap<string, import("../components/Dnd/FloatingWindow").FloatingWindowSavedGeometry>;
31
- resize: ObservableMap<string, {
32
- height?: number | string;
33
- width?: number | string;
34
- }>;
35
- rightMenu: ObservableMap<string, import("../components/Menu/RightMenuStore").MenuRightSavedState>;
30
+ rnd: ObservableMap<string, import("../components/Dnd").FloatingWindowSavedGeometry>;
31
+ resize: ObservableMap<string, import("./persistedMaps").ResizableSavedSize>;
32
+ rightMenu: ObservableMap<string, import("../components/Menu").MenuRightSavedState>;
36
33
  other: ObservableMap<string, object>;
37
34
  };
@@ -1,8 +1,6 @@
1
1
  import { deepClone } from "wenay-common2";
2
2
  import { renderBy } from "../../updateBy";
3
- import { floatingWindowMap } from "../components/Dnd/FloatingWindow";
4
- import { mapResiReact } from "../components/Dnd/Resizable";
5
- import { mapRightMenu } from "../components/Menu/RightMenuStore";
3
+ import { floatingWindowMap, mapResiReact, mapRightMenu } from "./persistedMaps";
6
4
  import { createCacheMap } from "./cache";
7
5
  import { ObservableMap } from "./observableMap";
8
6
  // observable - memoryCache marks itself dirty on its mutations
@@ -0,0 +1,16 @@
1
+ import { ObservableMap } from "./observableMap";
2
+ import type { FloatingWindowSavedGeometry } from "../components/Dnd/FloatingWindow";
3
+ import type { MenuRightSavedState } from "../components/Menu/RightMenuStore";
4
+ /** Persisted-state maps live in a utils LEAF so the runtime dependency graph points one way:
5
+ * owning components import their map from here, and memoryStore assembles the memoryCache
6
+ * registry without reaching up into the component layer (it used to import FloatingWindow/
7
+ * Resizable/RightMenuStore, dragging react-rnd and the Menu tree into every utils consumer).
8
+ * The `import type` lines above are erased at build - shapes stay documented at their owners. */
9
+ /** Saved size of an FResizableReact column/box; the shape the resize layer persists. */
10
+ export type ResizableSavedSize = {
11
+ height?: number | string;
12
+ width?: number | string;
13
+ };
14
+ export declare const floatingWindowMap: ObservableMap<string, FloatingWindowSavedGeometry>;
15
+ export declare const mapResiReact: ObservableMap<string, ResizableSavedSize>;
16
+ export declare const mapRightMenu: ObservableMap<string, MenuRightSavedState>;
@@ -0,0 +1,5 @@
1
+ import { ObservableMap } from "./observableMap";
2
+ // observable - memoryCache marks itself dirty on their mutations
3
+ export const floatingWindowMap = new ObservableMap();
4
+ export const mapResiReact = new ObservableMap();
5
+ export const mapRightMenu = new ObservableMap();
@@ -7,9 +7,11 @@ export function createSearchHistory(opts) {
7
7
  const max = Math.max(1, opts.max ?? 8);
8
8
  const st = memoryGetOrCreate(opts.key, { items: [] });
9
9
  const stApi = createUpdateApi(st);
10
- function normalize() {
10
+ /** Pure derivation - reads must not rewrite the persisted object (a getter that
11
+ * mutates state silently marks nothing dirty and surprises memoryCache diffing). */
12
+ function normalized() {
11
13
  const seen = new Set();
12
- st.items = st.items
14
+ return st.items
13
15
  .map(normalizeSearchHistoryItem)
14
16
  .filter(Boolean)
15
17
  .filter(item => {
@@ -20,20 +22,19 @@ export function createSearchHistory(opts) {
20
22
  return true;
21
23
  })
22
24
  .slice(0, max);
23
- return st.items;
24
25
  }
25
26
  function emit() {
26
- normalize();
27
+ st.items = normalized(); // normalization happens on WRITE, announced below
27
28
  stApi.render();
28
29
  memoryMarkDirty(opts.key);
29
30
  }
30
31
  return {
31
32
  get items() {
32
- return normalize().slice();
33
+ return normalized();
33
34
  },
34
35
  use() {
35
36
  stApi.use();
36
- return normalize().slice();
37
+ return normalized();
37
38
  },
38
39
  add(value) {
39
40
  const item = normalizeSearchHistoryItem(value);
@@ -0,0 +1,12 @@
1
+ /** Structural deep equality for plain config/data trees (objects, arrays, primitives).
2
+ *
3
+ * Replacement for the `JSON.stringify(a) == JSON.stringify(b)` idiom, with the same
4
+ * tolerance but none of its key-order sensitivity:
5
+ * - object key ORDER does not matter (stringify's false-positive "changed");
6
+ * - object props with `undefined` values count as absent (JSON drops them);
7
+ * - `NaN` equals `NaN` (JSON serializes both to `null`);
8
+ * - no serialization cost, early exit on first difference.
9
+ *
10
+ * Not for class instances / Maps / Sets / cycles - data that would not survive
11
+ * JSON round-tripping did not work with the stringify idiom either. */
12
+ export declare function structEqual(a: unknown, b: unknown): boolean;
@@ -0,0 +1,40 @@
1
+ /** Structural deep equality for plain config/data trees (objects, arrays, primitives).
2
+ *
3
+ * Replacement for the `JSON.stringify(a) == JSON.stringify(b)` idiom, with the same
4
+ * tolerance but none of its key-order sensitivity:
5
+ * - object key ORDER does not matter (stringify's false-positive "changed");
6
+ * - object props with `undefined` values count as absent (JSON drops them);
7
+ * - `NaN` equals `NaN` (JSON serializes both to `null`);
8
+ * - no serialization cost, early exit on first difference.
9
+ *
10
+ * Not for class instances / Maps / Sets / cycles - data that would not survive
11
+ * JSON round-tripping did not work with the stringify idiom either. */
12
+ export function structEqual(a, b) {
13
+ if (a === b)
14
+ return true;
15
+ if (typeof a == 'number' && typeof b == 'number')
16
+ return Number.isNaN(a) && Number.isNaN(b);
17
+ if (typeof a != 'object' || typeof b != 'object' || a == null || b == null)
18
+ return false;
19
+ const aArr = Array.isArray(a);
20
+ if (aArr != Array.isArray(b))
21
+ return false;
22
+ if (aArr) {
23
+ const x = a, y = b;
24
+ if (x.length != y.length)
25
+ return false;
26
+ for (let i = 0; i < x.length; i++)
27
+ if (!structEqual(x[i], y[i]))
28
+ return false;
29
+ return true;
30
+ }
31
+ const ao = a, bo = b;
32
+ const ka = Object.keys(ao).filter(k => ao[k] !== undefined);
33
+ const kb = Object.keys(bo).filter(k => bo[k] !== undefined);
34
+ if (ka.length != kb.length)
35
+ return false;
36
+ for (const k of ka)
37
+ if (!structEqual(ao[k], bo[k]))
38
+ return false;
39
+ return true;
40
+ }
@@ -2,7 +2,10 @@ import React from "react";
2
2
  type Listener = (a?: any) => void;
3
3
  interface ObserverState {
4
4
  listeners: Set<Listener>;
5
+ callbacks: Set<Listener>;
5
6
  version: number;
7
+ running: boolean;
8
+ pending: boolean;
6
9
  }
7
10
  export declare const map3: WeakMap<object, ObserverState>;
8
11
  export declare const mapWait: Map<object, {