wenay-react2 1.0.23 → 1.0.25

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.
package/README.md CHANGED
@@ -1,384 +1,58 @@
1
- Here is an expanded API guide with typings and usage examples. This format is suitable for documentation so other developers or AI assistants can quickly understand which props a component accepts and how to integrate it.
1
+ # wenay-react2
2
2
 
3
- ---
3
+ Common React UI primitives for wenay apps.
4
4
 
5
- ## API Reference (Components & Hooks)
5
+ Use the new concise API guide first:
6
6
 
7
- ### Modal and Floating Windows (MDI)
7
+ - [wenay-react2.md](./wenay-react2.md) - canonical everyday API.
8
+ - [wenay-react2-rare.md](./wenay-react2-rare.md) - old names, low-level exports, migration notes.
9
+ - [agGrid4 README](./src/common/src/grid/agGrid4/README.md) - table buffer/controller details.
10
+ - [agGrid4 wrapper boundary](./src/common/src/grid/agGrid4/WRAPPER.md) - where generic primitives end and app wrappers begin.
8
11
 
9
- #### `DivRnd3`
10
- Wrapper for creating freely movable, resizable windows with state persistence.
12
+ ## Import
11
13
 
12
- **Typing:**
13
- ```typescript
14
- type tDivRndBase = {
15
- children: React.ReactElement | ((update: number) => React.ReactElement);
16
- position?: { x: number; y: number };
17
- size?: { height: number | string; width: number | string };
18
- keyForSave?: string; // Key for caching the position/size in memory
19
- limit?: { x?: { max?: number; min?: number }, y?: { max?: number; min?: number } };
20
- onCLickClose?: () => void; // Adds a close button to the window
21
- header?: React.ReactElement | boolean; // Drag handle header; otherwise the whole block is draggable
22
- zIndex?: number;
23
- }
14
+ ```ts
15
+ import { useModal, useAgGrid, createGridBuffer, createColumnBuffer, useStoreMirror } from "wenay-react2"
24
16
  ```
25
17
 
26
- **Example:**
27
- ```textmate
28
- <DivRnd3
29
- keyForSave="my_tool_window"
30
- size={{ width: 300, height: 200 }}
31
- onCLickClose={() => setOpen(false)}
32
- header={<div>My tool</div>}
33
- >
34
- <div>Window content</div>
35
- </DivRnd3>
36
- ```
37
-
38
-
39
- #### `GetModalJSX()`
40
- Factory for global modal windows. It lets business logic open windows without being tied to local component state.
41
-
42
- **Example:**
43
- ```textmate
44
- const myModal = GetModalJSX();
45
-
46
- // 1. In the app root file (App.tsx):
47
- <myModal.Render />
48
-
49
- // 2. Anywhere in the logic:
50
- myModal.set(<div onClick={() => myModal.set(null)}>Close me</div>);
51
- // For multiple windows: myModal.addJSX(<Component/>)
52
- ```
53
-
54
-
55
- ---
56
-
57
- ### Drag-and-Drop and Interaction (Hooks & Wrappers)
58
-
59
- #### `useOutside` / `DivOutsideClick`
60
- Tracks clicks outside an element, useful for closing dropdowns and menus.
61
-
62
- **Typing:**
63
- ```typescript
64
- // Hook
65
- function useOutside(params: { outsideClick: () => void, status?: boolean, ref?: RefObject }): RefObject;
66
-
67
- // Component
68
- type Props = { outsideClick: () => void, status?: boolean, zIndex?: number } & HTMLAttributes;
69
- ```
70
-
71
- **Example:**
72
- ```textmate
73
- <DivOutsideClick outsideClick={() => setDropdownOpen(false)} status={isOpen}>
74
- <div className="dropdown-menu">...</div>
75
- </DivOutsideClick>
76
- ```
77
-
78
-
79
- #### `Drag22`
80
- Tracks mouse/finger movement and returns new coordinates without touching CSS directly.
81
-
82
- **Typing:**
83
- ```typescript
84
- type Drag2Props = {
85
- x?: number; y?: number; // Start/current coordinates
86
- onX?: (val: number) => void; // X change callback
87
- onY?: (val: number) => void; // Y change callback
88
- onStart?: () => void; onStop?: () => void;
89
- children: ReactNode;
90
- };
91
- ```
92
-
93
-
94
- ---
95
-
96
- ### Context Menus (`MenuR` / `mouseMenuApi`)
97
-
98
- Smart context menus. They open on desktop via right click and on mobile via long tap / double tap. They support asynchronous item generation.
99
-
100
- **Menu item typing (`tMenuReactStrictly`):**
101
- ```typescript
102
- type tMenuReactStrictly = {
103
- name: string | ((status?: any) => string);
104
- onClick?: (e: any) => void | Promise<any> | Promise<any>[]; // Promise support with ok/error counters
105
- status?: boolean; // Whether the submenu is expanded
106
- next?: () => tMenuReact[] | Promise<tMenuReact[]>; // Nested menu
107
- func?: () => React.ReactElement | Promise<React.ReactElement>; // Custom hover render
108
- };
109
- ```
110
-
111
- **Examples:**
112
- ```textmate
113
- // 1. Using the wrapper component
114
- const menuItems = () => [
115
- { name: "Copy", onClick: () => copy() },
116
- { name: "Options", next: () => [{ name: "Option 1", onClick: () => {} }] }
117
- ];
118
-
119
- <MenuR other={menuItems}>
120
- <div className="item">Right-click me</div>
121
- </MenuR>
122
-
123
- // 2. Global call through the API
124
- mouseMenuApi.map.set("global_action", [{ name: "Global action", onClick: () => {} }]);
125
- ```
126
-
127
-
128
- ---
129
-
130
- ### Ag-Grid Table Updates (`applyTransactionAsyncUpdate2`)
131
-
132
- Function for safe batched asynchronous table data updates synchronized with a local buffer/cache.
133
-
134
- **Typing:**
135
- ```typescript
136
- type Params<T> = {
137
- gridRef?: React.RefObject<GridReadyEvent<T, any>>;
138
- newData: Partial<T>[]; // New data array
139
- getId: (row: Partial<T>) => string; // ID getter
140
- bufTable: { [id: string]: Partial<T> }; // Local cache dictionary
141
- option?: { update?: boolean, add?: boolean, updateBuffer?: boolean, sync?: boolean };
142
- };
143
- ```
144
-
145
- **Example:**
146
- ```textmate
147
- // Immediately updates the cache and sends a transaction to Ag-Grid
148
- applyTransactionAsyncUpdate2({
149
- gridRef: apiGrid,
150
- newData: [{ id: "user_1", balance: 500 }],
151
- getId: (e) => e.id,
152
- bufTable: myLocalBuffer,
153
- option: { update: true, add: true }
154
- });
155
- ```
156
-
157
-
158
- ---
159
-
160
- ### UI Settings Autogeneration (`ParametersReact`)
161
-
162
- Engine that accepts an object with a data schema and renders a ready-to-use control form with inputs, sliders, and selects.
163
-
164
- **Typing:**
165
- The schema is built from `Params.IParamsExpandableReadonly` types.
166
- Supported types: `number`, `string`, `boolean`, `Date`, arrays, and nested objects.
167
-
168
- **Example:**
169
- ```textmate
170
- const mySettings = {
171
- showGrid: true, // Generates a Checkbox
172
- opacity: {
173
- value: 0.5,
174
- range: { min: 0, max: 1, step: 0.1 },
175
- name: "Opacity"
176
- }, // Generates a Range Slider + Number Input
177
- theme: {
178
- value: "dark",
179
- range: ["dark", "light", "system"]
180
- } // Generates a Select
181
- };
18
+ The root export is still flat. A grouped namespace is also exported for large files:
182
19
 
183
- <ParametersReact
184
- params={mySettings}
185
- onChange={(newParams) => {
186
- console.log("New value:", newParams.opacity.value);
187
- }}
188
- />
189
- ```
190
-
191
-
192
- ---
193
-
194
- ### Logging System (`logsApi`)
195
-
196
- Log registration, log viewer table, and toast notification system.
197
-
198
- **API:**
199
- ```typescript
200
- // 1. Adding a log
201
- logsApi.addLogs({
202
- id: "system",
203
- var: 10, // Severity, important for notification filtering
204
- time: new Date(),
205
- txt: "Connection lost"
206
- });
207
-
208
- // 2. Rendering components, usually somewhere near the root:
209
- <logsApi.React.Message zIndex={9999} /> // Toast notifications in the top-right corner
210
- <logsApi.React.PageLogs /> // Ag-Grid table with all logs
211
- ```
212
-
213
- There are several other important architectural patterns and UI components actively used in the codebase. They should be documented so another developer or AI assistant can understand how interfaces are built in this project.
214
-
215
- Here is the second part of the `README.md` additions:
216
-
217
- ---
218
-
219
- ### Sidebar Navigation
220
-
221
- #### `ApiLeftMenu`
222
- Ready-made API for controlling the left slide-out sidebar menu. It supports swipes, smooth snap scrolling, and imperative tab control.
223
-
224
- **API and Typing:**
225
- ```typescript
226
- type MenuItem = {
227
- el: () => React.JSX.Element; // Tab component
228
- button?: React.JSX.Element; // Custom button, optional
229
- color?: ColorString; // Background color
230
- textB?: string; // Default button text
231
- };
232
-
233
- // Register menu items:
234
- ApiLeftMenu.setMenu(items: MenuItem[], key?: string);
235
- ```
236
-
237
- **Usage example:**
238
- ```textmate
239
- // 1. Register tabs, can be done anywhere in the logic:
240
- ApiLeftMenu.setMenu([
241
- { textB: "Dashboard", el: () => <Dashboard />, color: "rgb(92,50,213)" },
242
- { textB: "Settings", el: () => <Settings /> }
243
- ], "main_menu");
244
-
245
- // 2. Render the menu itself in the root Layout:
246
- export function AppLayout() {
247
- return <ApiLeftMenu.Modal2 zIndex={20} />;
248
- }
249
- ```
250
-
251
-
252
- ---
253
-
254
- ### Interactive Elements and Buttons
20
+ ```ts
21
+ import { v2 } from "wenay-react2"
255
22
 
256
- #### `Button` (from `useOutside.tsx`) / `MiniButton`
257
- Advanced button components that encapsulate dropdown logic, popover panels, and outside-click tracking for automatic closing.
258
-
259
- **`Button` typing:**
260
- ```typescript
261
- type tButton = {
262
- button: ReactElement | ((status: boolean) => ReactElement); // The button itself; can change by status
263
- children: ReactNode | ((api: {onClose: () => void}) => ReactNode); // Dropdown content
264
- outClick?: boolean | (() => void); // Whether to close on outside click; true by default
265
- statusDef?: boolean; // Initial state, open/closed
266
- };
267
- ```
268
-
269
- **Example:**
270
- ```textmate
271
- <Button
272
- outClick={true}
273
- button={(isOpen) => <div className={isOpen ? "active" : ""}>Options</div>}
274
- >
275
- {({ onClose }) => (
276
- <div className="dropdown-panel">
277
- <div onClick={() => { doSomething(); onClose(); }}>Action 1</div>
278
- </div>
279
- )}
280
- </Button>
23
+ v2.grid.useAgGrid(...)
24
+ v2.modal.useModal()
281
25
  ```
282
26
 
27
+ ## API Standard
283
28
 
284
- #### `FResizableReact`
285
- Wrapper over `re-resizable`. It creates resizable panels, for example columns or lower log panes, and stores their size in cache.
286
-
287
- **Example:**
288
- ```textmate
289
- <FResizableReact
290
- keyForSave="bottom_panel_size"
291
- size={{ height: 200, width: "100%" }}
292
- moveWith={false} // Allow resize only by height
293
- >
294
- <LogsTable />
295
- </FResizableReact>
296
- ```
297
-
298
-
299
- ---
300
-
301
- ### Global Reactivity (`renderBy` / `updateBy` Pattern)
302
-
303
- *(Note: this is a key project concept used instead of classic `useState` / `Redux` for complex business logic.)*
304
-
305
- Many places in the project use a mutable state approach: you mutate properties of a plain JS object and call `renderBy(obj)` to force an update of all components subscribed to that object through `updateBy(obj)`.
306
-
307
- **Pattern example:**
308
- ```textmate
309
- import { renderBy, updateBy } from "./updateBy";
29
+ New code should use the short controller-style surface:
310
30
 
311
- // 1. Global or local state, a plain object
312
- const myState = {
313
- count: 0,
314
- text: "hello"
315
- };
31
+ ```tsx
32
+ const modal = useModal()
33
+ modal.open(<Dialog />)
34
+ modal.close()
316
35
 
317
- // 2. Consumer component
318
- function CounterViewer() {
319
- // The component subscribes to myState changes
320
- updateBy(myState);
36
+ const grid = useAgGrid<Row>({getId: row => row.id})
37
+ grid.update({newData})
38
+ grid.clean()
39
+ grid.flush()
321
40
 
322
- return <div>{myState.count}</div>;
323
- }
324
-
325
- // 3. State change, anywhere, even outside React
326
- function increment() {
327
- myState.count += 1;
328
- renderBy(myState); // Triggers a CounterViewer rerender
329
- }
41
+ const mirror = useStoreMirror(remoteStore, initialState, {mask, current: true})
330
42
  ```
331
43
 
44
+ Old `Get*`, `*2/*3`, `FuncJSX`, and legacy grid helper names remain documented in `wenay-react2-rare.md` for existing code, but they are not the teaching path for new code.
332
45
 
333
- ---
334
-
335
- ### Useful Ag-Grid Utilities
336
-
337
- #### `GridStyleDefault` and `StyleCSSHeadGrid`
338
- Quickly apply the common corporate style to all application tables.
46
+ ## QA Stand
339
47
 
340
- **Root integration example:**
341
- ```typescript
342
- import { GridStyleDefault, StyleCSSHeadGrid } from "./styleGrid";
343
-
344
- // Applies the dark theme, compresses spacing, and centers headers
345
- GridStyleDefault();
346
- StyleCSSHeadGrid();
48
+ ```sh
49
+ npm run testReact -- --host 127.0.0.1 --port 3002
347
50
  ```
348
51
 
52
+ The stand source is `src/common/testUseReact/qa.tsx`. Use it for visual checks after changing UI primitives.
349
53
 
350
- #### `getComparatorGrid`
351
- Factory for creating custom Ag-Grid column sort functions that correctly handle `undefined`, `NaN`, and inversion.
352
- **Example:**
353
- ```textmate
354
- const columnDefs = [
355
- {
356
- field: "price",
357
- comparator: getComparatorGrid() // Safe numeric sorting
358
- }
359
- ];
360
- ```
361
-
362
-
363
- ---
364
-
365
- ### Global Hooks
366
-
367
- #### `useAddDownAnyKey`
368
- Registers a global keyboard listener and stores the last pressed key in the exported reactive object `KeyDown`.
369
-
370
- **Usage example:**
371
- ```textmate
372
- import { KeyDown, useAddDownAnyKey } from "./useAddDownAnyKey";
373
- import { updateBy } from "./updateBy";
374
-
375
- function HotkeyListener() {
376
- useAddDownAnyKey(); // Hook initialization
377
- updateBy(KeyDown); // Subscribe to key presses
54
+ ## Build
378
55
 
379
- if (KeyDown.key === "Escape") {
380
- return <div>Escape pressed!</div>;
381
- }
382
- return null;
383
- }
56
+ ```sh
57
+ npm run build
384
58
  ```
@@ -1,25 +1,34 @@
1
1
  import React from 'react';
2
2
  import { AgGridReactProps } from 'ag-grid-react';
3
3
  import type { GridApi, GetRowIdParams, GridPreDestroyedEvent, GridReadyEvent } from 'ag-grid-community';
4
- import { type BufferTable, type GetId } from './core';
4
+ import { type BufferTable, type GetId, type GridBufferCore, type GridBufferMode, type PushOptions } from './core';
5
5
  export type UseAgGridOptions<T> = {
6
6
  /** How to get a row id. Defaults to the `id` field. Captured once (first render). */
7
7
  getId?: GetId<T>;
8
8
  /** External buffer: a plain object above the component (like datum.tableArr) that survives route remounts. */
9
9
  externalBuffer?: BufferTable<T>;
10
+ /** mirror (default): buffer owns rows. overlay: rowData owns rows and sync only refreshes intersection. */
11
+ mode?: GridBufferMode;
12
+ /** Default delivery policy for updateData. Useful for overlay streams: { add: false }. */
13
+ pushDefaults?: PushOptions;
14
+ /** Ready-made module-level core/store; getId/externalBuffer/mode are already captured inside it. */
15
+ core?: GridBufferCore<T>;
10
16
  };
11
17
  /**
12
18
  * Headless table hook. All race handling lives in the core; the hook only binds it
13
19
  * to the React and grid lifecycle:
14
20
  *
15
21
  * const grid = useAgGrid<Row>({ getId })
22
+ * const grid = useAgGrid({ core: mainTable })
16
23
  * <AgGridMy<Row> controller={grid} columnDefs={cols} />
17
24
  * grid.update({ newData })
18
25
  */
19
26
  export declare function useAgGrid<T>(options?: UseAgGridOptions<T>): {
20
27
  update: (args: import("./core").UpdateArgs<T>) => void;
21
28
  remove(rows: Partial<T>[]): void;
29
+ clean: (args?: import("./core").CleanArgs) => void;
22
30
  fit: () => boolean;
31
+ flush: () => boolean;
23
32
  updateData: (args: import("./core").UpdateArgs<T>) => void;
24
33
  sync: () => void;
25
34
  getId: GetId<T>;
@@ -38,6 +47,7 @@ export declare function useAgGrid<T>(options?: UseAgGridOptions<T>): {
38
47
  getApi(): GridApi<T> | null;
39
48
  withApi<R>(fn: (api: GridApi<T>) => R): R | undefined;
40
49
  sizeColumnsToFit: () => boolean;
50
+ flushAsyncTransactions: () => boolean;
41
51
  };
42
52
  export type AgGridController<T> = ReturnType<typeof useAgGrid<T>>;
43
53
  export type AgGridMyProps<T> = AgGridReactProps<T> & {
@@ -23,6 +23,7 @@ function ensureAgGridModules() {
23
23
  * to the React and grid lifecycle:
24
24
  *
25
25
  * const grid = useAgGrid<Row>({ getId })
26
+ * const grid = useAgGrid({ core: mainTable })
26
27
  * <AgGridMy<Row> controller={grid} columnDefs={cols} />
27
28
  * grid.update({ newData })
28
29
  */
@@ -31,8 +32,15 @@ export function useAgGrid(options) {
31
32
  const apiRef = useRef(null);
32
33
  // The core is created once; dependencies live in its closure, with no useCallback chains.
33
34
  const [core] = useState(function createCore() {
35
+ if (options?.core)
36
+ return options.core;
34
37
  const getId = options?.getId ?? (data => String(data?.id));
35
- return createGridBuffer({ getId, externalBuffer: options?.externalBuffer });
38
+ return createGridBuffer({
39
+ getId,
40
+ externalBuffer: options?.externalBuffer,
41
+ mode: options?.mode,
42
+ pushDefaults: options?.pushDefaults,
43
+ });
36
44
  });
37
45
  const gridProps = useMemo(() => ({
38
46
  getRowId(params) {
@@ -55,12 +63,21 @@ export function useAgGrid(options) {
55
63
  api.sizeColumnsToFit();
56
64
  return true;
57
65
  };
66
+ const flush = () => {
67
+ const api = apiRef.current;
68
+ if (!api)
69
+ return false;
70
+ api.flushAsyncTransactions();
71
+ return true;
72
+ };
58
73
  return {
59
74
  update: core.api.updateData,
60
75
  remove(rows) {
61
76
  core.api.updateData({ removeData: rows });
62
77
  },
78
+ clean: core.api.clean,
63
79
  fit,
80
+ flush,
64
81
  updateData: core.api.updateData,
65
82
  sync: core.api.sync,
66
83
  getId: core.api.getId,
@@ -76,6 +93,7 @@ export function useAgGrid(options) {
76
93
  return api ? fn(api) : undefined;
77
94
  },
78
95
  sizeColumnsToFit: fit,
96
+ flushAsyncTransactions: flush,
79
97
  };
80
98
  }, [core, gridProps]);
81
99
  }
@@ -85,14 +103,18 @@ export function useAgGrid(options) {
85
103
  const ROW_SELECTION = { mode: 'multiRow' };
86
104
  function AgGridMyInner(props) {
87
105
  const { controller, data, autoSizeColumns = true, defaultColDef, theme, getRowId, onGridReady, onGridPreDestroyed, ...rest } = props;
88
- // Own controller for declarative mode (without an external controller); the hook is unconditional.
89
- const own = useAgGrid();
106
+ // Own controller is overlay-safe: rowData grids without a controller must not be cleared on attach.
107
+ const own = useAgGrid({
108
+ getId: getRowId ? data => getRowId({ data: data }) : undefined,
109
+ mode: 'overlay',
110
+ pushDefaults: { add: false, remove: false },
111
+ });
90
112
  const grid = controller ?? own;
91
113
  const defaultTheme = useAgGridTheme();
92
114
  const containerRef = useRef(null);
93
115
  useEffect(function pushData() {
94
116
  if (data)
95
- grid.update({ newData: data });
117
+ grid.update({ newData: data, option: { add: true } });
96
118
  }, [data, grid]);
97
119
  useEffect(function observeResize() {
98
120
  if (!containerRef.current || !autoSizeColumns)
@@ -114,10 +136,11 @@ function AgGridMyInner(props) {
114
136
  };
115
137
  }, [autoSizeColumns, grid]);
116
138
  const mergedColDef = useMemo(() => ({ sortable: true, resizable: true, filter: true, ...defaultColDef }), [defaultColDef]);
139
+ const wiredGetRowId = getRowId ?? (controller || data ? grid.props.getRowId : undefined);
117
140
  return (_jsx("div", { ref: containerRef, style: { height: '100%', width: '100%', overflow: 'hidden' }, children: _jsx(AgGridReact, { theme: theme ?? defaultTheme, asyncTransactionWaitMillis: 50, suppressCellFocus: true, rowSelection: ROW_SELECTION, ...rest, defaultColDef: mergedColDef,
118
141
  // getRowId after the spread: undefined from rest must not erase the wiring,
119
142
  // otherwise the grid falls back to index-based row identity.
120
- getRowId: getRowId ?? grid.props.getRowId, onGridReady: function ready(event) {
143
+ getRowId: wiredGetRowId, onGridReady: function ready(event) {
121
144
  grid.props.onGridReady(event);
122
145
  if (autoSizeColumns)
123
146
  event.api.sizeColumnsToFit();
@@ -0,0 +1,26 @@
1
+ import type { GridApi } from 'ag-grid-community';
2
+ export type ColumnApplyContext<TData = any> = {
3
+ api: GridApi<TData>;
4
+ names: readonly string[];
5
+ };
6
+ export type ColumnAttach<TData = any> = {
7
+ /** Project/page wrapper decides how names affect the grid. */
8
+ apply: (context: ColumnApplyContext<TData>) => void;
9
+ };
10
+ /**
11
+ * Buffer for the exact set of dynamic column names.
12
+ * setNames() means exactly this dynamic set; attach() applies the saved set; detach() keeps names.
13
+ * This utility deliberately knows nothing about groups, columnDefs shape, or business names.
14
+ */
15
+ export declare function createColumnBuffer<TData = any>(): {
16
+ control: {
17
+ attach: (gridApi: GridApi<TData>, opts: ColumnAttach<TData>) => void;
18
+ detach: () => void;
19
+ };
20
+ api: {
21
+ setNames: (list: string[]) => void;
22
+ apply: () => void;
23
+ readonly names: string[];
24
+ };
25
+ };
26
+ export type ColumnBuffer<TData = any> = ReturnType<typeof createColumnBuffer<TData>>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Buffer for the exact set of dynamic column names.
3
+ * setNames() means exactly this dynamic set; attach() applies the saved set; detach() keeps names.
4
+ * This utility deliberately knows nothing about groups, columnDefs shape, or business names.
5
+ */
6
+ export function createColumnBuffer() {
7
+ let names = [];
8
+ let attachOptions = null;
9
+ let api = null;
10
+ function apply() {
11
+ if (api && attachOptions)
12
+ attachOptions.apply({ api, names });
13
+ }
14
+ function attach(gridApi, opts) {
15
+ api = gridApi;
16
+ attachOptions = opts;
17
+ apply();
18
+ }
19
+ function detach() {
20
+ api = null;
21
+ attachOptions = null;
22
+ }
23
+ function setNames(list) {
24
+ names = [...new Set(list)];
25
+ apply();
26
+ }
27
+ return {
28
+ control: { attach, detach },
29
+ api: {
30
+ setNames,
31
+ apply,
32
+ get names() {
33
+ return names;
34
+ },
35
+ },
36
+ };
37
+ }
@@ -9,15 +9,21 @@ export type GridTransaction<T> = {
9
9
  };
10
10
  /** Minimal grid api contract (the real GridApi is structurally compatible). */
11
11
  export type GridApiLike<T> = {
12
+ /** Used by overlay mode: row ownership belongs to rowData, not to this buffer. */
13
+ getRowNode?(id: RowId): {
14
+ data?: T;
15
+ } | undefined;
12
16
  forEachNode(callback: (node: {
13
17
  data?: T;
14
18
  }) => void): void;
15
19
  applyTransaction(tx: GridTransaction<T>): unknown;
16
20
  applyTransactionAsync(tx: GridTransaction<T>): void;
17
21
  };
22
+ export type GridBufferMode = 'mirror' | 'overlay';
18
23
  export type PushOptions = {
19
24
  add?: boolean;
20
25
  update?: boolean;
26
+ remove?: boolean;
21
27
  /** add+update in one SYNCHRONOUS transaction. Default: add is sync, update is async (batched). */
22
28
  sync?: boolean;
23
29
  };
@@ -28,23 +34,36 @@ export type UpdateArgs<T> = {
28
34
  onlyMemo?: boolean;
29
35
  option?: PushOptions;
30
36
  };
37
+ export type CleanArgs = {
38
+ /** Only clear the buffer; do not remove rows from a mirror-owned grid. */
39
+ onlyMemo?: boolean;
40
+ };
41
+ export type CreateGridBufferOptions<T> = {
42
+ getId: GetId<T>;
43
+ /** External buffer (object above the component) survives route remounts. Otherwise use an internal one. */
44
+ externalBuffer?: BufferTable<T>;
45
+ /**
46
+ * mirror (default): the buffer is the source of truth; sync adds, updates and removes grid rows.
47
+ * overlay: external rowData owns rows; sync only refreshes rows that already exist in the grid.
48
+ */
49
+ mode?: GridBufferMode;
50
+ /** Delivery defaults for updateData, e.g. { add: false } for rowData-owned streaming overlays. */
51
+ pushDefaults?: PushOptions;
52
+ };
31
53
  /**
32
54
  * Table core: buffer + delivery to the grid. Lifecycle:
33
55
  * 1. updateData at any time: the buffer is always updated, the grid only if attached;
34
56
  * 2. attach(api) on onGridReady: sync catches up with everything that arrived earlier;
35
57
  * 3. detach() on grid destroy: the core accumulates in the buffer again until the next attach.
36
58
  */
37
- export declare function createGridBuffer<T>(deps: {
38
- getId: GetId<T>;
39
- /** External buffer (object above the component) survives route remounts. Otherwise use an internal one. */
40
- externalBuffer?: BufferTable<T>;
41
- }): {
59
+ export declare function createGridBuffer<T>(deps: CreateGridBufferOptions<T>): {
42
60
  control: {
43
61
  attach: (gridApi: GridApiLike<T>) => void;
44
62
  detach: () => void;
45
63
  };
46
64
  api: {
47
65
  updateData: (args: UpdateArgs<T>) => void;
66
+ clean: (args?: CleanArgs) => void;
48
67
  sync: () => void;
49
68
  getId: GetId<T>;
50
69
  buffer: BufferTable<T>;
@@ -6,7 +6,7 @@
6
6
  // updateData, instead of duplicating buffer/grid branching in the hook;
7
7
  // - in-place row merge (Object.assign), with no new object allocated for each update
8
8
  // (important on streams: less garbage for GC);
9
- // - add vs update is decided by a Set of delivered ids, without api.getRowNode per row.
9
+ // - mirror add vs update is decided by a Set of delivered ids; overlay checks the grid by row id.
10
10
  /**
11
11
  * Table core: buffer + delivery to the grid. Lifecycle:
12
12
  * 1. updateData at any time: the buffer is always updated, the grid only if attached;
@@ -14,10 +14,25 @@
14
14
  * 3. detach() on grid destroy: the core accumulates in the buffer again until the next attach.
15
15
  */
16
16
  export function createGridBuffer(deps) {
17
- const { getId } = deps;
17
+ const { getId, mode = 'mirror', pushDefaults } = deps;
18
18
  const buf = deps.externalBuffer ?? {};
19
19
  const inGrid = new Set(); // ids already delivered to the grid
20
20
  let api = null;
21
+ function gridHas(id) {
22
+ if (!api)
23
+ return false;
24
+ if (mode == 'mirror')
25
+ return inGrid.has(id);
26
+ const node = api.getRowNode?.(id);
27
+ if (node)
28
+ return !!node.data;
29
+ let found = false;
30
+ api.forEachNode(rowNode => {
31
+ if (!found && rowNode.data && getId(rowNode.data) == id)
32
+ found = true;
33
+ });
34
+ return found;
35
+ }
21
36
  // --- Buffer ----------------------------------------------------------------
22
37
  function upsert(rows) {
23
38
  for (const row of rows) {
@@ -36,13 +51,17 @@ export function createGridBuffer(deps) {
36
51
  delete buf[getId(row)];
37
52
  if (onlyMemo || !api)
38
53
  return;
39
- const opt = { add: true, update: true, sync: false, ...option };
54
+ const opt = { add: true, update: true, remove: true, sync: false, ...pushDefaults, ...option };
40
55
  const toAdd = [];
41
56
  const toUpdate = [];
57
+ const seen = new Set();
42
58
  for (const row of newData ?? []) {
43
59
  const id = getId(row);
60
+ if (seen.has(id))
61
+ continue;
62
+ seen.add(id);
44
63
  const merged = buf[id];
45
- if (inGrid.has(id))
64
+ if (gridHas(id))
46
65
  toUpdate.push(merged);
47
66
  else
48
67
  toAdd.push(merged);
@@ -53,7 +72,13 @@ export function createGridBuffer(deps) {
53
72
  for (const row of add)
54
73
  inGrid.add(getId(row));
55
74
  // ag-grid finds rows for remove through getRowId, so partial data is enough.
56
- const remove = removeData?.filter(row => inGrid.delete(getId(row)));
75
+ const remove = opt.remove
76
+ ? removeData?.filter(row => {
77
+ const id = getId(row);
78
+ const delivered = inGrid.delete(id);
79
+ return mode == 'overlay' ? gridHas(id) : delivered;
80
+ })
81
+ : undefined;
57
82
  if (opt.sync) {
58
83
  api.applyTransaction({ add, update, remove });
59
84
  return;
@@ -67,10 +92,24 @@ export function createGridBuffer(deps) {
67
92
  /**
68
93
  * Bring the whole grid in line with the buffer (the buffer is the source of truth).
69
94
  * Missing from the buffer -> remove; missing from the grid -> add; everything else -> update.
95
+ * In overlay mode, rowData is the source of truth, so sync only updates row intersection.
70
96
  */
71
97
  function sync() {
72
98
  if (!api)
73
99
  return;
100
+ if (mode == 'overlay') {
101
+ const update = [];
102
+ api.forEachNode(function refresh(node) {
103
+ if (!node.data)
104
+ return;
105
+ const id = getId(node.data);
106
+ if (buf[id])
107
+ update.push(buf[id]);
108
+ });
109
+ if (update.length)
110
+ api.applyTransaction({ update });
111
+ return;
112
+ }
74
113
  const add = [];
75
114
  const update = [];
76
115
  const remove = [];
@@ -94,6 +133,21 @@ export function createGridBuffer(deps) {
94
133
  if (add.length || update.length || remove.length)
95
134
  api.applyTransaction({ add, update, remove });
96
135
  }
136
+ /** Clear the buffer. In mirror mode, also remove all current grid rows unless onlyMemo is set. */
137
+ function clean(args = {}) {
138
+ const remove = [];
139
+ if (!args.onlyMemo && api && mode == 'mirror') {
140
+ api.forEachNode(node => {
141
+ if (node.data)
142
+ remove.push(node.data);
143
+ });
144
+ }
145
+ for (const id in buf)
146
+ delete buf[id];
147
+ inGrid.clear();
148
+ if (remove.length)
149
+ api?.applyTransaction({ remove });
150
+ }
97
151
  // --- Lifecycle -------------------------------------------------------------
98
152
  function attach(gridApi) {
99
153
  api = gridApi;
@@ -107,7 +161,7 @@ export function createGridBuffer(deps) {
107
161
  // for the React binding (or another grid owner)
108
162
  control: { attach, detach },
109
163
  // public consumer api
110
- api: { updateData, sync, getId, buffer: buf },
164
+ api: { updateData, clean, sync, getId, buffer: buf },
111
165
  };
112
166
  }
113
167
  /** Column comparator: numbers as numbers, empty/NaN values last (respecting inversion). */
@@ -0,0 +1,25 @@
1
+ export { numericComparator } from './core';
2
+ export type { NumberPair } from './core';
3
+ /** Centered cells + sort/filter defaults for dense data tables. */
4
+ export declare const colDefCentered: {
5
+ headerClass: () => string;
6
+ resizable: true;
7
+ cellStyle: {
8
+ textAlign: string;
9
+ };
10
+ sortable: true;
11
+ filter: boolean;
12
+ };
13
+ /** Wrapped text cells for list/selection grids. */
14
+ export declare const colDefWrap: {
15
+ headerClass: () => string;
16
+ resizable: true;
17
+ cellClass: string;
18
+ cellStyle: {
19
+ fontFamily: string;
20
+ fontStyle: string;
21
+ fontWeight: string;
22
+ fontSize: string;
23
+ textAlign: string;
24
+ };
25
+ };
@@ -0,0 +1,17 @@
1
+ import { StyleGridDefault } from '../../styles/styleGrid';
2
+ export { numericComparator } from './core';
3
+ /** Centered cells + sort/filter defaults for dense data tables. */
4
+ export const colDefCentered = {
5
+ headerClass: () => 'gridTable-header',
6
+ resizable: true,
7
+ cellStyle: { textAlign: 'center' },
8
+ sortable: true,
9
+ filter: true,
10
+ };
11
+ /** Wrapped text cells for list/selection grids. */
12
+ export const colDefWrap = {
13
+ headerClass: () => 'gridTable-header',
14
+ resizable: true,
15
+ cellClass: 'cell-wrap-text',
16
+ cellStyle: { ...StyleGridDefault },
17
+ };
@@ -1,6 +1,10 @@
1
- export { createGridBuffer, numericComparator } from './core';
2
- export type { BufferTable, GetId, GridApiLike, GridBufferCore, GridTransaction, NumberPair, PushOptions, RowId, UpdateArgs, } from './core';
1
+ export { createGridBuffer } from './core';
2
+ export type { BufferTable, CleanArgs, CreateGridBufferOptions, GetId, GridApiLike, GridBufferCore, GridBufferMode, GridTransaction, PushOptions, RowId, UpdateArgs, } from './core';
3
+ export { createColumnBuffer } from './columnBuffer';
4
+ export type { ColumnApplyContext, ColumnAttach, ColumnBuffer } from './columnBuffer';
3
5
  export { useAgGrid, AgGridMy } from './agGrid4';
4
6
  export type { AgGridController, AgGridMyProps, UseAgGridOptions } from './agGrid4';
7
+ export { numericComparator, colDefCentered, colDefWrap } from './gridUtils';
8
+ export type { NumberPair } from './gridUtils';
5
9
  export { useAgGridTheme, buildAgTheme } from './theme';
6
10
  export type { tThemeMode } from './theme';
@@ -1,3 +1,5 @@
1
- export { createGridBuffer, numericComparator } from './core';
1
+ export { createGridBuffer } from './core';
2
+ export { createColumnBuffer } from './columnBuffer';
2
3
  export { useAgGrid, AgGridMy } from './agGrid4';
4
+ export { numericComparator, colDefCentered, colDefWrap } from './gridUtils';
3
5
  export { useAgGridTheme, buildAgTheme } from './theme';
@@ -1,3 +1,4 @@
1
1
  export * from './useAddDownAnyKey';
2
2
  export * from './useDraggable';
3
+ export * from './useObserveStore';
3
4
  export * from './useOutside';
@@ -1,3 +1,4 @@
1
1
  export * from './useAddDownAnyKey';
2
2
  export * from './useDraggable';
3
+ export * from './useObserveStore';
3
4
  export * from './useOutside';
@@ -0,0 +1,105 @@
1
+ import { ObserveAll2 } from "wenay-common2";
2
+ type StoreChange = ObserveAll2.StoreChange;
3
+ type StoreSubOpts = ObserveAll2.StoreSubOpts;
4
+ type StoreSyncOpts = ObserveAll2.StoreSyncOpts;
5
+ type StoreDrain = ObserveAll2.StoreDrain;
6
+ type StoreMask<T> = ObserveAll2.StoreMask<T>;
7
+ type StorePick<T, M> = ObserveAll2.StorePick<T, M>;
8
+ type StoreNode<T> = ObserveAll2.StoreNode<T>;
9
+ type StoreSelection<T, M> = ObserveAll2.StoreSelection<T, M>;
10
+ export type ListenLike<TArgs extends readonly unknown[] = readonly unknown[]> = {
11
+ on(cb: (...args: TArgs) => void, opts?: {
12
+ key?: string | symbol;
13
+ current?: boolean | (() => TArgs | undefined);
14
+ }): () => void;
15
+ };
16
+ export type UseStoreNodeOptions<T> = StoreSubOpts & {
17
+ mode?: "get" | "snapshot";
18
+ fallback?: T;
19
+ };
20
+ export type StoreNodeController<T> = {
21
+ readonly node: StoreNode<T>;
22
+ readonly value: T;
23
+ readonly exists: boolean;
24
+ readonly path: PropertyKey[];
25
+ readonly pathString: string;
26
+ refresh(): void;
27
+ get(): T;
28
+ snapshot(): T;
29
+ has(): boolean;
30
+ set(value: T): void;
31
+ replace(value: T): void;
32
+ update<M extends StoreMask<T>>(mask: M, opts?: StoreSubOpts): StoreSelection<T, M>;
33
+ };
34
+ export declare function useStoreNode<T>(node: StoreNode<T>, options?: UseStoreNodeOptions<T>): StoreNodeController<T>;
35
+ export type UseStoreSelectOptions<TValue> = StoreSubOpts & {
36
+ fallback?: TValue;
37
+ };
38
+ export type StoreSelectionController<T, M extends StoreMask<T>> = {
39
+ readonly selection: StoreSelection<T, M>;
40
+ readonly value: StorePick<T, M>;
41
+ readonly mask: M;
42
+ readonly paths: PropertyKey[][];
43
+ refresh(): void;
44
+ get(): StorePick<T, M>;
45
+ };
46
+ export type StoreKeysController<T extends object = any> = {
47
+ readonly node: StoreNode<T>;
48
+ readonly keys: PropertyKey[];
49
+ readonly stringKeys: string[];
50
+ readonly value: T;
51
+ readonly exists: boolean;
52
+ refresh(): void;
53
+ has(key: PropertyKey): boolean;
54
+ get<K extends keyof T>(key: K): T[K];
55
+ };
56
+ export declare function useStoreKeys<T extends object>(node: StoreNode<T>, options?: Omit<UseStoreNodeOptions<T>, "mode">): StoreKeysController<T>;
57
+ export declare function useStoreSelect<T, M extends StoreMask<T>>(selection: StoreSelection<T, M>, options?: UseStoreSelectOptions<StorePick<T, M>>): StoreSelectionController<T, M>;
58
+ export type RemoteStoreLike<T extends object> = {
59
+ get(mask?: any): T | Promise<T>;
60
+ changed: ListenLike<readonly []>;
61
+ changedPaths?: ListenLike<readonly [StoreChange]>;
62
+ };
63
+ export type UseStoreMirrorOptions<T extends object, M extends StoreMask<T>> = StoreSyncOpts & {
64
+ mask: M;
65
+ auto?: boolean;
66
+ };
67
+ export type StoreMirrorController<T extends object, M extends StoreMask<T>> = {
68
+ readonly store: ObserveAll2.Store<T> & {
69
+ sync(mask: M, subOpts?: StoreSyncOpts): Promise<() => void>;
70
+ };
71
+ readonly value: StorePick<T, M>;
72
+ readonly selection: StoreSelectionController<T, M>;
73
+ readonly ready: boolean;
74
+ readonly syncing: boolean;
75
+ readonly error: unknown;
76
+ sync(mask?: M, opts?: StoreSyncOpts): Promise<() => void>;
77
+ stop(): void;
78
+ };
79
+ export declare function useStoreMirror<T extends object, M extends StoreMask<T>>(remote: RemoteStoreLike<T>, initial: T, options: UseStoreMirrorOptions<T, M>): StoreMirrorController<T, M>;
80
+ export declare function useListenEffect<TArgs extends readonly unknown[]>(listen: ListenLike<TArgs> | null | undefined, cb: (...args: TArgs) => void, opts?: {
81
+ key?: string | symbol;
82
+ current?: boolean | (() => TArgs | undefined);
83
+ }): void;
84
+ export type StoreChangedPathsController = {
85
+ readonly change: StoreChange | undefined;
86
+ readonly paths: PropertyKey[][];
87
+ readonly count: number;
88
+ reset(): void;
89
+ };
90
+ export declare function useStoreChangedPaths(listen: ListenLike<readonly [StoreChange]> | null | undefined, opts?: {
91
+ initial?: StoreChange;
92
+ key?: string | symbol;
93
+ }): StoreChangedPathsController;
94
+ export declare function useListenArgs<TArgs extends readonly unknown[]>(listen: ListenLike<TArgs> | null | undefined, opts?: {
95
+ initial?: TArgs;
96
+ key?: string | symbol;
97
+ current?: boolean | (() => TArgs | undefined);
98
+ }): TArgs | undefined;
99
+ export declare function useListenValue<T, TArgs extends readonly unknown[] = readonly [T]>(listen: ListenLike<TArgs> | null | undefined, opts?: {
100
+ initial?: T;
101
+ key?: string | symbol;
102
+ current?: boolean | (() => TArgs | undefined);
103
+ map?: (...args: TArgs) => T;
104
+ }): T | undefined;
105
+ export type { StoreChange, StoreDrain, StoreMask, StoreNode, StorePick, StoreSelection, StoreSubOpts, StoreSyncOpts };
@@ -0,0 +1,224 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { ObserveAll2 } from "wenay-common2";
3
+ function readNode(node, options) {
4
+ if (!node.has())
5
+ return options?.fallback;
6
+ return (options?.mode == "snapshot" ? node.snapshot() : node.get());
7
+ }
8
+ function isSameStoreMask(a, b) {
9
+ if (a === b)
10
+ return true;
11
+ if (a === true || b === true)
12
+ return false;
13
+ if (!a || !b || typeof a != "object" || typeof b != "object")
14
+ return false;
15
+ const keysA = Reflect.ownKeys(a);
16
+ const keysB = Reflect.ownKeys(b);
17
+ if (keysA.length != keysB.length)
18
+ return false;
19
+ for (const key of keysA) {
20
+ if (!keysB.includes(key))
21
+ return false;
22
+ if (!isSameStoreMask(a[key], b[key]))
23
+ return false;
24
+ }
25
+ return true;
26
+ }
27
+ function useStableStoreMask(mask) {
28
+ const ref = useRef(mask);
29
+ if (!isSameStoreMask(ref.current, mask))
30
+ ref.current = mask;
31
+ return ref.current;
32
+ }
33
+ function useLatestRef(value) {
34
+ const ref = useRef(value);
35
+ ref.current = value;
36
+ return ref;
37
+ }
38
+ export function useStoreNode(node, options = {}) {
39
+ const { current, drain, key, mode = "get", fallback } = options;
40
+ const [version, setVersion] = useState(0);
41
+ const refresh = useCallback(() => setVersion(v => v + 1), []);
42
+ useEffect(() => {
43
+ return node.on(() => refresh(), { current, drain, key });
44
+ }, [node, current, drain, key, refresh]);
45
+ const value = useMemo(() => readNode(node, { mode, fallback }), [node, version, mode, fallback]);
46
+ const exists = useMemo(() => node.has(), [node, version]);
47
+ return useMemo(() => ({
48
+ node,
49
+ value,
50
+ exists,
51
+ path: node.path,
52
+ pathString: node.pathString,
53
+ refresh,
54
+ get: () => node.get(),
55
+ snapshot: () => node.snapshot(),
56
+ has: () => node.has(),
57
+ set: (next) => node.set(next),
58
+ replace: (next) => node.replace(next),
59
+ update: (mask, opts) => node.update(mask, opts),
60
+ }), [node, value, exists, refresh]);
61
+ }
62
+ export function useStoreKeys(node, options = {}) {
63
+ const state = useStoreNode(node, { ...options, mode: "snapshot" });
64
+ const keys = useMemo(() => {
65
+ const value = state.value;
66
+ return value != null && typeof value == "object" ? Reflect.ownKeys(value) : [];
67
+ }, [state.value]);
68
+ return useMemo(() => ({
69
+ node,
70
+ keys,
71
+ stringKeys: keys.map(String),
72
+ value: state.value,
73
+ exists: state.exists,
74
+ refresh: state.refresh,
75
+ has: (key) => state.value != null && typeof state.value == "object" && key in state.value,
76
+ get: (key) => state.value[key],
77
+ }), [node, keys, state]);
78
+ }
79
+ export function useStoreSelect(selection, options = {}) {
80
+ const { current, drain, key, fallback } = options;
81
+ const [version, setVersion] = useState(0);
82
+ const refresh = useCallback(() => setVersion(v => v + 1), []);
83
+ useEffect(() => {
84
+ return selection.on(() => refresh(), { current, drain, key });
85
+ }, [selection, current, drain, key, refresh]);
86
+ const value = useMemo(() => {
87
+ const next = selection.get();
88
+ return next === undefined ? fallback : next;
89
+ }, [selection, version, fallback]);
90
+ return useMemo(() => ({
91
+ selection,
92
+ value,
93
+ mask: selection.mask,
94
+ paths: selection.paths,
95
+ refresh,
96
+ get: () => selection.get(),
97
+ }), [selection, value, refresh]);
98
+ }
99
+ export function useStoreMirror(remote, initial, options) {
100
+ const { mask, auto = true, current = true, drain, key, partial, onError } = options;
101
+ const stableMask = useStableStoreMask(mask);
102
+ const syncOptionsRef = useLatestRef({ current, drain, key, partial, onError });
103
+ const mountedRef = useRef(false);
104
+ const stopRef = useRef(null);
105
+ const [ready, setReady] = useState(false);
106
+ const [syncing, setSyncing] = useState(false);
107
+ const [error, setError] = useState(null);
108
+ const storeRef = useRef(null);
109
+ if (!storeRef.current || storeRef.current.remote !== remote) {
110
+ storeRef.current = {
111
+ remote,
112
+ store: ObserveAll2.createStoreMirror(remote, initial),
113
+ };
114
+ }
115
+ const store = storeRef.current.store;
116
+ const selectionRaw = useMemo(() => store.update(stableMask), [store, stableMask]);
117
+ const selection = useStoreSelect(selectionRaw, { drain });
118
+ useEffect(() => {
119
+ mountedRef.current = true;
120
+ return () => {
121
+ mountedRef.current = false;
122
+ stopRef.current?.();
123
+ stopRef.current = null;
124
+ };
125
+ }, []);
126
+ const stop = useCallback(() => {
127
+ stopRef.current?.();
128
+ stopRef.current = null;
129
+ if (mountedRef.current)
130
+ setReady(false);
131
+ }, []);
132
+ const sync = useCallback(async (nextMask = stableMask, opts) => {
133
+ stopRef.current?.();
134
+ stopRef.current = null;
135
+ if (mountedRef.current) {
136
+ setSyncing(true);
137
+ setError(null);
138
+ }
139
+ try {
140
+ const latest = syncOptionsRef.current;
141
+ const overrideOnError = opts?.onError;
142
+ const off = await store.sync(nextMask, {
143
+ current: latest.current,
144
+ drain: latest.drain,
145
+ key: latest.key,
146
+ partial: latest.partial,
147
+ ...opts,
148
+ onError(error) {
149
+ if (mountedRef.current)
150
+ setError(error);
151
+ (overrideOnError ?? syncOptionsRef.current.onError)?.(error);
152
+ },
153
+ });
154
+ stopRef.current = off;
155
+ if (mountedRef.current)
156
+ setReady(true);
157
+ return off;
158
+ }
159
+ catch (e) {
160
+ if (mountedRef.current)
161
+ setError(e);
162
+ throw e;
163
+ }
164
+ finally {
165
+ if (mountedRef.current)
166
+ setSyncing(false);
167
+ }
168
+ }, [store, stableMask, syncOptionsRef]);
169
+ useEffect(() => {
170
+ if (!auto)
171
+ return;
172
+ sync().catch(() => { });
173
+ return () => {
174
+ stopRef.current?.();
175
+ stopRef.current = null;
176
+ };
177
+ }, [auto, sync, current, drain, key, partial]);
178
+ return useMemo(() => ({
179
+ store,
180
+ value: selection.value,
181
+ selection,
182
+ ready,
183
+ syncing,
184
+ error,
185
+ sync,
186
+ stop,
187
+ }), [store, selection, ready, syncing, error, sync, stop]);
188
+ }
189
+ export function useListenEffect(listen, cb, opts) {
190
+ const cbRef = useRef(cb);
191
+ cbRef.current = cb;
192
+ const key = opts?.key;
193
+ const current = opts?.current;
194
+ useEffect(() => {
195
+ if (!listen)
196
+ return;
197
+ return listen.on((...args) => cbRef.current(...args), { key, current });
198
+ }, [listen, key, current]);
199
+ }
200
+ export function useStoreChangedPaths(listen, opts) {
201
+ const [state, setState] = useState({ change: opts?.initial, count: 0 });
202
+ useListenEffect(listen, (change) => {
203
+ setState(prev => ({ change, count: prev.count + 1 }));
204
+ }, { key: opts?.key });
205
+ return useMemo(() => ({
206
+ change: state.change,
207
+ paths: state.change?.paths ?? [],
208
+ count: state.count,
209
+ reset: () => setState({ change: opts?.initial, count: 0 }),
210
+ }), [state, opts?.initial]);
211
+ }
212
+ export function useListenArgs(listen, opts) {
213
+ const [value, setValue] = useState(opts?.initial);
214
+ useListenEffect(listen, (...args) => setValue(args), { key: opts?.key, current: opts?.current });
215
+ return value;
216
+ }
217
+ export function useListenValue(listen, opts) {
218
+ const [value, setValue] = useState(opts?.initial);
219
+ const map = opts?.map;
220
+ useListenEffect(listen, (...args) => {
221
+ setValue(map ? map(...args) : args[0]);
222
+ }, { key: opts?.key, current: opts?.current });
223
+ return value;
224
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-react2",
3
- "version": "1.0.23",
3
+ "version": "1.0.25",
4
4
  "description": "Common react",
5
5
  "strict": true,
6
6
  "main": "dist/index.js",
@@ -23,7 +23,7 @@
23
23
  "react": "^19.2.0",
24
24
  "react-dom": "^19.2.0",
25
25
  "react-rnd": "^10.5.3",
26
- "wenay-common2": "^1.0.52"
26
+ "wenay-common2": "^1.0.53"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19.2.0",