wenay-react2 1.0.46 → 1.0.48

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 (50) hide show
  1. package/README.md +17 -15
  2. package/doc/EXAMPLE_USAGE.md +969 -969
  3. package/doc/PROJECT_FUNCTIONALITY.md +403 -403
  4. package/doc/PROJECT_RULES.md +27 -27
  5. package/doc/WENAY_REACT2_RENAMES.md +170 -170
  6. package/doc/changes/v1.0.35.md +18 -18
  7. package/doc/changes/v1.0.38.md +37 -37
  8. package/doc/changes/v1.0.39.md +23 -23
  9. package/doc/changes/v1.0.40.md +14 -14
  10. package/doc/changes/v1.0.41.md +35 -35
  11. package/doc/changes/v1.0.42.md +26 -26
  12. package/doc/changes/v1.0.43.md +10 -10
  13. package/doc/changes/v1.0.44.md +14 -14
  14. package/doc/changes/v1.0.45.md +8 -8
  15. package/doc/changes/v1.0.46.md +11 -11
  16. package/doc/changes/v1.0.47.md +9 -0
  17. package/doc/changes/v1.0.48.md +8 -0
  18. package/doc/examples/README.md +4 -4
  19. package/doc/examples/peer-call-media.tsx +7 -7
  20. package/doc/examples/stand.tsx +5 -5
  21. package/doc/native.md +37 -0
  22. package/doc/progress/README.md +13 -13
  23. package/doc/progress/architecture-fix-queue.md +74 -74
  24. package/doc/progress/column-state-present-gate.md +28 -28
  25. package/doc/progress/common2-adoption-1.0.73.md +28 -28
  26. package/doc/progress/common2-adoption-1.0.74.md +24 -24
  27. package/doc/progress/hook-controller-opportunities.md +363 -363
  28. package/doc/progress/hook-extraction-audit.md +195 -195
  29. package/doc/progress/public-surface-normalization.md +351 -351
  30. package/doc/progress/qa-stand-walkthrough-2026-07-12.md +62 -0
  31. package/doc/progress/stand-as-examples-audit.md +20 -20
  32. package/doc/progress/style-system-normalization.md +121 -121
  33. package/doc/target/README.md +32 -32
  34. package/doc/target/my.md +122 -122
  35. package/doc/wenay-react2-rare.md +807 -807
  36. package/doc/wenay-react2.md +541 -541
  37. package/lib/common/demo/peerMedia.js +6 -1
  38. package/lib/common/src/logs/logsController.js +2 -2
  39. package/lib/common/testUseReact/qa.js +3 -3
  40. package/lib/native/columnDots.d.ts +41 -0
  41. package/lib/native/columnDots.js +104 -0
  42. package/lib/native/columnState.d.ts +69 -0
  43. package/lib/native/columnState.js +209 -0
  44. package/lib/native/index.d.ts +3 -0
  45. package/lib/native/index.js +3 -0
  46. package/lib/style/menuRight.css +19 -19
  47. package/lib/style/style.css +23 -23
  48. package/lib/style/tokens.css +184 -184
  49. package/package.json +2 -1
  50. package/doc/wenay-react2-1.0.8.tgz +0 -0
@@ -1,971 +1,971 @@
1
- # wenay-react2 Example Usage Standards
2
-
3
- This document explains how to choose and use the library primitives, and why
4
- those choices matter.
5
-
6
- It is a standards file, not a complete API reference. For raw signatures, read
7
- `doc/wenay-react2.md`. For rare behavior and compatibility notes, read
8
- `doc/wenay-react2-rare.md`.
9
-
10
- ## General Rule
11
-
12
- Prefer the highest-level primitive that owns the lifecycle you need. For reusable behavior, the canonical primitive should usually be a headless `use*` hook or `create*` controller, with visual components layered above it.
13
-
14
- Examples:
15
-
16
- - Use `useAgGrid` / `AgGridTable` before direct `applyGridRows`.
17
- - Use `createColumnState` before a local "visible columns" object.
18
- - Use `createToolbar({source})` before manually bridging toolbar and grid
19
- order.
20
- - Use `contextMenu.openAt(e, items)` before queued/global context menu writes.
21
- - Use `useStoreMirror` or replay hooks before hand-written subscribe effects.
22
- - Use `memoryCache.onDirty` save wiring before direct storage writes inside a
23
- shared primitive.
24
-
25
- The reason is lifecycle ownership. The canonical primitive already knows how to
26
- attach, detach, replay current state, notify React, and preserve user config.
27
-
28
- ## Hook/Controller Layering Standard
29
-
30
- For new reusable functionality, design the hook/controller API first. A visual component should usually be a thin layer over that API, and old component-only entry points should remain as compatibility wrappers when practical.
31
-
32
- Preferred shape:
33
-
34
- - Layer 1: `use*` hook or `create*` controller that owns state, lifecycle, subscriptions, persistence, commands, and callbacks.
35
- - Layer 2: visual `*View` / `*Table` / `*Panel` component that consumes the hook/controller and renders DOM.
36
- - Layer 3: compatibility component with the old name/props, implemented through Layer 1 and Layer 2.
37
-
38
- Do not add a hook just to add a hook. The hook/controller should expose real control: methods, subscription state, refs/controllers, persistence hooks, or an API object that a stand card/app can use without copying JSX.
39
-
40
- ## Compatibility Standard
41
-
42
- New examples should teach the canonical hook/controller API first. Old public APIs should usually remain supported during internal refactors, but an aggressive migration is allowed when it is explicitly planned and recorded in changelog/migration notes. Do not present a removed API as supported.
43
-
44
- When a replacement API is introduced:
45
-
46
- - keep the old import working;
47
- - document the old path as compatibility/low-level if needed;
48
- - add migration notes before changing examples broadly;
49
- - collect local usage statistics for risky surfaces before proposing removal;
50
- - keep default styles available until a replacement class/token contract is documented.
51
-
52
- ## Imports
53
-
54
- Use root imports in consumer code:
55
-
56
- ```ts
57
- import {
58
- AgGridTable,
59
- createColumnState,
60
- createToolbar,
61
- memoryCache,
62
- useAgGrid,
63
- } from "wenay-react2"
64
- ```
65
-
66
- Use deep imports only inside this repository or when working on a local module
67
- that has no root export yet.
68
-
69
- ## Style Standard
70
-
71
- Shared primitives must ship with usable default styling. Prefer CSS classes plus custom properties over inline visual styles. Inline style is acceptable for computed runtime geometry such as drag offsets, dot positions, measured sizes, and z-index decisions; it is not the right place for reusable colors, spacing, borders, shadows, fonts, or hover/active states.
72
-
73
- When changing styles:
74
-
75
- - keep the old default class working or provide a documented replacement;
76
- - add/extend CSS variables before broad visual rewrites;
77
- - write the changelog in terms of what changed visually and how apps override it;
78
- - check the relevant QA card before and after the migration.
79
-
80
- ## App Startup Persistence
81
-
82
- Use when the app wants persisted shared UI preferences.
83
-
84
- ```ts
85
- import { memoryCache } from "wenay-react2"
86
-
87
- memoryCache.load()
88
-
89
- const off = memoryCache.onDirty(() => {
90
- memoryCache.saveDebounced(800)
91
- })
92
- // Call off() during app shutdown or test cleanup when this setup is scoped.
93
-
94
- document.addEventListener("visibilitychange", () => {
95
- if (document.visibilityState == "hidden") void memoryCache.flush()
96
- })
97
-
98
- window.addEventListener("pagehide", () => {
99
- void memoryCache.flush()
100
- })
101
- ```
102
-
103
- Why:
104
-
105
- - `createToolbar`, `createUiSlot`, `createColumnState`, floating windows, and
106
- resize maps can mark their data dirty.
107
- - The library must not decide when the product writes storage.
108
- - The app can choose debounce, final flush, or explicit save behavior.
109
-
110
- Standard:
111
-
112
- - Call `memoryCache.load()` before mounting UI that reads persisted config.
113
- - Do not put `localStorage.setItem` inside a shared primitive.
114
- - Use `memoryUpdate` or `memoryMarkDirty` when mutating stored objects in place
115
- from app code.
116
-
117
- ## Declarative Table
118
-
119
- Use when React owns the full row list.
120
-
121
- ```tsx
122
- import { AgGridTable } from "wenay-react2"
123
- import type { ColDef } from "ag-grid-community"
124
-
125
- type Row = { id: string; name: string; price: number }
126
-
127
- const columns: ColDef<Row>[] = [
128
- { field: "name" },
129
- { field: "price" },
130
- ]
131
-
132
- export function PricesTable({ rows }: { rows: Row[] }) {
133
- return (
134
- <AgGridTable<Row>
135
- rowData={rows}
136
- columnDefs={columns}
137
- getRowId={p => p.data.id}
138
- />
139
- )
140
- }
141
- ```
142
-
143
- Why:
144
-
145
- - React already has the complete row array.
146
- - There is no patch stream or transaction buffer to manage.
147
- - `AgGridTable` keeps the shared theme/wrapper path while preserving normal
148
- ag-grid props.
149
-
150
- Standard:
151
-
152
- - Provide `getRowId` when selection, row state, streaming overlays, or stable
153
- identity matters.
154
- - Do not create a buffer just to render a static list.
155
-
156
- ## Streaming Or Patch-Updating Table
157
-
158
- Use when rows arrive as add/update/remove patches.
159
-
160
- ```tsx
161
- import { AgGridTable, useAgGrid } from "wenay-react2"
162
- import type { ColDef } from "ag-grid-community"
163
- import { useEffect } from "react"
164
-
165
- type Row = { id: string; name: string; price: number }
166
-
167
- const columns: ColDef<Row>[] = [
168
- { field: "name" },
169
- { field: "price" },
170
- ]
171
-
172
- export function StreamTable({ subscribe }: {
173
- subscribe: (cb: (rows: Row[]) => void) => () => void
174
- }) {
175
- const grid = useAgGrid<Row>({ getId: row => row.id })
176
-
177
- useEffect(() => {
178
- return subscribe(rows => {
179
- grid.update({ newData: rows })
180
- })
181
- }, [grid, subscribe])
182
-
183
- return <AgGridTable<Row> controller={grid} columnDefs={columns} />
184
- }
185
- ```
186
-
187
- Why:
188
-
189
- - The controller owns grid readiness.
190
- - Patches received before the grid is ready are kept in the buffer.
191
- - Route remounts can resync from the buffer.
192
- - Remove/update semantics are centralized.
193
-
194
- Standard:
195
-
196
- - Prefer `grid.update`, `grid.remove`, `grid.clean`, `grid.sync`, `grid.fit`.
197
- - Keep a stable `getId`.
198
- - Use a module-level `createGridBuffer` when the buffer must survive component
199
- unmounts or be shared by several views.
200
- - Treat direct `applyGridRows` as a low-level helper or legacy migration path,
201
- not the first example for new code.
202
-
203
- ## Overlay Stream Over React-Owned Rows
204
-
205
- Use when React owns which rows exist, but a stream updates values on those rows.
206
-
207
- ```tsx
208
- import { AgGridTable, createGridBuffer, useAgGrid } from "wenay-react2"
209
- import { useEffect, useState } from "react"
210
-
211
- type Row = { id: string; name: string; streamPrice?: number }
212
-
213
- export function OverlayTable({ rows, subscribe }: {
214
- rows: Row[]
215
- subscribe: (cb: (rows: Partial<Row>[]) => void) => () => void
216
- }) {
217
- const [core] = useState(() => createGridBuffer<Row>({
218
- getId: row => row.id,
219
- mode: "overlay",
220
- pushDefaults: { add: false },
221
- }))
222
- const grid = useAgGrid({ core })
223
-
224
- useEffect(() => core.api.sync(), [core, rows])
225
-
226
- useEffect(() => {
227
- return subscribe(patches => {
228
- core.api.updateData({ newData: patches as Row[] })
229
- })
230
- }, [core, subscribe])
231
-
232
- return (
233
- <AgGridTable<Row>
234
- controller={grid}
235
- rowData={rows}
236
- columnDefs={[
237
- { field: "name" },
238
- { field: "streamPrice" },
239
- ]}
240
- getRowId={p => p.data.id}
241
- />
242
- )
243
- }
244
- ```
245
-
246
- Why:
247
-
248
- - React decides row membership.
249
- - Stream-only rows should not appear by accident.
250
- - Buffered patches can be applied when a matching row appears later.
251
-
252
- Standard:
253
-
254
- - Use `mode: "overlay"` and usually `pushDefaults: { add: false }`.
255
- - Call `sync()` when the declarative row set changes.
256
- - Do not mix overlay ownership with independent transaction add/remove logic
257
- unless the behavior is deliberately specified.
258
-
259
- ## Dynamic Columns
260
-
261
- Use when a table has a stable base schema plus a variable set of dynamic
262
- column names.
263
-
264
- ```tsx
265
- import { AgGridTable, createColumnBuffer } from "wenay-react2"
266
- import type { ColDef, ColGroupDef } from "ag-grid-community"
267
- import { useState } from "react"
268
-
269
- type Row = { id: string; symbol: string; [key: string]: string | number }
270
- type AnyCol = ColDef<Row> | ColGroupDef<Row>
271
-
272
- const baseColumns: AnyCol[] = [
273
- { field: "symbol" },
274
- { headerName: "Metrics", groupId: "metrics", children: [] },
275
- ]
276
-
277
- function buildColumn(name: string): ColDef<Row> {
278
- return {
279
- colId: `metric:${name}`,
280
- field: name,
281
- headerName: name,
282
- }
283
- }
284
-
285
- function buildColumns(names: readonly string[]): AnyCol[] {
286
- return baseColumns.map(col =>
287
- "children" in col && col.groupId == "metrics"
288
- ? { ...col, children: names.map(buildColumn) }
289
- : col,
290
- )
291
- }
292
-
293
- export function DynamicGrid({ rows }: { rows: Row[] }) {
294
- const [columns] = useState(() => createColumnBuffer<Row>())
295
-
296
- return (
297
- <AgGridTable<Row>
298
- rowData={rows}
299
- getRowId={p => p.data.id}
300
- columnDefs={baseColumns}
301
- onGridReady={event => columns.control.attach(event.api, {
302
- apply: ({ api, names }) => {
303
- api.setGridOption("columnDefs", buildColumns(names))
304
- },
305
- })}
306
- onGridPreDestroyed={() => columns.control.detach()}
307
- />
308
- )
309
- }
310
- ```
311
-
312
- Why:
313
-
314
- - The shared library stores and replays the dynamic name set.
315
- - The app owns group selection, column IDs, headers, value formatters, and
316
- domain naming.
317
-
318
- Standard:
319
-
320
- - `createColumnBuffer` should not know product group IDs.
321
- - Build stable `colId` values in the app wrapper.
322
- - Use `columns.api.setNames(nextNames)` when the set changes.
323
- - Use `columns.api.apply()` when wrapper policy changes but names do not.
324
-
325
- ## Column State For Grid, Menu, Toolbar, And Mobile Cards
326
-
327
- Use when one column config should drive multiple surfaces.
328
-
329
- ```tsx
330
- import {
331
- AgGridTable,
332
- CardList,
333
- ColumnDots,
334
- ColumnsMenu,
335
- createColumnState,
336
- createToolbar,
337
- } from "wenay-react2"
338
- import type { ColDef } from "ag-grid-community"
339
-
340
- type Row = { id: string; name: string; price: number; qty: number }
341
-
342
- const state = createColumnState({
343
- key: "orders.columns",
344
- columns: [
345
- { key: "name", title: "Name", fixed: true, cardRole: "title" },
346
- { key: "price", title: "Price", short: "px" },
347
- { key: "qty", title: "Quantity", short: "qty" },
348
- ],
349
- })
350
-
351
- const columnsToolbar = createToolbar({
352
- key: "orders.columns.toolbar",
353
- items: state.columns.map(col => ({
354
- key: col.key,
355
- title: col.title,
356
- short: col.short,
357
- })),
358
- source: state.api.listSource,
359
- })
360
- const ColumnsToolbarBar = columnsToolbar.Bar
361
-
362
- const columnDefs: ColDef<Row>[] = [
363
- { colId: "name", field: "name" },
364
- { colId: "price", field: "price" },
365
- { colId: "qty", field: "qty" },
366
- ]
367
-
368
- export function OrdersGrid({ rows, compact }: { rows: Row[]; compact?: boolean }) {
369
- if (compact) {
370
- return (
371
- <>
372
- <ColumnDots state={state} />
373
- <CardList<Row> state={state} data={rows} getId={row => row.id} />
374
- </>
375
- )
376
- }
377
-
378
- return (
379
- <>
380
- <ColumnsToolbarBar settings />
381
- <ColumnsMenu state={state} compact />
382
- <AgGridTable<Row>
383
- rowData={rows}
384
- columnDefs={columnDefs}
385
- getRowId={p => p.data.id}
386
- autoSizeColumns={false}
387
- onGridReady={e => state.grid.attach(e.api)}
388
- onGridPreDestroyed={() => state.grid.detach()}
389
- />
390
- </>
391
- )
392
- }
393
- ```
394
-
395
- Why:
396
-
397
- - Grid drag, menu toggle, toolbar settings, and mobile dots edit one config.
398
- - User preferences survive remounts.
399
- - Mobile cards do not need ag-grid.
400
-
401
- Standard:
402
-
403
- - Keep `createColumnState` at module/app-wrapper level when config should
404
- survive component remounts.
405
- - Attach the ag-grid adapter in `onGridReady` and detach in
406
- `onGridPreDestroyed`.
407
- - Pass `autoSizeColumns={false}` when restoring widths, otherwise auto-fit can
408
- overwrite persisted width state.
409
- - Use `state.api.listSource` when a toolbar should mirror column order and
410
- visibility.
411
- - Use `sourceMode: "order"` when toolbar membership and extra toolbar-only
412
- buttons must stay local while column order comes from `columnState`.
413
-
414
- ## Default Column Grid Wrapper
415
-
416
- Use when a normal grid should get the standard column menu, toolbar settings,
417
- and mobile card/table representation without hand-wiring every surface.
418
-
419
- ```tsx
420
- import { createColumnGrid } from "wenay-react2"
421
- import type { ColDef } from "ag-grid-community"
422
-
423
- type Row = { id: string; name: string; price: number; qty: number; note: string }
424
-
425
- const columnDefs: ColDef<Row>[] = [
426
- { colId: "name", field: "name" },
427
- { colId: "price", field: "price" },
428
- { colId: "qty", field: "qty", headerName: "Qty" },
429
- { colId: "note", field: "note" },
430
- ]
431
-
432
- const ordersGrid = createColumnGrid<Row>({
433
- key: "orders.columns",
434
- columnDefs,
435
- getId: row => row.id,
436
- autoSizeOnColumnCountChange: true,
437
- columns: [
438
- { key: "name", fixed: true, cardRole: "title" },
439
- { key: "note", defaultVisible: false },
440
- ],
441
- })
442
-
443
- export function OrdersView({ rows, mobile }: { rows: Row[]; mobile?: boolean }) {
444
- return (
445
- <ordersGrid.View
446
- mode={mobile ? "cards" : "table"}
447
- data={rows}
448
- table={{ getRowId: p => p.data.id }}
449
- />
450
- )
451
- }
452
- ```
453
-
454
- Standard:
455
-
456
- - `createColumnGrid` infers column metadata from `colId`, `field`, and
457
- `headerName`; pass `columns` only for overrides like `fixed`, `icon`,
458
- `defaultVisible`, and `cardRole`.
459
- - The returned `Table` and `View` attach/detach `columnState` automatically and
460
- default `autoSizeColumns` to `false` so restored widths survive remounts.
461
- - `View` defaults to the dots overlay (`controls="auto"`) for both table and cards;
462
- pass `controls="menu"`, `controls="toolbar"`, or `controls={false}` only when needed.
463
- - `Dots` can drive a table too; it is a column selector over the shared config,
464
- not a card-only control, and the wrapper defaults its max to all columns.
465
- - `autoSizeOnColumnCountChange` is optional and separate from `autoSizeColumns`;
466
- it fits once when the number of visible columns changes.
467
- - Drop to raw `createColumnState` when a product needs custom grouping, runtime
468
- presence gates, or a fully custom toolbar skin.
469
-
470
- ## Toolbar With Local Commands
471
-
472
- Use when a command strip has its own commands and user-configurable layout.
473
-
474
- ```tsx
475
- import { createToolbar } from "wenay-react2"
476
-
477
- const ordersToolbar = createToolbar({
478
- key: "orders.toolbar",
479
- items: [
480
- { key: "refresh", title: "Refresh", short: "ref", onClick: refresh },
481
- { key: "export", title: "Export", short: "csv", onClick: exportCsv },
482
- { key: "settings", title: "Settings", fixed: true, onClick: openSettings },
483
- ],
484
- resetItem: { title: "Reset" },
485
- })
486
-
487
- export function OrdersToolbar() {
1
+ # wenay-react2 Example Usage Standards
2
+
3
+ This document explains how to choose and use the library primitives, and why
4
+ those choices matter.
5
+
6
+ It is a standards file, not a complete API reference. For raw signatures, read
7
+ `doc/wenay-react2.md`. For rare behavior and compatibility notes, read
8
+ `doc/wenay-react2-rare.md`.
9
+
10
+ ## General Rule
11
+
12
+ Prefer the highest-level primitive that owns the lifecycle you need. For reusable behavior, the canonical primitive should usually be a headless `use*` hook or `create*` controller, with visual components layered above it.
13
+
14
+ Examples:
15
+
16
+ - Use `useAgGrid` / `AgGridTable` before direct `applyGridRows`.
17
+ - Use `createColumnState` before a local "visible columns" object.
18
+ - Use `createToolbar({source})` before manually bridging toolbar and grid
19
+ order.
20
+ - Use `contextMenu.openAt(e, items)` before queued/global context menu writes.
21
+ - Use `useStoreMirror` or replay hooks before hand-written subscribe effects.
22
+ - Use `memoryCache.onDirty` save wiring before direct storage writes inside a
23
+ shared primitive.
24
+
25
+ The reason is lifecycle ownership. The canonical primitive already knows how to
26
+ attach, detach, replay current state, notify React, and preserve user config.
27
+
28
+ ## Hook/Controller Layering Standard
29
+
30
+ For new reusable functionality, design the hook/controller API first. A visual component should usually be a thin layer over that API, and old component-only entry points should remain as compatibility wrappers when practical.
31
+
32
+ Preferred shape:
33
+
34
+ - Layer 1: `use*` hook or `create*` controller that owns state, lifecycle, subscriptions, persistence, commands, and callbacks.
35
+ - Layer 2: visual `*View` / `*Table` / `*Panel` component that consumes the hook/controller and renders DOM.
36
+ - Layer 3: compatibility component with the old name/props, implemented through Layer 1 and Layer 2.
37
+
38
+ Do not add a hook just to add a hook. The hook/controller should expose real control: methods, subscription state, refs/controllers, persistence hooks, or an API object that a stand card/app can use without copying JSX.
39
+
40
+ ## Compatibility Standard
41
+
42
+ New examples should teach the canonical hook/controller API first. Old public APIs should usually remain supported during internal refactors, but an aggressive migration is allowed when it is explicitly planned and recorded in changelog/migration notes. Do not present a removed API as supported.
43
+
44
+ When a replacement API is introduced:
45
+
46
+ - keep the old import working;
47
+ - document the old path as compatibility/low-level if needed;
48
+ - add migration notes before changing examples broadly;
49
+ - collect local usage statistics for risky surfaces before proposing removal;
50
+ - keep default styles available until a replacement class/token contract is documented.
51
+
52
+ ## Imports
53
+
54
+ Use root imports in consumer code:
55
+
56
+ ```ts
57
+ import {
58
+ AgGridTable,
59
+ createColumnState,
60
+ createToolbar,
61
+ memoryCache,
62
+ useAgGrid,
63
+ } from "wenay-react2"
64
+ ```
65
+
66
+ Use deep imports only inside this repository or when working on a local module
67
+ that has no root export yet.
68
+
69
+ ## Style Standard
70
+
71
+ Shared primitives must ship with usable default styling. Prefer CSS classes plus custom properties over inline visual styles. Inline style is acceptable for computed runtime geometry such as drag offsets, dot positions, measured sizes, and z-index decisions; it is not the right place for reusable colors, spacing, borders, shadows, fonts, or hover/active states.
72
+
73
+ When changing styles:
74
+
75
+ - keep the old default class working or provide a documented replacement;
76
+ - add/extend CSS variables before broad visual rewrites;
77
+ - write the changelog in terms of what changed visually and how apps override it;
78
+ - check the relevant QA card before and after the migration.
79
+
80
+ ## App Startup Persistence
81
+
82
+ Use when the app wants persisted shared UI preferences.
83
+
84
+ ```ts
85
+ import { memoryCache } from "wenay-react2"
86
+
87
+ memoryCache.load()
88
+
89
+ const off = memoryCache.onDirty(() => {
90
+ memoryCache.saveDebounced(800)
91
+ })
92
+ // Call off() during app shutdown or test cleanup when this setup is scoped.
93
+
94
+ document.addEventListener("visibilitychange", () => {
95
+ if (document.visibilityState == "hidden") void memoryCache.flush()
96
+ })
97
+
98
+ window.addEventListener("pagehide", () => {
99
+ void memoryCache.flush()
100
+ })
101
+ ```
102
+
103
+ Why:
104
+
105
+ - `createToolbar`, `createUiSlot`, `createColumnState`, floating windows, and
106
+ resize maps can mark their data dirty.
107
+ - The library must not decide when the product writes storage.
108
+ - The app can choose debounce, final flush, or explicit save behavior.
109
+
110
+ Standard:
111
+
112
+ - Call `memoryCache.load()` before mounting UI that reads persisted config.
113
+ - Do not put `localStorage.setItem` inside a shared primitive.
114
+ - Use `memoryUpdate` or `memoryMarkDirty` when mutating stored objects in place
115
+ from app code.
116
+
117
+ ## Declarative Table
118
+
119
+ Use when React owns the full row list.
120
+
121
+ ```tsx
122
+ import { AgGridTable } from "wenay-react2"
123
+ import type { ColDef } from "ag-grid-community"
124
+
125
+ type Row = { id: string; name: string; price: number }
126
+
127
+ const columns: ColDef<Row>[] = [
128
+ { field: "name" },
129
+ { field: "price" },
130
+ ]
131
+
132
+ export function PricesTable({ rows }: { rows: Row[] }) {
133
+ return (
134
+ <AgGridTable<Row>
135
+ rowData={rows}
136
+ columnDefs={columns}
137
+ getRowId={p => p.data.id}
138
+ />
139
+ )
140
+ }
141
+ ```
142
+
143
+ Why:
144
+
145
+ - React already has the complete row array.
146
+ - There is no patch stream or transaction buffer to manage.
147
+ - `AgGridTable` keeps the shared theme/wrapper path while preserving normal
148
+ ag-grid props.
149
+
150
+ Standard:
151
+
152
+ - Provide `getRowId` when selection, row state, streaming overlays, or stable
153
+ identity matters.
154
+ - Do not create a buffer just to render a static list.
155
+
156
+ ## Streaming Or Patch-Updating Table
157
+
158
+ Use when rows arrive as add/update/remove patches.
159
+
160
+ ```tsx
161
+ import { AgGridTable, useAgGrid } from "wenay-react2"
162
+ import type { ColDef } from "ag-grid-community"
163
+ import { useEffect } from "react"
164
+
165
+ type Row = { id: string; name: string; price: number }
166
+
167
+ const columns: ColDef<Row>[] = [
168
+ { field: "name" },
169
+ { field: "price" },
170
+ ]
171
+
172
+ export function StreamTable({ subscribe }: {
173
+ subscribe: (cb: (rows: Row[]) => void) => () => void
174
+ }) {
175
+ const grid = useAgGrid<Row>({ getId: row => row.id })
176
+
177
+ useEffect(() => {
178
+ return subscribe(rows => {
179
+ grid.update({ newData: rows })
180
+ })
181
+ }, [grid, subscribe])
182
+
183
+ return <AgGridTable<Row> controller={grid} columnDefs={columns} />
184
+ }
185
+ ```
186
+
187
+ Why:
188
+
189
+ - The controller owns grid readiness.
190
+ - Patches received before the grid is ready are kept in the buffer.
191
+ - Route remounts can resync from the buffer.
192
+ - Remove/update semantics are centralized.
193
+
194
+ Standard:
195
+
196
+ - Prefer `grid.update`, `grid.remove`, `grid.clean`, `grid.sync`, `grid.fit`.
197
+ - Keep a stable `getId`.
198
+ - Use a module-level `createGridBuffer` when the buffer must survive component
199
+ unmounts or be shared by several views.
200
+ - Treat direct `applyGridRows` as a low-level helper or legacy migration path,
201
+ not the first example for new code.
202
+
203
+ ## Overlay Stream Over React-Owned Rows
204
+
205
+ Use when React owns which rows exist, but a stream updates values on those rows.
206
+
207
+ ```tsx
208
+ import { AgGridTable, createGridBuffer, useAgGrid } from "wenay-react2"
209
+ import { useEffect, useState } from "react"
210
+
211
+ type Row = { id: string; name: string; streamPrice?: number }
212
+
213
+ export function OverlayTable({ rows, subscribe }: {
214
+ rows: Row[]
215
+ subscribe: (cb: (rows: Partial<Row>[]) => void) => () => void
216
+ }) {
217
+ const [core] = useState(() => createGridBuffer<Row>({
218
+ getId: row => row.id,
219
+ mode: "overlay",
220
+ pushDefaults: { add: false },
221
+ }))
222
+ const grid = useAgGrid({ core })
223
+
224
+ useEffect(() => core.api.sync(), [core, rows])
225
+
226
+ useEffect(() => {
227
+ return subscribe(patches => {
228
+ core.api.updateData({ newData: patches as Row[] })
229
+ })
230
+ }, [core, subscribe])
231
+
232
+ return (
233
+ <AgGridTable<Row>
234
+ controller={grid}
235
+ rowData={rows}
236
+ columnDefs={[
237
+ { field: "name" },
238
+ { field: "streamPrice" },
239
+ ]}
240
+ getRowId={p => p.data.id}
241
+ />
242
+ )
243
+ }
244
+ ```
245
+
246
+ Why:
247
+
248
+ - React decides row membership.
249
+ - Stream-only rows should not appear by accident.
250
+ - Buffered patches can be applied when a matching row appears later.
251
+
252
+ Standard:
253
+
254
+ - Use `mode: "overlay"` and usually `pushDefaults: { add: false }`.
255
+ - Call `sync()` when the declarative row set changes.
256
+ - Do not mix overlay ownership with independent transaction add/remove logic
257
+ unless the behavior is deliberately specified.
258
+
259
+ ## Dynamic Columns
260
+
261
+ Use when a table has a stable base schema plus a variable set of dynamic
262
+ column names.
263
+
264
+ ```tsx
265
+ import { AgGridTable, createColumnBuffer } from "wenay-react2"
266
+ import type { ColDef, ColGroupDef } from "ag-grid-community"
267
+ import { useState } from "react"
268
+
269
+ type Row = { id: string; symbol: string; [key: string]: string | number }
270
+ type AnyCol = ColDef<Row> | ColGroupDef<Row>
271
+
272
+ const baseColumns: AnyCol[] = [
273
+ { field: "symbol" },
274
+ { headerName: "Metrics", groupId: "metrics", children: [] },
275
+ ]
276
+
277
+ function buildColumn(name: string): ColDef<Row> {
278
+ return {
279
+ colId: `metric:${name}`,
280
+ field: name,
281
+ headerName: name,
282
+ }
283
+ }
284
+
285
+ function buildColumns(names: readonly string[]): AnyCol[] {
286
+ return baseColumns.map(col =>
287
+ "children" in col && col.groupId == "metrics"
288
+ ? { ...col, children: names.map(buildColumn) }
289
+ : col,
290
+ )
291
+ }
292
+
293
+ export function DynamicGrid({ rows }: { rows: Row[] }) {
294
+ const [columns] = useState(() => createColumnBuffer<Row>())
295
+
296
+ return (
297
+ <AgGridTable<Row>
298
+ rowData={rows}
299
+ getRowId={p => p.data.id}
300
+ columnDefs={baseColumns}
301
+ onGridReady={event => columns.control.attach(event.api, {
302
+ apply: ({ api, names }) => {
303
+ api.setGridOption("columnDefs", buildColumns(names))
304
+ },
305
+ })}
306
+ onGridPreDestroyed={() => columns.control.detach()}
307
+ />
308
+ )
309
+ }
310
+ ```
311
+
312
+ Why:
313
+
314
+ - The shared library stores and replays the dynamic name set.
315
+ - The app owns group selection, column IDs, headers, value formatters, and
316
+ domain naming.
317
+
318
+ Standard:
319
+
320
+ - `createColumnBuffer` should not know product group IDs.
321
+ - Build stable `colId` values in the app wrapper.
322
+ - Use `columns.api.setNames(nextNames)` when the set changes.
323
+ - Use `columns.api.apply()` when wrapper policy changes but names do not.
324
+
325
+ ## Column State For Grid, Menu, Toolbar, And Mobile Cards
326
+
327
+ Use when one column config should drive multiple surfaces.
328
+
329
+ ```tsx
330
+ import {
331
+ AgGridTable,
332
+ CardList,
333
+ ColumnDots,
334
+ ColumnsMenu,
335
+ createColumnState,
336
+ createToolbar,
337
+ } from "wenay-react2"
338
+ import type { ColDef } from "ag-grid-community"
339
+
340
+ type Row = { id: string; name: string; price: number; qty: number }
341
+
342
+ const state = createColumnState({
343
+ key: "orders.columns",
344
+ columns: [
345
+ { key: "name", title: "Name", fixed: true, cardRole: "title" },
346
+ { key: "price", title: "Price", short: "px" },
347
+ { key: "qty", title: "Quantity", short: "qty" },
348
+ ],
349
+ })
350
+
351
+ const columnsToolbar = createToolbar({
352
+ key: "orders.columns.toolbar",
353
+ items: state.columns.map(col => ({
354
+ key: col.key,
355
+ title: col.title,
356
+ short: col.short,
357
+ })),
358
+ source: state.api.listSource,
359
+ })
360
+ const ColumnsToolbarBar = columnsToolbar.Bar
361
+
362
+ const columnDefs: ColDef<Row>[] = [
363
+ { colId: "name", field: "name" },
364
+ { colId: "price", field: "price" },
365
+ { colId: "qty", field: "qty" },
366
+ ]
367
+
368
+ export function OrdersGrid({ rows, compact }: { rows: Row[]; compact?: boolean }) {
369
+ if (compact) {
370
+ return (
371
+ <>
372
+ <ColumnDots state={state} />
373
+ <CardList<Row> state={state} data={rows} getId={row => row.id} />
374
+ </>
375
+ )
376
+ }
377
+
378
+ return (
379
+ <>
380
+ <ColumnsToolbarBar settings />
381
+ <ColumnsMenu state={state} compact />
382
+ <AgGridTable<Row>
383
+ rowData={rows}
384
+ columnDefs={columnDefs}
385
+ getRowId={p => p.data.id}
386
+ autoSizeColumns={false}
387
+ onGridReady={e => state.grid.attach(e.api)}
388
+ onGridPreDestroyed={() => state.grid.detach()}
389
+ />
390
+ </>
391
+ )
392
+ }
393
+ ```
394
+
395
+ Why:
396
+
397
+ - Grid drag, menu toggle, toolbar settings, and mobile dots edit one config.
398
+ - User preferences survive remounts.
399
+ - Mobile cards do not need ag-grid.
400
+
401
+ Standard:
402
+
403
+ - Keep `createColumnState` at module/app-wrapper level when config should
404
+ survive component remounts.
405
+ - Attach the ag-grid adapter in `onGridReady` and detach in
406
+ `onGridPreDestroyed`.
407
+ - Pass `autoSizeColumns={false}` when restoring widths, otherwise auto-fit can
408
+ overwrite persisted width state.
409
+ - Use `state.api.listSource` when a toolbar should mirror column order and
410
+ visibility.
411
+ - Use `sourceMode: "order"` when toolbar membership and extra toolbar-only
412
+ buttons must stay local while column order comes from `columnState`.
413
+
414
+ ## Default Column Grid Wrapper
415
+
416
+ Use when a normal grid should get the standard column menu, toolbar settings,
417
+ and mobile card/table representation without hand-wiring every surface.
418
+
419
+ ```tsx
420
+ import { createColumnGrid } from "wenay-react2"
421
+ import type { ColDef } from "ag-grid-community"
422
+
423
+ type Row = { id: string; name: string; price: number; qty: number; note: string }
424
+
425
+ const columnDefs: ColDef<Row>[] = [
426
+ { colId: "name", field: "name" },
427
+ { colId: "price", field: "price" },
428
+ { colId: "qty", field: "qty", headerName: "Qty" },
429
+ { colId: "note", field: "note" },
430
+ ]
431
+
432
+ const ordersGrid = createColumnGrid<Row>({
433
+ key: "orders.columns",
434
+ columnDefs,
435
+ getId: row => row.id,
436
+ autoSizeOnColumnCountChange: true,
437
+ columns: [
438
+ { key: "name", fixed: true, cardRole: "title" },
439
+ { key: "note", defaultVisible: false },
440
+ ],
441
+ })
442
+
443
+ export function OrdersView({ rows, mobile }: { rows: Row[]; mobile?: boolean }) {
444
+ return (
445
+ <ordersGrid.View
446
+ mode={mobile ? "cards" : "table"}
447
+ data={rows}
448
+ table={{ getRowId: p => p.data.id }}
449
+ />
450
+ )
451
+ }
452
+ ```
453
+
454
+ Standard:
455
+
456
+ - `createColumnGrid` infers column metadata from `colId`, `field`, and
457
+ `headerName`; pass `columns` only for overrides like `fixed`, `icon`,
458
+ `defaultVisible`, and `cardRole`.
459
+ - The returned `Table` and `View` attach/detach `columnState` automatically and
460
+ default `autoSizeColumns` to `false` so restored widths survive remounts.
461
+ - `View` defaults to the dots overlay (`controls="auto"`) for both table and cards;
462
+ pass `controls="menu"`, `controls="toolbar"`, or `controls={false}` only when needed.
463
+ - `Dots` can drive a table too; it is a column selector over the shared config,
464
+ not a card-only control, and the wrapper defaults its max to all columns.
465
+ - `autoSizeOnColumnCountChange` is optional and separate from `autoSizeColumns`;
466
+ it fits once when the number of visible columns changes.
467
+ - Drop to raw `createColumnState` when a product needs custom grouping, runtime
468
+ presence gates, or a fully custom toolbar skin.
469
+
470
+ ## Toolbar With Local Commands
471
+
472
+ Use when a command strip has its own commands and user-configurable layout.
473
+
474
+ ```tsx
475
+ import { createToolbar } from "wenay-react2"
476
+
477
+ const ordersToolbar = createToolbar({
478
+ key: "orders.toolbar",
479
+ items: [
480
+ { key: "refresh", title: "Refresh", short: "ref", onClick: refresh },
481
+ { key: "export", title: "Export", short: "csv", onClick: exportCsv },
482
+ { key: "settings", title: "Settings", fixed: true, onClick: openSettings },
483
+ ],
484
+ resetItem: { title: "Reset" },
485
+ })
486
+
487
+ export function OrdersToolbar() {
488
488
  const OrdersToolbarBar = ordersToolbar.Bar
489
- return <OrdersToolbarBar settings reset />
490
- }
491
- ```
492
-
493
- Why:
494
-
495
- - Users can choose order, visibility, and density.
496
- - The same `toolbar.Settings` can be rendered in an inline popover, global
497
- settings dialog, or a custom floating window.
498
-
499
- Standard:
500
-
501
- - Toolbar items should be commands or view controls, not product data rows.
502
- - Use `fixed` for commands that must not disappear.
503
- - Prefer `item.render(density)` only when the default icon/label renderer is
504
- insufficient.
505
-
506
- ## Settings Dialog
507
-
508
- Use when app settings need a searchable tree.
509
-
510
- ```tsx
511
- import { SettingsDialog, registerSettingsSection, useSettingsDialogController } from "wenay-react2"
512
- import { useEffect } from "react"
513
-
514
- const ColumnsSettings = columnsToolbar.Settings
515
-
516
- export function OrdersSettingsRegistration() {
517
- useEffect(() => {
518
- return registerSettingsSection({
519
- key: "orders.columns",
520
- name: "Columns",
521
- parentKey: "orders",
522
- keywords: ["table", "grid", "visibility"],
523
- render: () => <ColumnsSettings />,
524
- })
525
- }, [])
526
-
527
- return null
528
- }
529
-
530
- export function SettingsButton() {
531
- return (
532
- <SettingsDialog
533
- trigger={<button>Settings</button>}
534
- sections={[
535
- { key: "orders", name: "Orders", render: () => null },
536
- ]}
537
- defaultSection="orders.columns"
538
- />
539
- )
540
- }
541
-
542
- export function CustomSettingsTrigger() {
543
- const settings = useSettingsDialogController({
544
- sections: [{ key: "orders", name: "Orders", render: () => null }],
545
- defaultSection: "orders",
546
- })
547
-
548
- return <button onClick={settings.openDialog}>{settings.open ? "Open" : "Settings"}</button>
549
- }
550
- ```
551
-
552
- Why:
553
-
554
- - Sections can be registered by feature modules.
555
- - Search and tree behavior stays consistent across apps.
556
- - `useSettingsDialogController` exposes the same open/search/tree/history/resize actions for custom settings chrome.
557
-
558
- Standard:
559
-
560
- - Use stable `key` values.
561
- - Put searchable synonyms in `keywords` or `searchText`.
562
- - Do not put settings registry state in product globals when the shared
563
- registry already fits.
564
- - Do not rewrite tree/search/divider UX just to customize the trigger; use the controller actions.
565
-
566
- ## Params Editor
567
-
568
- Use `ParamsEditor` for the normal generated editor. Use `useParamsEditorController` only when the app needs a custom renderer over the same draft/debounce/expand contract.
569
-
570
- ```tsx
571
- import { ParamsEditor, useParamsEditorController } from "wenay-react2"
572
-
573
- export function DefaultParams({params, save}: {params: any; save: (next: any) => void}) {
574
- return <ParamsEditor params={params} onChange={save} onExpand={save} />
575
- }
576
-
577
- export function CustomParamsHeader({params, save}: {params: any; save: (next: any) => void}) {
578
- const editor = useParamsEditorController({params, onChange: save})
579
-
580
- return (
581
- <button onClick={() => editor.notifyChange()}>
582
- Save draft
583
- </button>
584
- )
585
- }
586
- ```
587
-
588
- Why:
589
-
590
- - `ParamsEditor` keeps the existing generated rows and input behavior.
591
- - The controller owns the mutable draft clone, immediate/delayed notify, expand callback, and timer cleanup.
592
- - Product-specific validation and save policy stay outside the shared editor.
593
-
594
- Standard:
595
-
596
- - Do not rewrite row/input rendering just to use the controller.
597
- - Keep `ParamsEdit` and `ParamsArrayEdit` as compatibility wrappers unless the async load/save policy is being redesigned explicitly.
598
- ## Context Menu
599
-
600
- Use for right-click actions.
601
-
602
- ```tsx
603
- import { contextMenu } from "wenay-react2"
604
-
605
- export function RowSurface() {
606
- return (
607
- <contextMenu.Layer>
608
- <div
609
- onContextMenu={event => contextMenu.openAt(event, [
610
- { name: "Copy", actionKey: "row.copy", onClick: copy },
611
- { name: "Delete", actionKey: "row.delete", onClick: removeSelected },
612
- { name: "More", actionKey: "row.more", next: () => [
613
- { name: "Open details", actionKey: "row.openDetails", onClick: openDetails },
614
- ] },
615
- ])}
616
- />
617
- </contextMenu.Layer>
618
- )
619
- }
620
- ```
621
-
622
- Why:
623
-
624
- - The open action is tied to a concrete event.
625
- - The old menu is closed/replaced consistently.
626
- - Menu items are a snapshot for the current open.
627
-
628
- Standard:
629
-
630
- - Prefer `contextMenu.openAt(event, items)`.
631
- - Keep legacy queued/global context menu paths working for compatibility; instrument them before considering removal.
632
- - Keep menu item construction close to the surface that owns the action.
633
- - Add explicit `actionKey` for actions that should appear in `contextMenu.stats.actions`; unkeyed actions are counted only in aggregate totals.
634
- - Do not use labels as diagnostics keys: labels can be translated or sensitive.
635
- - Do not mutate menu item objects to track hover/open state.
636
-
637
- ## Floating Right Menu
638
-
639
- Use `DropdownMenu` for the default floating action menu. Use `useRightMenuController` when the app needs its own DOM while keeping the same open/fixed/select/submenu state contract.
640
-
641
- ```tsx
642
- import { DropdownMenu, useRightMenuController } from "wenay-react2"
643
-
644
- const elements = [
645
- { label: "Columns", subMenuContent: () => <ColumnsPanel /> },
646
- ]
647
-
648
- export function DefaultFloatingMenu() {
649
- return <DropdownMenu elements={elements} keyForSave="orders.rightMenu" />
650
- }
651
-
652
- export function CustomFloatingMenu() {
653
- const menu = useRightMenuController({ elements })
654
-
655
- return (
656
- <div onMouseEnter={menu.open} onMouseLeave={() => !menu.isFixed && menu.setOpen(false)}>
657
- <button onClick={menu.toggleFixed}>{menu.isFixed ? "Pinned" : "Menu"}</button>
658
- {menu.isOpen && elements.map((item, index) => (
659
- <button key={item.label} onMouseEnter={() => menu.selectItem(item, index)}>
660
- {item.label}
661
- </button>
662
- ))}
663
- {menu.submenuRender}
664
- </div>
665
- )
666
- }
667
- ```
668
-
669
- Why:
670
-
671
- - `DropdownMenu` remains the compatibility visual wrapper.
672
- - `useRightMenuController` owns state and drag/persist contracts when a custom view is needed.
673
- - The app decides trigger/content styling instead of changing shared default styles.
674
-
675
- Standard:
676
-
677
- - Do not duplicate right-menu hover/fixed/submenu timers in product code.
678
- - Keep persisted placement in `keyForSave` / `mapRightMenu`; do not create another store for the same menu.
679
- ## Floating Settings Window
680
-
681
- Use when a settings editor needs to stay stable while the bar itself reflows.
682
-
683
- ```tsx
684
- import { FloatingWindow, OutsideClickArea } from "wenay-react2"
685
-
686
- const OrdersToolbarSettings = ordersToolbar.Settings
687
-
688
- export function ToolbarSettingsWindow({ open, close }: { open: boolean; close: () => void }) {
689
- if (!open) return null
690
-
691
- return (
692
- <OutsideClickArea status={open} outsideClick={close}>
693
- <FloatingWindow
694
- keyForSave="orders.toolbar.settings"
695
- size={{ width: 360, height: 420 }}
696
- header={<div>Toolbar</div>}
697
- onCLickClose={close}
698
- moveOnlyHeader
699
- >
700
- <OrdersToolbarSettings />
701
- </FloatingWindow>
702
- </OutsideClickArea>
703
- )
704
- }
705
- ```
706
-
707
- Why:
708
-
709
- - `toolbar.Settings` is presentation-agnostic.
710
- - `FloatingWindow` owns drag/position/close chrome.
711
- - `OutsideClickArea` owns outside-click closing.
712
-
713
- Standard:
714
-
715
- - Do not bake floating-window behavior into `createToolbar`.
716
- - Choose the settings container in the app.
717
- - Reuse `FloatingWindow` rather than inventing another movable modal.
718
-
719
- Controller-first variant for custom chrome:
720
-
721
- ```tsx
722
- import type { ReactNode } from "react"
723
- import { useFloatingWindowController } from "wenay-react2"
724
-
725
- export function CustomFloatingPanel({ children }: { children: ReactNode }) {
726
- const wnd = useFloatingWindowController({
727
- keyForSave: "orders.custom.panel",
728
- size: { width: 360, height: 260 },
729
- limit: { x: { min: 0 }, y: { min: 0 } },
730
- })
731
-
732
- return (
733
- <section
734
- onMouseDown={wnd.onWindowMouseDown}
735
- style={{ position: "absolute", left: wnd.position.x, top: wnd.position.y, zIndex: wnd.zIndex }}
736
- >
737
- <header ref={wnd.headerRef} onMouseDown={wnd.onHeaderMouseDown} onTouchStart={wnd.onHeaderTouchStart}>
738
- Custom panel
739
- </header>
740
- {children}
741
- </section>
742
- )
743
- }
744
- ```
745
-
746
- Use the hook only when the default `FloatingWindow` DOM/chrome is not suitable. Keep `FloatingWindow` as the normal path for standard draggable windows.
747
-
748
- ## Observe Store Mirror
749
-
750
- Use when React needs a local mirror of a remote Observe store.
751
-
752
- ```tsx
753
- import { useStoreMirror, useStoreNode } from "wenay-react2"
754
-
755
- type State = { data: Record<string, number>; meta: { status: string } }
756
-
757
- export function PriceStatus({ remote }: { remote: any }) {
758
- const mirror = useStoreMirror<State, any>(remote, {
759
- data: {},
760
- meta: { status: "loading" },
761
- }, {
762
- mask: { data: true, meta: { status: true } },
763
- current: true,
764
- drain: 250,
765
- })
766
-
767
- const status = useStoreNode(mirror.store.node.meta.status)
768
-
769
- return <span>{mirror.ready ? status.value : "loading"}</span>
770
- }
771
- ```
772
-
773
- Why:
774
-
775
- - The hook owns subscription lifecycle.
776
- - The mask says exactly what is mirrored.
777
- - Transport stays explicit in the remote object.
778
-
779
- Standard:
780
-
781
- - Do not start network sync from a simple node read.
782
- - Subscribe to parent keys with `useStoreKeys` when add/delete matters.
783
- - For per-key grid updates, prefer `useStoreEach` or `useStoreReplayEach`
784
- feeding a grid/controller outside React state.
785
-
786
- ## Replay Feed Into A Grid
787
-
788
- Use when a replay line carries store patches and the grid should update per key.
789
-
790
- ```tsx
791
- import { AgGridTable, useAgGrid, useStoreReplayEach } from "wenay-react2"
792
-
793
- type Row = { id: string; price: number }
794
- type Rows = Record<string, Row>
795
-
796
- export function ReplayRowsGrid({ remote }: { remote: any }) {
797
- const grid = useAgGrid<Row>({ getId: row => row.id })
798
-
799
- const feed = useStoreReplayEach<Rows>(remote, (key, row) => {
800
- if (row == null) {
801
- grid.remove([{ id: key } as Row])
802
- } else {
803
- grid.update({ newData: [{ ...row, id: key }] })
804
- }
805
- }, {
806
- initial: {},
807
- staleMs: 2500,
808
- policy: "frame",
809
- })
810
-
811
- return (
812
- <>
813
- <span>{feed.ready ? "live" : "connecting"}</span>
814
- <AgGridTable<Row> controller={grid} columnDefs={[
815
- { field: "id" },
816
- { field: "price" },
817
- ]} />
818
- </>
819
- )
820
- }
821
- ```
822
-
823
- Why:
824
-
825
- - Keyframe and root replace expand into per-key calls.
826
- - Deletes arrive as `undefined`.
827
- - The grid updates without rerendering a React row array on every event.
828
-
829
- Standard:
830
-
831
- - Keep the fold target outside React state: grid controller, ref, Map, canvas,
832
- or external store.
833
- - Use `staleMs` when freshness is user-visible.
834
- - Use route hand-off hooks when switching relay/direct without gaps.
835
-
836
- ## Logs
837
-
838
- Use for shared app-visible log output.
839
-
840
- ```tsx
841
- import { logsApi } from "wenay-react2"
842
-
843
- logsApi.addLogs({
844
- id: "orders",
845
- time: new Date(),
846
- txt: "order saved",
847
- var: 1,
848
- })
849
-
850
- const PageLogs = logsApi.React.PageLogs
851
-
852
- export function LogsPanel() {
853
- return <PageLogs />
854
- }
855
- ```
856
-
857
- For headless custom log stores, use `createLogsController`; `getLogsApi` remains the compatibility entrypoint for the shared global logger:
858
-
859
- ```ts
860
- import { createLogsController } from "wenay-react2"
861
-
862
- const logs = createLogsController({options: {limit: 50, limitPer: 500}})
863
- logs.addLogs({id: "orders", time: new Date(), txt: "queued", var: 1})
864
- logs.getLatest()
865
- ```
866
-
867
- For corner notifications, the compatibility wrapper is still the shortest path. Use the hook/controller layer when the app needs to place or restyle the notification chrome itself:
868
-
869
- ```tsx
870
- import { MessageEventLogsView, useMessageEventLogsController } from "wenay-react2"
871
-
872
- export function CornerLogs() {
873
- const notifications = useMessageEventLogsController({maxVisible: 4})
874
- return <MessageEventLogsView controller={notifications} zIndex={80} />
875
- }
876
- ```
877
-
878
- For compact embedded log tables, use the MiniLogs hook/controller first when the parent needs grid control:
879
-
880
- ```tsx
881
- import { AgGridTable, MiniLogsTable, useMiniLogsTable } from "wenay-react2"
882
-
883
- type LogRow = { time: Date; id: string; var: number; txt: string; address?: string }
884
-
885
- export function CompactLogs({rows}: {rows: LogRow[]}) {
886
- const table = useMiniLogsTable<LogRow>({
887
- data: rows,
888
- onClick: e => console.log(e.data),
889
- })
890
-
891
- return <AgGridTable<LogRow> {...table.props} />
892
- }
893
-
894
- export function SimpleCompactLogs({rows}: {rows: LogRow[]}) {
895
- return <MiniLogsTable<LogRow> data={rows} />
896
- }
897
- ```
898
-
899
- Why:
900
-
901
- - The shared logger already has notification/table chrome.
902
- - `createLogsController` keeps append/limit/settings state testable without rewriting logger UI.
903
- - `useMessageEventLogsController` owns notification queue/timers/settings; `MessageEventLogsView` is the visual layer and `MessageEventLogs` remains the wrapper.
904
- - `useMiniLogsTable` keeps the compact table defaults, click contract, and grid control API in one reusable layer.
905
- - `MiniLogsTable` is the visual layer; `MiniLogs` remains the old compatibility wrapper.
906
- - Styling is tokenized through `--logs-*`.
907
-
908
- Standard:
909
-
910
- - Use a stable `id` per source area.
911
- - Keep high-volume debug-only logs out of user-facing logger surfaces unless
912
- the app intentionally exposes them.
913
-
914
- ## QA Stand Standards
915
-
916
- The QA stand and in-repository example files are maintained usage examples, not disposable tests. When a public standard changes, update the affected card/example in the same pass, or record why no example change is needed.
917
-
918
- Use the QA stand to validate behavior, but read it critically.
919
-
920
- Current strong examples:
921
-
922
- - `qa.tsx` card 20 for `SettingsDialog` search/tree registry behavior.
923
- - `qa.tsx` card 21 for `createUiSlot` persisted placement behavior.
924
- - `qa.tsx` card 25 for local `createToolbar` config/settings behavior.
925
- - `qa.tsx` cards 28-32 for `columnState`, `createColumnGrid`, `ColumnsMenu`, `ColumnDots`,
926
- `CardList`, and `createToolbar({source})`.
927
- - `qa.tsx` cards 23-26 for Replay hooks.
928
- - `src/common/src/grid/agGrid4/example.tsx` for `useAgGrid` / `AgGridTable` controller examples.
929
- - `qa.tsx` active cards for current work.
930
-
931
- Legacy or low-level examples:
932
-
933
- - `src/common/testUseReact/useGrid.tsx` and archive card 5 use direct
934
- `applyGridRows`. Keep this as regression coverage for the low-level helper,
935
- but do not copy it as the first pattern for new grids.
936
- - Archive cards may preserve old bug repros or compatibility paths. Their
937
- presence does not automatically make the shown API canonical.
938
-
939
- When adding an example:
940
-
941
- - Explain what problem it solves.
942
- - Name the owner of lifecycle/state.
943
- - Mention what the app still owns.
944
- - Prefer root imports.
945
- - Avoid business-specific names in shared docs.
946
- - If the example is intentionally low-level, label it as low-level.
947
-
948
- ## Anti-Patterns To Avoid
949
-
950
- | Anti-pattern | Prefer | Reason |
951
- | --- | --- | --- |
489
+ return <OrdersToolbarBar settings reset />
490
+ }
491
+ ```
492
+
493
+ Why:
494
+
495
+ - Users can choose order, visibility, and density.
496
+ - The same `toolbar.Settings` can be rendered in an inline popover, global
497
+ settings dialog, or a custom floating window.
498
+
499
+ Standard:
500
+
501
+ - Toolbar items should be commands or view controls, not product data rows.
502
+ - Use `fixed` for commands that must not disappear.
503
+ - Prefer `item.render(density)` only when the default icon/label renderer is
504
+ insufficient.
505
+
506
+ ## Settings Dialog
507
+
508
+ Use when app settings need a searchable tree.
509
+
510
+ ```tsx
511
+ import { SettingsDialog, registerSettingsSection, useSettingsDialogController } from "wenay-react2"
512
+ import { useEffect } from "react"
513
+
514
+ const ColumnsSettings = columnsToolbar.Settings
515
+
516
+ export function OrdersSettingsRegistration() {
517
+ useEffect(() => {
518
+ return registerSettingsSection({
519
+ key: "orders.columns",
520
+ name: "Columns",
521
+ parentKey: "orders",
522
+ keywords: ["table", "grid", "visibility"],
523
+ render: () => <ColumnsSettings />,
524
+ })
525
+ }, [])
526
+
527
+ return null
528
+ }
529
+
530
+ export function SettingsButton() {
531
+ return (
532
+ <SettingsDialog
533
+ trigger={<button>Settings</button>}
534
+ sections={[
535
+ { key: "orders", name: "Orders", render: () => null },
536
+ ]}
537
+ defaultSection="orders.columns"
538
+ />
539
+ )
540
+ }
541
+
542
+ export function CustomSettingsTrigger() {
543
+ const settings = useSettingsDialogController({
544
+ sections: [{ key: "orders", name: "Orders", render: () => null }],
545
+ defaultSection: "orders",
546
+ })
547
+
548
+ return <button onClick={settings.openDialog}>{settings.open ? "Open" : "Settings"}</button>
549
+ }
550
+ ```
551
+
552
+ Why:
553
+
554
+ - Sections can be registered by feature modules.
555
+ - Search and tree behavior stays consistent across apps.
556
+ - `useSettingsDialogController` exposes the same open/search/tree/history/resize actions for custom settings chrome.
557
+
558
+ Standard:
559
+
560
+ - Use stable `key` values.
561
+ - Put searchable synonyms in `keywords` or `searchText`.
562
+ - Do not put settings registry state in product globals when the shared
563
+ registry already fits.
564
+ - Do not rewrite tree/search/divider UX just to customize the trigger; use the controller actions.
565
+
566
+ ## Params Editor
567
+
568
+ Use `ParamsEditor` for the normal generated editor. Use `useParamsEditorController` only when the app needs a custom renderer over the same draft/debounce/expand contract.
569
+
570
+ ```tsx
571
+ import { ParamsEditor, useParamsEditorController } from "wenay-react2"
572
+
573
+ export function DefaultParams({params, save}: {params: any; save: (next: any) => void}) {
574
+ return <ParamsEditor params={params} onChange={save} onExpand={save} />
575
+ }
576
+
577
+ export function CustomParamsHeader({params, save}: {params: any; save: (next: any) => void}) {
578
+ const editor = useParamsEditorController({params, onChange: save})
579
+
580
+ return (
581
+ <button onClick={() => editor.notifyChange()}>
582
+ Save draft
583
+ </button>
584
+ )
585
+ }
586
+ ```
587
+
588
+ Why:
589
+
590
+ - `ParamsEditor` keeps the existing generated rows and input behavior.
591
+ - The controller owns the mutable draft clone, immediate/delayed notify, expand callback, and timer cleanup.
592
+ - Product-specific validation and save policy stay outside the shared editor.
593
+
594
+ Standard:
595
+
596
+ - Do not rewrite row/input rendering just to use the controller.
597
+ - Keep `ParamsEdit` and `ParamsArrayEdit` as compatibility wrappers unless the async load/save policy is being redesigned explicitly.
598
+ ## Context Menu
599
+
600
+ Use for right-click actions.
601
+
602
+ ```tsx
603
+ import { contextMenu } from "wenay-react2"
604
+
605
+ export function RowSurface() {
606
+ return (
607
+ <contextMenu.Layer>
608
+ <div
609
+ onContextMenu={event => contextMenu.openAt(event, [
610
+ { name: "Copy", actionKey: "row.copy", onClick: copy },
611
+ { name: "Delete", actionKey: "row.delete", onClick: removeSelected },
612
+ { name: "More", actionKey: "row.more", next: () => [
613
+ { name: "Open details", actionKey: "row.openDetails", onClick: openDetails },
614
+ ] },
615
+ ])}
616
+ />
617
+ </contextMenu.Layer>
618
+ )
619
+ }
620
+ ```
621
+
622
+ Why:
623
+
624
+ - The open action is tied to a concrete event.
625
+ - The old menu is closed/replaced consistently.
626
+ - Menu items are a snapshot for the current open.
627
+
628
+ Standard:
629
+
630
+ - Prefer `contextMenu.openAt(event, items)`.
631
+ - Keep legacy queued/global context menu paths working for compatibility; instrument them before considering removal.
632
+ - Keep menu item construction close to the surface that owns the action.
633
+ - Add explicit `actionKey` for actions that should appear in `contextMenu.stats.actions`; unkeyed actions are counted only in aggregate totals.
634
+ - Do not use labels as diagnostics keys: labels can be translated or sensitive.
635
+ - Do not mutate menu item objects to track hover/open state.
636
+
637
+ ## Floating Right Menu
638
+
639
+ Use `DropdownMenu` for the default floating action menu. Use `useRightMenuController` when the app needs its own DOM while keeping the same open/fixed/select/submenu state contract.
640
+
641
+ ```tsx
642
+ import { DropdownMenu, useRightMenuController } from "wenay-react2"
643
+
644
+ const elements = [
645
+ { label: "Columns", subMenuContent: () => <ColumnsPanel /> },
646
+ ]
647
+
648
+ export function DefaultFloatingMenu() {
649
+ return <DropdownMenu elements={elements} keyForSave="orders.rightMenu" />
650
+ }
651
+
652
+ export function CustomFloatingMenu() {
653
+ const menu = useRightMenuController({ elements })
654
+
655
+ return (
656
+ <div onMouseEnter={menu.open} onMouseLeave={() => !menu.isFixed && menu.setOpen(false)}>
657
+ <button onClick={menu.toggleFixed}>{menu.isFixed ? "Pinned" : "Menu"}</button>
658
+ {menu.isOpen && elements.map((item, index) => (
659
+ <button key={item.label} onMouseEnter={() => menu.selectItem(item, index)}>
660
+ {item.label}
661
+ </button>
662
+ ))}
663
+ {menu.submenuRender}
664
+ </div>
665
+ )
666
+ }
667
+ ```
668
+
669
+ Why:
670
+
671
+ - `DropdownMenu` remains the compatibility visual wrapper.
672
+ - `useRightMenuController` owns state and drag/persist contracts when a custom view is needed.
673
+ - The app decides trigger/content styling instead of changing shared default styles.
674
+
675
+ Standard:
676
+
677
+ - Do not duplicate right-menu hover/fixed/submenu timers in product code.
678
+ - Keep persisted placement in `keyForSave` / `mapRightMenu`; do not create another store for the same menu.
679
+ ## Floating Settings Window
680
+
681
+ Use when a settings editor needs to stay stable while the bar itself reflows.
682
+
683
+ ```tsx
684
+ import { FloatingWindow, OutsideClickArea } from "wenay-react2"
685
+
686
+ const OrdersToolbarSettings = ordersToolbar.Settings
687
+
688
+ export function ToolbarSettingsWindow({ open, close }: { open: boolean; close: () => void }) {
689
+ if (!open) return null
690
+
691
+ return (
692
+ <OutsideClickArea status={open} outsideClick={close}>
693
+ <FloatingWindow
694
+ keyForSave="orders.toolbar.settings"
695
+ size={{ width: 360, height: 420 }}
696
+ header={<div>Toolbar</div>}
697
+ onCLickClose={close}
698
+ moveOnlyHeader
699
+ >
700
+ <OrdersToolbarSettings />
701
+ </FloatingWindow>
702
+ </OutsideClickArea>
703
+ )
704
+ }
705
+ ```
706
+
707
+ Why:
708
+
709
+ - `toolbar.Settings` is presentation-agnostic.
710
+ - `FloatingWindow` owns drag/position/close chrome.
711
+ - `OutsideClickArea` owns outside-click closing.
712
+
713
+ Standard:
714
+
715
+ - Do not bake floating-window behavior into `createToolbar`.
716
+ - Choose the settings container in the app.
717
+ - Reuse `FloatingWindow` rather than inventing another movable modal.
718
+
719
+ Controller-first variant for custom chrome:
720
+
721
+ ```tsx
722
+ import type { ReactNode } from "react"
723
+ import { useFloatingWindowController } from "wenay-react2"
724
+
725
+ export function CustomFloatingPanel({ children }: { children: ReactNode }) {
726
+ const wnd = useFloatingWindowController({
727
+ keyForSave: "orders.custom.panel",
728
+ size: { width: 360, height: 260 },
729
+ limit: { x: { min: 0 }, y: { min: 0 } },
730
+ })
731
+
732
+ return (
733
+ <section
734
+ onMouseDown={wnd.onWindowMouseDown}
735
+ style={{ position: "absolute", left: wnd.position.x, top: wnd.position.y, zIndex: wnd.zIndex }}
736
+ >
737
+ <header ref={wnd.headerRef} onMouseDown={wnd.onHeaderMouseDown} onTouchStart={wnd.onHeaderTouchStart}>
738
+ Custom panel
739
+ </header>
740
+ {children}
741
+ </section>
742
+ )
743
+ }
744
+ ```
745
+
746
+ Use the hook only when the default `FloatingWindow` DOM/chrome is not suitable. Keep `FloatingWindow` as the normal path for standard draggable windows.
747
+
748
+ ## Observe Store Mirror
749
+
750
+ Use when React needs a local mirror of a remote Observe store.
751
+
752
+ ```tsx
753
+ import { useStoreMirror, useStoreNode } from "wenay-react2"
754
+
755
+ type State = { data: Record<string, number>; meta: { status: string } }
756
+
757
+ export function PriceStatus({ remote }: { remote: any }) {
758
+ const mirror = useStoreMirror<State, any>(remote, {
759
+ data: {},
760
+ meta: { status: "loading" },
761
+ }, {
762
+ mask: { data: true, meta: { status: true } },
763
+ current: true,
764
+ drain: 250,
765
+ })
766
+
767
+ const status = useStoreNode(mirror.store.node.meta.status)
768
+
769
+ return <span>{mirror.ready ? status.value : "loading"}</span>
770
+ }
771
+ ```
772
+
773
+ Why:
774
+
775
+ - The hook owns subscription lifecycle.
776
+ - The mask says exactly what is mirrored.
777
+ - Transport stays explicit in the remote object.
778
+
779
+ Standard:
780
+
781
+ - Do not start network sync from a simple node read.
782
+ - Subscribe to parent keys with `useStoreKeys` when add/delete matters.
783
+ - For per-key grid updates, prefer `useStoreEach` or `useStoreReplayEach`
784
+ feeding a grid/controller outside React state.
785
+
786
+ ## Replay Feed Into A Grid
787
+
788
+ Use when a replay line carries store patches and the grid should update per key.
789
+
790
+ ```tsx
791
+ import { AgGridTable, useAgGrid, useStoreReplayEach } from "wenay-react2"
792
+
793
+ type Row = { id: string; price: number }
794
+ type Rows = Record<string, Row>
795
+
796
+ export function ReplayRowsGrid({ remote }: { remote: any }) {
797
+ const grid = useAgGrid<Row>({ getId: row => row.id })
798
+
799
+ const feed = useStoreReplayEach<Rows>(remote, (key, row) => {
800
+ if (row == null) {
801
+ grid.remove([{ id: key } as Row])
802
+ } else {
803
+ grid.update({ newData: [{ ...row, id: key }] })
804
+ }
805
+ }, {
806
+ initial: {},
807
+ staleMs: 2500,
808
+ policy: "frame",
809
+ })
810
+
811
+ return (
812
+ <>
813
+ <span>{feed.ready ? "live" : "connecting"}</span>
814
+ <AgGridTable<Row> controller={grid} columnDefs={[
815
+ { field: "id" },
816
+ { field: "price" },
817
+ ]} />
818
+ </>
819
+ )
820
+ }
821
+ ```
822
+
823
+ Why:
824
+
825
+ - Keyframe and root replace expand into per-key calls.
826
+ - Deletes arrive as `undefined`.
827
+ - The grid updates without rerendering a React row array on every event.
828
+
829
+ Standard:
830
+
831
+ - Keep the fold target outside React state: grid controller, ref, Map, canvas,
832
+ or external store.
833
+ - Use `staleMs` when freshness is user-visible.
834
+ - Use route hand-off hooks when switching relay/direct without gaps.
835
+
836
+ ## Logs
837
+
838
+ Use for shared app-visible log output.
839
+
840
+ ```tsx
841
+ import { logsApi } from "wenay-react2"
842
+
843
+ logsApi.addLogs({
844
+ id: "orders",
845
+ time: new Date(),
846
+ txt: "order saved",
847
+ var: 1,
848
+ })
849
+
850
+ const PageLogs = logsApi.React.PageLogs
851
+
852
+ export function LogsPanel() {
853
+ return <PageLogs />
854
+ }
855
+ ```
856
+
857
+ For headless custom log stores, use `createLogsController`; `getLogsApi` remains the compatibility entrypoint for the shared global logger:
858
+
859
+ ```ts
860
+ import { createLogsController } from "wenay-react2"
861
+
862
+ const logs = createLogsController({options: {limit: 50, limitPer: 500}})
863
+ logs.addLogs({id: "orders", time: new Date(), txt: "queued", var: 1})
864
+ logs.getLatest()
865
+ ```
866
+
867
+ For corner notifications, the compatibility wrapper is still the shortest path. Use the hook/controller layer when the app needs to place or restyle the notification chrome itself:
868
+
869
+ ```tsx
870
+ import { MessageEventLogsView, useMessageEventLogsController } from "wenay-react2"
871
+
872
+ export function CornerLogs() {
873
+ const notifications = useMessageEventLogsController({maxVisible: 4})
874
+ return <MessageEventLogsView controller={notifications} zIndex={80} />
875
+ }
876
+ ```
877
+
878
+ For compact embedded log tables, use the MiniLogs hook/controller first when the parent needs grid control:
879
+
880
+ ```tsx
881
+ import { AgGridTable, MiniLogsTable, useMiniLogsTable } from "wenay-react2"
882
+
883
+ type LogRow = { time: Date; id: string; var: number; txt: string; address?: string }
884
+
885
+ export function CompactLogs({rows}: {rows: LogRow[]}) {
886
+ const table = useMiniLogsTable<LogRow>({
887
+ data: rows,
888
+ onClick: e => console.log(e.data),
889
+ })
890
+
891
+ return <AgGridTable<LogRow> {...table.props} />
892
+ }
893
+
894
+ export function SimpleCompactLogs({rows}: {rows: LogRow[]}) {
895
+ return <MiniLogsTable<LogRow> data={rows} />
896
+ }
897
+ ```
898
+
899
+ Why:
900
+
901
+ - The shared logger already has notification/table chrome.
902
+ - `createLogsController` keeps append/limit/settings state testable without rewriting logger UI.
903
+ - `useMessageEventLogsController` owns notification queue/timers/settings; `MessageEventLogsView` is the visual layer and `MessageEventLogs` remains the wrapper.
904
+ - `useMiniLogsTable` keeps the compact table defaults, click contract, and grid control API in one reusable layer.
905
+ - `MiniLogsTable` is the visual layer; `MiniLogs` remains the old compatibility wrapper.
906
+ - Styling is tokenized through `--logs-*`.
907
+
908
+ Standard:
909
+
910
+ - Use a stable `id` per source area.
911
+ - Keep high-volume debug-only logs out of user-facing logger surfaces unless
912
+ the app intentionally exposes them.
913
+
914
+ ## QA Stand Standards
915
+
916
+ The QA stand and in-repository example files are maintained usage examples, not disposable tests. When a public standard changes, update the affected card/example in the same pass, or record why no example change is needed.
917
+
918
+ Use the QA stand to validate behavior, but read it critically.
919
+
920
+ Current strong examples:
921
+
922
+ - `qa.tsx` card 20 for `SettingsDialog` search/tree registry behavior.
923
+ - `qa.tsx` card 21 for `createUiSlot` persisted placement behavior.
924
+ - `qa.tsx` card 25 for local `createToolbar` config/settings behavior.
925
+ - `qa.tsx` cards 28-32 for `columnState`, `createColumnGrid`, `ColumnsMenu`, `ColumnDots`,
926
+ `CardList`, and `createToolbar({source})`.
927
+ - `qa.tsx` cards 23-26 for Replay hooks.
928
+ - `src/common/src/grid/agGrid4/example.tsx` for `useAgGrid` / `AgGridTable` controller examples.
929
+ - `qa.tsx` active cards for current work.
930
+
931
+ Legacy or low-level examples:
932
+
933
+ - `src/common/testUseReact/useGrid.tsx` and archive card 5 use direct
934
+ `applyGridRows`. Keep this as regression coverage for the low-level helper,
935
+ but do not copy it as the first pattern for new grids.
936
+ - Archive cards may preserve old bug repros or compatibility paths. Their
937
+ presence does not automatically make the shown API canonical.
938
+
939
+ When adding an example:
940
+
941
+ - Explain what problem it solves.
942
+ - Name the owner of lifecycle/state.
943
+ - Mention what the app still owns.
944
+ - Prefer root imports.
945
+ - Avoid business-specific names in shared docs.
946
+ - If the example is intentionally low-level, label it as low-level.
947
+
948
+ ## Anti-Patterns To Avoid
949
+
950
+ | Anti-pattern | Prefer | Reason |
951
+ | --- | --- | --- |
952
952
  | Reusable behavior owned by a visual component | Headless `use*` hook or `create*` controller plus a thin component | Apps and stand cards can reuse lifecycle/state without copying JSX. |
953
- | Direct `applyGridRows` in new React UI | `useAgGrid` / `createGridBuffer` | Controller owns readiness, sync, detach, and remount behavior. |
954
- | Manual toolbar-grid order bridge | `createToolbar({source: columnState.api.listSource})` | One config should drive all views. |
955
- | Writing storage inside a primitive | `memoryCache.onDirty` in app startup | App owns persistence timing. |
956
- | One-off search history state in a component | `createSearchHistory({key})` | Search recall is reusable and publishes through memoryCache. |
957
- | Product group logic inside `createColumnBuffer` | App wrapper around `createColumnBuffer` | Shared layer must stay domain-free. |
958
- | Context menu global queue for new code | `contextMenu.openAt(e, items)` | Opens a concrete snapshot at a concrete point. |
959
- | `useState` for every replay frame | Ref/store/grid/canvas fold target | Avoid rerendering high-frequency streams. |
960
- | New examples with `Get*`, `FuncJSX`, `*2`, `*3` names | `create*`, `use*`, controller names | Keep canonical naming stable. |
961
-
962
- ## Documentation Rule
963
-
964
- When a new public usage pattern is introduced:
965
-
966
- - Put the short signature in `doc/wenay-react2.md`.
967
- - Put the detailed edge cases in `doc/wenay-react2-rare.md`.
968
- - Put purpose and standards in this file.
969
- - Put broad project role changes in `doc/PROJECT_FUNCTIONALITY.md`.
970
- - Link new durable docs from `README.md`.
971
- - Add a changelog note in `doc/changes/`.
953
+ | Direct `applyGridRows` in new React UI | `useAgGrid` / `createGridBuffer` | Controller owns readiness, sync, detach, and remount behavior. |
954
+ | Manual toolbar-grid order bridge | `createToolbar({source: columnState.api.listSource})` | One config should drive all views. |
955
+ | Writing storage inside a primitive | `memoryCache.onDirty` in app startup | App owns persistence timing. |
956
+ | One-off search history state in a component | `createSearchHistory({key})` | Search recall is reusable and publishes through memoryCache. |
957
+ | Product group logic inside `createColumnBuffer` | App wrapper around `createColumnBuffer` | Shared layer must stay domain-free. |
958
+ | Context menu global queue for new code | `contextMenu.openAt(e, items)` | Opens a concrete snapshot at a concrete point. |
959
+ | `useState` for every replay frame | Ref/store/grid/canvas fold target | Avoid rerendering high-frequency streams. |
960
+ | New examples with `Get*`, `FuncJSX`, `*2`, `*3` names | `create*`, `use*`, controller names | Keep canonical naming stable. |
961
+
962
+ ## Documentation Rule
963
+
964
+ When a new public usage pattern is introduced:
965
+
966
+ - Put the short signature in `doc/wenay-react2.md`.
967
+ - Put the detailed edge cases in `doc/wenay-react2-rare.md`.
968
+ - Put purpose and standards in this file.
969
+ - Put broad project role changes in `doc/PROJECT_FUNCTIONALITY.md`.
970
+ - Link new durable docs from `README.md`.
971
+ - Add a changelog note in `doc/changes/`.