wenay-react2 1.0.40 → 1.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/doc/EXAMPLE_USAGE.md +971 -0
  2. package/doc/PROJECT_FUNCTIONALITY.md +405 -0
  3. package/doc/PROJECT_RULES.md +80 -0
  4. package/doc/WENAY_REACT2_RENAMES.md +177 -0
  5. package/doc/changes/v1.0.35.md +26 -0
  6. package/doc/changes/v1.0.37.md +13 -0
  7. package/doc/changes/v1.0.38.md +48 -0
  8. package/doc/changes/v1.0.39.md +35 -0
  9. package/doc/changes/v1.0.40.md +15 -0
  10. package/doc/changes/v1.0.41.md +36 -0
  11. package/doc/changes/v1.0.42.md +26 -0
  12. package/doc/progress/README.md +14 -0
  13. package/doc/progress/architecture-fix-queue.md +74 -0
  14. package/doc/progress/hook-controller-opportunities.md +367 -0
  15. package/doc/progress/hook-extraction-audit.md +195 -0
  16. package/doc/progress/public-surface-normalization.md +368 -0
  17. package/doc/progress/style-system-normalization.md +121 -0
  18. package/doc/target/README.md +33 -0
  19. package/doc/target/my.md +81 -0
  20. package/doc/wenay-react2-1.0.8.tgz +0 -0
  21. package/doc/wenay-react2-rare.md +807 -0
  22. package/doc/wenay-react2.md +539 -0
  23. package/lib/common/api.d.ts +1 -0
  24. package/lib/common/api.js +8 -4
  25. package/lib/common/src/components/Dnd/DragArea.d.ts +5 -0
  26. package/lib/common/src/components/Dnd/DragArea.js +5 -1
  27. package/lib/common/src/components/Dnd/FloatingWindow.d.ts +9 -5
  28. package/lib/common/src/components/Dnd/FloatingWindow.js +39 -97
  29. package/lib/common/src/components/Dnd/Resizable.d.ts +3 -7
  30. package/lib/common/src/components/Dnd/Resizable.js +4 -3
  31. package/lib/common/src/components/Menu/RightMenu.js +8 -4
  32. package/lib/common/src/components/Menu/RightMenuStore.d.ts +2 -2
  33. package/lib/common/src/components/Menu/RightMenuStore.js +5 -3
  34. package/lib/common/src/components/Modal/LeftModal.js +4 -2
  35. package/lib/common/src/components/Modal/ModalContextProvider.js +5 -16
  36. package/lib/common/src/components/MyResizeObserver.d.ts +24 -0
  37. package/lib/common/src/components/MyResizeObserver.js +49 -0
  38. package/lib/common/src/components/Overlay.d.ts +24 -0
  39. package/lib/common/src/components/Overlay.js +28 -0
  40. package/lib/common/src/components/Settings/SettingsDialog.js +21 -22
  41. package/lib/common/src/components/Toolbar/Toolbar.d.ts +1 -0
  42. package/lib/common/src/components/Toolbar/Toolbar.js +16 -23
  43. package/lib/common/src/grid/columnState/ColumnsMenu.js +4 -13
  44. package/lib/common/src/grid/columnState/columnGrid.d.ts +103 -0
  45. package/lib/common/src/grid/columnState/columnGrid.js +231 -0
  46. package/lib/common/src/grid/columnState/columnState.js +10 -9
  47. package/lib/common/src/grid/columnState/index.js +3 -0
  48. package/lib/common/src/hooks/useDraggable.d.ts +8 -0
  49. package/lib/common/src/hooks/useDraggable.js +13 -4
  50. package/lib/common/src/hooks/useOutside.d.ts +4 -1
  51. package/lib/common/src/hooks/useOutside.js +2 -1
  52. package/lib/common/src/logs/logs.d.ts +123 -5
  53. package/lib/common/src/logs/logs.js +218 -130
  54. package/lib/common/src/logs/logsController.d.ts +3 -0
  55. package/lib/common/src/styles/tokens.d.ts +7 -6
  56. package/lib/common/src/styles/tokens.js +8 -5
  57. package/lib/common/src/utils/cache.d.ts +12 -0
  58. package/lib/common/src/utils/cache.js +0 -0
  59. package/lib/common/src/utils/fixedOrder.d.ts +15 -0
  60. package/lib/common/src/utils/fixedOrder.js +30 -0
  61. package/lib/common/src/utils/index.d.ts +2 -0
  62. package/lib/common/src/utils/index.js +2 -0
  63. package/lib/common/src/utils/memoryStore.d.ts +3 -6
  64. package/lib/common/src/utils/memoryStore.js +1 -3
  65. package/lib/common/src/utils/persistedMaps.d.ts +16 -0
  66. package/lib/common/src/utils/persistedMaps.js +5 -0
  67. package/lib/common/src/utils/searchHistory.js +7 -6
  68. package/lib/common/src/utils/structEqual.d.ts +12 -0
  69. package/lib/common/src/utils/structEqual.js +40 -0
  70. package/lib/common/updateBy.d.ts +3 -0
  71. package/lib/common/updateBy.js +57 -22
  72. package/lib/style/menuRight.css +29 -23
  73. package/lib/style/style.css +73 -10
  74. package/lib/style/tokens.css +8 -4
  75. package/package.json +3 -2
@@ -0,0 +1,539 @@
1
+ # wenay-react2 - BRIEF cheat sheet (canonical UI/controller API)
2
+
3
+ > Root import: `import { ... } from "wenay-react2"`.
4
+ > Notation: `name(args) -> ret`. JSX examples show the intended public path, not every prop.
5
+ > Short controller-style names are canonical. Removed names are recorded in
6
+ > **WENAY_REACT2_RENAMES.md** for migration only; old aliases are not exported.
7
+
8
+ ## Standard
9
+ ```
10
+ Hooks that own lifecycle/state return a small controller:
11
+ api.props / api.bind // props to spread on an element
12
+ api.open() / close() / set() // modal/menu-like state
13
+ api.update() / remove() / clean() // data buffers
14
+ api.fit() / flush() / sync() // ag-grid lifecycle
15
+
16
+ Components receive controllers when state must outlive the component.
17
+ Library primitives never know app-specific terms, domain objects, or group policy.
18
+ Put app-specific layout/build rules in an app wrapper above the primitive.
19
+ ```
20
+
21
+ ## Render Memory
22
+ ```
23
+ updateBy(obj) / useUpdateBy(obj) // subscribe current component to renderBy(obj)
24
+ updateBy(obj, cb) // imperative callback instead of a re-render;
25
+ // cb goes through a ref - inline identity is fine
26
+ renderBy(obj, ms?) // emit render for subscribers
27
+ renderByRevers(obj, ms?, reverse?=true) // reverse/last affect ONLY React subscribers;
28
+ renderByLast(obj, ms?) // updateBy(obj, cb) callbacks always all run first
29
+
30
+ const state = { count: 0 }
31
+ const api = createUpdateApi(state)
32
+ api.use() // hook subscription
33
+ api.emit(ms?) // renderBy(state)
34
+ api.on(obj => {}) -> off
35
+
36
+ const api2 = useUpdateByApi(state) // hook + controller in one call
37
+ ```
38
+
39
+ Persistent process memory:
40
+ ```
41
+ memoryGetOrCreate(key, def, {abs?, deepAutoMerge?, reversDeep?}) -> def-or-stored
42
+ memoryGetById(key, def, id) -> stored only while id is the same
43
+ memorySet(key, data)
44
+ memoryGet(key)
45
+ memoryUpdate(key, mutate) -> cur? // mutate + rerender + announce in one call
46
+ memoryMarkDirty(key) // announce an in-place mutation of a memoryGetOrCreate object
47
+ createSearchHistory({key, max?}) // reusable small persisted search history controller
48
+ memoryMaps // rnd / resize / other maps
49
+ ```
50
+
51
+ Persistence contract (memoryCache): the library NEVER writes storage by itself. The persisted maps
52
+ (floatingWindowMap, mapResiReact, mapRightMenu, memoryProps) are `ObservableMap`s - set/delete/clear
53
+ announce themselves, in-place mutations are announced at the commit points (drag/resize stop,
54
+ menu drag end, setPlace). memoryCache observes the maps it owns; the app owns the write policy:
55
+ ```
56
+ memoryCache.load() // once on start; remembers the saved snapshot
57
+ memoryCache.onDirty((scope?, key?) => ...) -> off // dirty channel (coalesced, async)
58
+ memoryCache.saveDebounced(ms?) / save() / flush() // write only payloads that differ from the snapshot
59
+ memoryCache.isDirty() // cheap hint, e.g. a beforeunload guard
60
+
61
+ memoryCache.load()
62
+ const off = memoryCache.onDirty(() => memoryCache.saveDebounced(800))
63
+ document.addEventListener("visibilitychange",
64
+ () => { if (document.visibilityState == "hidden") void memoryCache.flush() })
65
+ window.addEventListener("pagehide", () => { void memoryCache.flush() }) // backstop: flush is async
66
+ ```
67
+
68
+ The load + dirty->saveDebounced part of that contract as one hook (React components):
69
+ ```
70
+ const persistence = useCacheMapPersistence(memoryCache, delayMs?=300)
71
+ persistence.isDirty(); persistence.flush(); persistence.save(); persistence.reload() // reload = load() alias
72
+ ```
73
+ The pagehide/visibility flush backstops stay app-side (see above) - the hook deliberately does not own them.
74
+
75
+ ## Outside Click / Buttons
76
+ ```
77
+ const outside = useOutside({onOutside, enabled?})
78
+ <div {...outside.props} />
79
+ outside.enable()
80
+ outside.disable()
81
+ outside.contains(event.target)
82
+
83
+ <OutsideClickArea outsideClick={close} status={open}>...</OutsideClickArea>
84
+
85
+ <Button button={<button>Open</button>} outClick keyForSave?>{...}</Button> // keySave = deprecated alias
86
+ <OutsideButton button={...}>{...}</OutsideButton>
87
+ <HoverButton button={...}>{...}</HoverButton>
88
+ <AbsoluteButton button={...}>{...}</AbsoluteButton>
89
+ ```
90
+
91
+ ## Element Size / Resize Observer
92
+ ```
93
+ const box = useResizeObserver<HTMLDivElement>(() => onResize()) // shared singleton observer
94
+ <div ref={box.ref} />; box.element()
95
+
96
+ const size = useElementSize<HTMLDivElement>() // "want the size -> get the value/method"
97
+ <div ref={size.ref} />
98
+ size.width; size.height // state, rounded, no-op resize does not re-render
99
+ size.getSize() // live getter (exact, no render wait)
100
+
101
+ setResizeableElement(el) / removeResizeableElement(el) // legacy imperative auto-shrink path, unchanged
102
+ ```
103
+ Both hooks ride the same module-level `CResizeObserver` singleton (one native `ResizeObserver` for the whole app). QA card 19.
104
+
105
+ ## Drag / Floating Windows
106
+ ```
107
+ const drag = useDraggableApi({initialPosition, holdMs?, onDragStart?, onDragEnd?, onMove?, trackState?})
108
+ <div {...drag.bind} style={{transform: `translate(${drag.position.x}px, ${drag.position.y}px)`}} />
109
+ drag.setPosition({x, y})
110
+ drag.resetPosition()
111
+ drag.cancelDrag()
112
+ // imperative path: onMove(p) fires per move tick (through a ref); trackState:false stops
113
+ // per-tick re-renders - position lives only in drag.positionRef/onMove (DragBox is this shape)
114
+
115
+ const r = useReorder({order, commit, move?, canDrag?, preview?, holdMs?}) // mini reorder-by-drag
116
+ <div ref={r.listRef}>{order.map(k => { // children 1:1 with order
117
+ const it = r.item(k) // it.dragging / it.active
118
+ return <div key={k} {...it.props} style={{...own, ...it.style}} /> // ONE commit(next) on drop
119
+ })}</div>
120
+
121
+ const b = useReorderBoard({columns: [{key, items}], commit, // drag between vertical columns
122
+ canDrag?, holdMs?, onDragStart?, onDragMove?, onOverChange?, onDragEnd?})
123
+ <div ref={b.columnRef('todo')}>{items.map(k => ...b.item(k)...)}</div> // one div per column, any count
124
+ b.over // {col, index} | null - live target
125
+ // column gravity is YOUR CSS: justify-content flex-start packs up, flex-end packs down
126
+
127
+ <FloatingWindow keyForSave="tool" size={{width: 320, height: 240}} header={<div>Tool</div>}
128
+ onClickClose={() => setOpen(false)}> {/* onCLickClose (typo) = deprecated alias */}
129
+ <Panel />
130
+ </FloatingWindow>
131
+
132
+ const wnd = useFloatingWindowController({keyForSave: "custom-tool", size: {width: 320, height: 240}})
133
+ wnd.position // {x,y}; bind wnd.onHeaderMouseDown/onHeaderTouchStart for custom chrome
134
+ ```
135
+
136
+ ## Modal / Input
137
+ ```
138
+ <ModalProvider>
139
+ <App />
140
+ </ModalProvider>
141
+
142
+ const modal = useModal()
143
+ modal.open(<Dialog />)
144
+ modal.set(<Dialog />)
145
+ modal.close()
146
+ modal(null) // callable shortcut for clearing
147
+ ```
148
+
149
+ Input helpers:
150
+ ```
151
+ const text = useTextInputPanel({callback: txt => {}, txt: ""}) // headless value + submit API
152
+ const file = useFileInputPanel({callback: file => {}}) // headless file + submit API
153
+ <TextInputPanel callback={txt => {}} name="Name" txt="" /> // thin visual wrapper over hook
154
+ <FileInputPanel callback={file => {}} name="File" /> // thin visual wrapper over hook
155
+ <TextInputModal callback={...} outClick={modal.close} />
156
+ <FileInputModal callback={...} outClick={modal.close} />
157
+ <FreeModal outClick={modal.close} size={{width: 400, height: 260}}>...</FreeModal>
158
+ ```
159
+
160
+ ## Settings Dialog
161
+ ```
162
+ <SettingsDialog trigger={<span>settings</span>} sections={[{key, name, render, children?, parentKey?, searchText?, keywords?}]} defaultSection? /> // searchable tree + persisted search history; history closes when search focus leaves
163
+ const settings = useSettingsDialogController({sections, defaultSection?}) // open/search/tree/history/resize actions for custom chrome
164
+ registerSettingsSection({key, name, render, parentKey?, searchText?, keywords?}) -> unregister
165
+ ```
166
+
167
+ ## UI Slot
168
+ ```
169
+ const slot = createUiSlot({key, places: {top: "Top bar", side: "Sidebar"}, def: "top"})
170
+
171
+ <slot.Slot place="top">{content}</slot.Slot> // each mount point decides by itself
172
+ <slot.PlacementSetting /> // segmented place switcher; setPlace marks memoryCache dirty
173
+ ```
174
+
175
+ ## Toolbar
176
+ ```
177
+ const tb = createToolbar({key, items: [{key, title, icon?, short?, render?, onClick?, defaultVisible?, fixed?}], def?, settingsItem?, resetItem?, source?, sourceMode?})
178
+
179
+ <tb.Bar settings reset? /> // the live bar; settings adds a gear; reset is hidden by default; item moves animate
180
+ <tb.Settings /> // the PURE config editor - same element drops into a settings section
181
+ tb.api.useConfig() / getConfig() / setConfig(next) / setOrder(order) / show(key,on) / setDensity(key) / reset()
182
+ tb.api.showSettings(on) / showReset(on) // pseudo-controls visibility
183
+ tb.api.useItems() // headless bar: ordered visible [{item, density, content}] - custom markup
184
+ tb.api.onChange.on(cfg => ...) -> off // fires on every edit
185
+ tb.api.dispose() // release the external-source subscription (HMR/remounts); persisted config stays
186
+
187
+ registerToolbarDensity({key, name, renderItem?}) -> unregister // built-ins: 'icon', 'label'
188
+ toolbarItemIcon(item) // the item's icon, or its first letters as a text pseudo-icon
189
+ ```
190
+ Config `{order, visible, density}` is serializable and persisted like createUiSlot
191
+ (memoryGetOrCreate -> memoryCache, the app owns the write policy). Items added in an app update are merged
192
+ into a stale persisted config (appended, default-visible), removed ones are ignored. `icon` is
193
+ optional: in icon density an icon-less item renders the first letters of its short/title.
194
+ The gear and reset button are pseudo-items: `settingsItem: {title?, icon?}` and `resetItem` re-skin them,
195
+ and the editor has separated toggle rows for them (`visible['__settings']` / `visible['__reset']`, no order slots - they always sit at the bar edge). Settings is visible by default; reset is hidden by default unless `resetItem.defaultVisible: true` or `api.showReset(true)` enables it. `resetItem: false` removes the reset feature.
196
+
197
+ `source?` makes the toolbar a VIEW over an external `UiListSource`. Default `sourceMode:"orderVisible"`
198
+ means the source owns item order and item visibility, so one config can drive a grid, its icon menu
199
+ AND the toolbar together (`columnState.api.listSource`, QA card 31). `sourceMode:"order"` shares only
200
+ the source-key order; item membership, density, pseudo-controls, and extra non-source item positions
201
+ stay in the toolbar store (QA card 30). Without `source` the toolbar keeps its own store (standalone mode).
202
+
203
+ ## Callback Hub
204
+ ```
205
+ const hub = createCallbackHub<[Args]>(emit => api.onX(emit)) // one slot -> many subscribers
206
+ hub.on(cb) -> off
207
+ ```
208
+
209
+ ## Params
210
+ ```
211
+ <ParamsEditor
212
+ params={params}
213
+ onChange={next => setParams(next)}
214
+ onExpand={next => setParams(next)}
215
+ expandStatus?
216
+ expandStatusLvl?
217
+ />
218
+
219
+ const controller = useParamsEditorController({params, onChange, onExpand?})
220
+
221
+ <ParamsEdit params={params} onSave={next => save(next)} />
222
+ <ParamRow name="Name">...</ParamRow>
223
+ ```
224
+
225
+ ## Menu
226
+ ```
227
+ type MenuItem = { name, actionKey?, onClick?, next?, func?, status? } | false | null | undefined
228
+
229
+ <Menu data={items} coordinate={{x: 0, y: 0}} />
230
+
231
+ <contextMenu.Layer>{children}</contextMenu.Layer>
232
+ contextMenu.openAt(event, [{name: "Copy", actionKey: "grid.copy", onClick: copy}], {source: "grid"})
233
+ contextMenu.stats.getSnapshot() // local counters: opens, source/layer usage, actionTotals, keyed actions
234
+ contextMenu.close()
235
+ ```
236
+
237
+ Use `contextMenu.openAt(e, items, {source?})` for new right-click integrations. `contextMenu.map` still exists as a legacy Layer queue, but it is not the recommended path for new code. `contextMenu.stats` is an in-memory diagnostics API; it never persists data, never reports over the network, and records per-action counters only for explicit `actionKey` values. Unkeyed items are counted in aggregate totals without storing labels.
238
+
239
+ `Menu` keeps the hovered/open item in internal React state. `status` is only an initial open hint and a compatibility field passed to custom renderers; menu objects are not mutated.
240
+
241
+ Floating right menu:
242
+ ```
243
+ <DropdownMenu elements={items} trigger={iconOrRender} classNames={...} styles={...} />
244
+ const menu = useRightMenuController({elements, keyForSave?})
245
+ ```
246
+
247
+ `DropdownMenu` is a floating action menu with caller-owned trigger/content styling, not the main context-menu primitive. `useRightMenuController` exposes the same open/fixed/select/submenu/drag state for custom views; `DropdownMenu` remains the compatible visual wrapper.
248
+
249
+ ## agGrid4
250
+ Plain declarative rows:
251
+ ```
252
+ <AgGridTable<Row> rowData={rows} columnDefs={cols} />
253
+ ```
254
+
255
+ Buffered mirror table:
256
+ ```
257
+ const grid = useAgGrid<Row>({getId: row => row.id})
258
+ <AgGridTable<Row> controller={grid} columnDefs={cols} />
259
+
260
+ grid.update({newData: rows})
261
+ grid.remove([{id}])
262
+ grid.clean()
263
+ grid.fit()
264
+ grid.flush()
265
+ ```
266
+
267
+ Shared module store:
268
+ ```
269
+ export const mainTable = createGridBuffer<Row>({getId, externalBuffer})
270
+
271
+ const grid = useAgGrid({core: mainTable})
272
+ <AgGridTable<Row> controller={grid} columnDefs={cols} />
273
+ ```
274
+
275
+ Overlay patches over React-owned `rowData`:
276
+ ```
277
+ const core = createGridBuffer<Row>({getId, mode: "overlay", pushDefaults: {add: false}})
278
+ const grid = useAgGrid({core})
279
+
280
+ <AgGridTable<Row> controller={grid} rowData={rows} columnDefs={cols} getRowId={p => getId(p.data)} />
281
+ ```
282
+
283
+ Dynamic columns are pure names + lifecycle replay:
284
+ ```
285
+ const columns = createColumnBuffer<Row>()
286
+
287
+ columns.api.setNames(["a", "b"])
288
+ columns.control.attach(api, {
289
+ apply: ({api, names}) => api.setGridOption("columnDefs", buildColumnDefs(names)),
290
+ })
291
+ columns.api.apply()
292
+ columns.control.detach()
293
+ ```
294
+
295
+ `createColumnBuffer()` knows nothing about groups, `columnDefs`, business names, or where columns live.
296
+
297
+
298
+ ## columnState
299
+ A persisted column layer over a keyed column set - order / visibility / width / sort / filter in a
300
+ standalone config store, with an OPTIONAL two-way ag-grid adapter. Mobile card views consume the
301
+ SAME config with no ag-grid at all. agGrid4 wrappers are never modified: this is the app-level
302
+ column wrapper `WRAPPER.md` postulates, packaged as a primitive.
303
+ ```
304
+ const cs = createColumnState({key, columns: [{key, title, short?, icon?, group?, fixed?, defaultVisible?, cardRole?}], def?, saveMs?})
305
+
306
+ cs.columns // the descriptors (UI renders from these + config)
307
+ cs.api.useConfig() / getConfig() / setConfig(next) / reset()
308
+ cs.api.show(key, on) / move(order) / setSort({key, dir} | null) / toggleSort(key) // asc->desc->off
309
+ cs.api.visibleKeys() // keys to render, in order (group-gated)
310
+ cs.api.onChange.on(cfg => ...) -> off
311
+ cs.api.usePresent() / isPresent(key) / setPresent(keys | null) // live-grid presence
312
+ cs.api.getPresentGate() / setPresentGate(keys | null) // app runtime availability gate, not persisted
313
+ cs.api.listSource // {order, visible} slice as a Toolbar `source`
314
+ ```
315
+ Config `{v, order, visible, width, sort, filter, groups}` is serializable and persisted like
316
+ createToolbar (memoryGetOrCreate(key) -> memoryCache, the app owns the write policy); a stale config migrates
317
+ softly (unknown keys dropped, new columns appended default-visible, `fixed` pinned). `sort` is
318
+ STICKY: independent of visibility/selection, may point at a hidden or grid-absent column, changes
319
+ only by an explicit toggle/header click.
320
+
321
+ Two-way ag-grid adapter (opt-in, agGrid4 untouched):
322
+ ```
323
+ <AgGridTable columnDefs={cols} autoSizeColumns={false}
324
+ onGridReady={e => cs.grid.attach(e.api)} // restore saved layout, then watch the grid
325
+ onGridPreDestroyed={() => cs.grid.detach()} /> // config survives remounts
326
+ ```
327
+ Grid drag/resize/hide/sort/filter fold back into the config; UI edits apply to the grid; a column
328
+ removed from the live grid (dynamic defs) keeps its config entry and reads `isPresent==false`.
329
+
330
+ Icon menu - a 1:1 grid mirror (reorder in the grid <-> reorder the buttons):
331
+ ```
332
+ <ColumnsMenu state={cs} compact? marks? tail? onItem? onTail? reorder? /> // default click = toggle visibility
333
+ <MenuStrip items={[{key, title, state:'on'|'off'|'disabled', marks?, fixed?}]} onItem? onMove? tail? compact? />
334
+ ```
335
+ `ColumnsMenu` binds `MenuStrip` - a presentation-only layer that reports clicks/drags but never
336
+ interprets them - to the config. `compact` = icon-only buttons; an item with no icon shows its
337
+ first letters. `tail` = trailing non-column buttons (a table-standards cycler etc.). For new
338
+ settings-integrated toolbar/grid surfaces prefer `createToolbar({source: cs.api.listSource})`;
339
+ keep `ColumnsMenu` for compact button strips or custom presentation-only menus.
340
+
341
+ Mobile (no ag-grid): dots selector + card rows over the same config:
342
+ ```
343
+ <ColumnDots state={cs} max?=8 /> // track of marks; dots = shown columns; tap empty=add,
344
+ // drag=replace, swipe up=remove, tap dot=select, sort button cycles
345
+ <CardList<Row> state={cs} data={rows} getId? renderValue? /> // visible cols -> card fields; cardRole:'title'/'accent'
346
+ ```
347
+
348
+ Default wrapper for repeated grid/menu/mobile use:
349
+ ```
350
+ const cg = createColumnGrid<Row>({
351
+ key: "orders.columns",
352
+ columnDefs: cols, // ColumnMeta inferred from colId/field/headerName
353
+ data: rows, // optional default; View props can override
354
+ getId: r => r.id,
355
+ autoSizeOnColumnCountChange: true, // optional fit when visible column count changes
356
+ columns: [{key: "name", fixed: true, cardRole: "title"}], // optional overrides
357
+ })
358
+
359
+ <cg.View mode="table" /> // default controls="auto" -> dots overlay
360
+ <cg.View mode="cards" data={otherRows} />
361
+ <cg.Table rowData={rows} getRowId={p => p.data.id} />
362
+ <cg.Menu compact />
363
+ <cg.Dots /> // no wrapper max; defaults to all columns
364
+ <cg.Toolbar settings />
365
+ <cg.Settings />
366
+ ```
367
+ `createColumnGrid` returns `{state, toolbar, api, grid, tableProps, Table, Menu, Dots, Cards, Toolbar, Settings, View, dispose}`; `dispose()` releases factory-lifetime subscriptions (config onChange, toolbar source, pending fit) without touching the persisted config. The columnState BARREL stays ag-grid-free; `createColumnGrid` ships from its own module (both are exported from the package root).
368
+ Its `Table`/`tableProps()` attach/detach the columnState grid adapter automatically and default
369
+ `autoSizeColumns=false` so restored widths are not overwritten. `autoSizeOnColumnCountChange` is
370
+ separate and only calls `sizeColumnsToFit()` when the visible column count changes. `useColumnGrid(opts)`
371
+ is the same controller captured once for component-local setups; keep module-level `createColumnGrid`
372
+ when the controller must survive route/component remounts.
373
+
374
+ QA cards 28 (grid layer + F5 restore), 29 (mobile dots + cards), 30 (toolbar icon menu + grouped sub-column mode button), 31 (Toolbar over the same config), 32 (createColumnGrid wrapper + dots driving table/cards).
375
+
376
+
377
+ ## Observe React Adapter
378
+ `wenay-common2` owns the store/listen/RPC primitives. `wenay-react2` owns React lifecycle hooks around them.
379
+
380
+ ```ts
381
+ import { Observe } from "wenay-common2"
382
+ import { useStoreMirror, useStoreNode, useStoreKeys, useStoreSelect, useStoreChangedPaths, useListenEffect, useListenValue } from "wenay-react2"
383
+ ```
384
+
385
+ Node subscription:
386
+ ```
387
+ const price = useStoreNode(store.node.data.BTC)
388
+ price.value
389
+ price.exists
390
+ price.replace(123)
391
+ price.refresh()
392
+ ```
393
+
394
+ Selection subscription:
395
+ ```
396
+ const sel = useStoreSelect(store.update({data: {BTC: true}, meta: {status: true}}))
397
+ sel.value
398
+ sel.get()
399
+ ```
400
+
401
+ Network mirror is explicit:
402
+ ```
403
+ const mirror = useStoreMirror(remoteStore, {data: {}, meta: {}}, {
404
+ mask: {data: {BTC: true}, meta: {status: true}},
405
+ current: true,
406
+ drain: 250,
407
+ })
408
+
409
+ mirror.value
410
+ mirror.ready
411
+ mirror.sync()
412
+ mirror.stop()
413
+ ```
414
+
415
+ Per-key feed (`store.each()` of common2 — one cb per CHANGED top-level key):
416
+ ```
417
+ useStoreEach(store, (key, value, ctx) => {
418
+ value === undefined ? removeRow(key) : upsertRow(key, value)
419
+ }, {enabled?})
420
+ ```
421
+ `undefined` = key deleted; a root replace (store.replace / mirror keyframe) expands into one call per key. The fold target should live outside React state (ref/Map/grid api) — the hook renders nothing itself. The wire counterpart is `useStoreReplayEach` below.
422
+
423
+ Listen helpers:
424
+ ```
425
+ useListenEffect(listen, (...args) => {})
426
+ const value = useListenValue(listen, {initial})
427
+ const args = useListenArgs(listen)
428
+ ```
429
+
430
+ The hook does not choose transport. `remoteStore` needs `{ get(mask?), changed }` and may also provide `changedPaths`; apps may implement it with RPC, WebSocket, SSE, or test HTTP. `changedPaths` is used as a transport optimization: mirror pulls `mask ∩ paths` instead of the whole mask. `initial` is only the local mirror seed; changing it does not reset an existing mirror. `mask` is treated as a small declarative StoreMask, so structurally equal inline masks do not resubscribe. For add/delete keys, subscribe to the parent object node with `useStoreKeys(node)`; this also covers deep objects. If a state key conflicts with StoreNode methods such as `count`, use `store.node.at("count")`.
431
+
432
+ ## Replay React Adapter
433
+ Client-side hooks over the wenay-common2 Replay stack (snapshot + sequenced delta line). Server parts (`conflateReplay`, `archiveReplay`, `createRpcServerAuto` replayOpts) are per-connection and stay hook-free by design.
434
+
435
+ ```ts
436
+ import { useReplaySubscribe, useReplayRouteSubscribe, useStoreReplaySync, useStoreReplayMirror, useStoreReplayRouteSync, useStoreReplayRouteMirror, useStoreReplayEach, useReplayFrame, useReplayHistory } from "wenay-react2"
437
+
438
+ // any replay line ({line, since, keyframe, frame?, frameLine?} remote)
439
+ const sub = useReplaySubscribe(remote, (frame) => draw(frame), {onSeq?, onError?, since?, enabled?, staleMs?, onStale?, policy?, hint?})
440
+ sub.ready; sub.error; sub.seq(); sub.restart()
441
+ sub.stale; sub.lastTs() // freshness (needs staleMs): stale re-renders on fresh<->stale transitions ONLY; lastTs() is a getter like seq()
442
+ // policy: 'queue' (default, nothing skipped) | 'frame' (rides the server frameLine when present: on lag the
443
+ // server drops and recovers with a mini-frame; old servers/in-proc degrade to 'queue'). hint -> the line's frame condenser.
444
+
445
+ // explicit route hand-off over the same logical replay line (relay <-> direct, etc.)
446
+ const routed = useReplayRouteSubscribe(relayRemote, (frame) => draw(frame), {label: "relay", onRoute?})
447
+ await routed.switchRoute(directRemote, {label: "direct"})
448
+ routed.ready; routed.switching; routed.route; routed.seq(); routed.label(); routed.active()
449
+ // route hooks do not expose stale/lastTs yet; use non-route hooks when freshness is required.
450
+ // store patch line (Observe.exposeStoreReplay(...).api.replay)
451
+ const mirror = useStoreReplayMirror(remote, initial, {enabled?}) // creates the mirror store; same staleMs/stale/policy surface
452
+ mirror.store; mirror.ready; mirror.seq(); mirror.restart()
453
+ const sync = useStoreReplaySync(existingStore, remote) // same, store supplied by the app
454
+ const routedSync = useStoreReplayRouteSync(existingStore, relayRemote, {label: "relay"}) // same store, switchRoute() replaces the route after catch-up
455
+ const routedMirror = useStoreReplayRouteMirror(relayRemote, initial, {label: "relay"}) // mirror store + route hand-off controller
456
+
457
+ // per-key fold over the same line (Observe.syncStoreReplayEach counterpart): cb per CHANGED top-level key,
458
+ // first delivery = keyframe expanded per key, (key, undefined) = deleted — grid rows without special cases
459
+ const feed = useStoreReplayEach<Rows>(remote, (key, row) => row === undefined ? removeRow(key) : upsertRow(key, row), {drain?, initial?})
460
+ feed.store; feed.ready; feed.stale; feed.seq(); feed.restart() // same controller surface as the mirror
461
+ // the mirror store lives in a ref: in-mount resubscribes (StrictMode/restart/enabled) reconnect by tail ON TOP of
462
+ // kept state — the consumer sees only the diff. After a full unmount: {since: prev.seq(), initial: prev.store.snapshot()}
463
+
464
+ // pull at YOUR pace (frame model): timer around remote.frame(seq, hint) — server condenses, no backlog, no live socket sub
465
+ const pf = useReplayFrame(remote, (quote) => apply(quote), {intervalMs: 500, hint?})
466
+ pf.ready; pf.error; pf.seq(); pf.pull(); pf.restart() // error (e.g. sacred line evicted) STOPS pulling until restart()
467
+
468
+ // time machine over Replay.openHistory(storage, live?)
469
+ const tt = useReplayHistory(history, (frame) => draw(frame), {head: () => replay.head()})
470
+ tt.live; tt.seq; tt.head; tt.pause(); tt.play(); tt.seek({seq})
471
+ ```
472
+
473
+ Route hand-off here is the MANUAL surface (`switchRoute`). common2 1.0.67 `Replay.createRouteCoordinator` moves route decisions (policy, promote/fallback/shadow) out of the consumer; a coordinator `link.subscribe(cb)` handle has the same shape (`ready, seq(), label(), active()`) and survives every route change, so components consuming a coordinator link do not need `switchRoute` at all. common2 1.0.68 supplies the direct transport for it: `createWebRtcConnector` with an app-injected `rtc: () => new RTCPeerConnection(cfg)` factory (the browser — i.e. the React app — owns that injection) and signaling over the existing RPC socket (`createSignalHub`). common2 1.0.69 wraps the whole stack into the `Peer` SDK (`wenay-common2/peer`): `createPeerClient(...).peer(account)` returns a live mirrored store (works with `useStoreNode`/`useStoreKeys` as-is) + route control, surviving relay<->direct hand-offs in one seq space. A dedicated `usePeer` adapter is a target-backlog item, not yet a library surface.
474
+
475
+ Media lines (common2 1.0.66 `Media.createAudioSource` / `Media.createVideoSource`) are ordinary binary Listen sources; with `replay:true` their `listen` is a replay line, so `useReplaySubscribe` / `useReplayFrame` consume mic/camera frames with no media-specific hook. Each frame is one `Uint8Array` (`Media.decodeMediaFrame`); draw/play it via ref (canvas, AudioContext), never useState — the same rule as any high-frequency line. Without `replay`, the plain `listen` works with the listen hooks above.
476
+
477
+ Contract: `off()` on unmount, StrictMode-safe; seq survives resubscribes inside one mount (keepSeq, default on) — a resubscribe reconnects with `{since}` and gets the journal tail, not a keyframe. Across a FULL unmount/remount keep the position outside via `onSeq` and pass it back as `since`. `seq()` is a getter — high-frequency lines (video frames, ticks) do not re-render per event; draw to canvas via ref, bypassing VDOM. Freshness: detection lives in wenay-common2 (`staleMs` watchdog); the non-route hooks mirror its edge-triggered `onStale` into `stale`, so a fresh 100 ev/s line causes zero extra renders. Route hand-off is explicit through `switchRoute(nextRemote, {label?, since?, reset?, policy?, hint?})`: old route stays live while the replacement catches up by `seq`, then closes. `useReplayHistory` is archive playback — staleness does not apply. QA cards 23 (video line + conflation + time travel + freshness), 24 (store sync), 25 (per-key feed), and 26 (route hand-off) are the live examples.
478
+
479
+ ## Logs
480
+ ```
481
+ logsApi.addLogs({id: "api", time: new Date(), txt: "done", var: 1})
482
+ <logsApi.React.Message />
483
+ <logsApi.React.PageLogs />
484
+ <logsApi.React.Setting />
485
+
486
+ const customLogs = getLogsApi<MyFields>({limitPer: 500, limit?: 50, varMin?: 0, settingsKey?: "myLogs"})
487
+ // each getLogsApi call now owns ISOLATED full/mini/settings state (settingsKey persists the
488
+ // instance settings; omitted = fresh non-persisted). The global logsApi keeps the legacy shared state.
489
+ const headless = createLogsController<MyFields>({options: {limitPer: 500, limit: 50}})
490
+
491
+ const messageNotifications = useMessageEventLogsController({maxVisible: 4})
492
+ <MessageEventLogsView controller={messageNotifications} zIndex={80} />
493
+
494
+ const contextTable = useLogsTableController()
495
+ const contextNotifications = useLogsNotificationsController()
496
+
497
+ const mini = useMiniLogsTable({data: rows, onClick: e => console.log(e.data)})
498
+ <AgGridTable {...mini.props} />
499
+ <MiniLogsTable data={rows} />
500
+
501
+ const table = useLogsPageTable() // full-page logs grid controller
502
+ <AgGridTable {...table.gridProps} />
503
+ table.fit(); table.applyImportanceFilter(min?); table.appendRow(row); table.getApi()
504
+ ```
505
+
506
+ Logger notification/tabs chrome uses `--logs-*` CSS variables; apps can theme it without forking logger components. `createLogsController` owns the headless append/limit/settings state for custom integrations. `useMessageEventLogsController` owns the global notification queue/timers/settings, `MessageEventLogsView` draws it, and `MessageEventLogs` / `logsApi.React.Message` remain compatibility wrappers. `MiniLogs` remains the compatibility wrapper; new compact-table code can use `useMiniLogsTable` or `MiniLogsTable`. `useLogsPageTable` owns the full-page table (mount-time row snapshot, transaction appends from the mini feed, one importance-filter method); `PageLogs` / `logsApi.React.PageLogs` remain the thin compatibility wrappers over it.
507
+
508
+ ## Styles / Theme
509
+ ```
510
+ tokens.color.bgDark
511
+ tokens.menu.outlineColor
512
+ tokens.logs.notificationAccent
513
+ tokens.zIndex.modal
514
+
515
+ GridStyleDefault() // inject legacy grid CSS vars/classes
516
+ StyleGridDefault // common ag-grid style object
517
+ buildAgTheme("dark" | "light")
518
+ useAgGridTheme("dark" | "light")
519
+ colDefCentered / colDefWrap / numericComparator
520
+ ```
521
+
522
+ Shared CSS variables include `--menu-outline-color`, `--logs-*`, `--dlg-*`, `--wnd-*`, `--tb-*`, `--cols-grid-*`, `--cols-menu-*`, `--cols-dots-*`, and `--cols-card-*`.
523
+
524
+ ## Charts
525
+ ```
526
+ <MyChartEngine style={{height: 400}} />
527
+ createChartCanvas(config) -> canvas controller
528
+ ```
529
+
530
+ The chart engine surface is still mostly low-level. Keep product-specific chart wrappers in the app.
531
+
532
+ ## QA Stand
533
+ ```
534
+ npm run testReact // http://localhost:3010/
535
+ ```
536
+
537
+ The stand lives in `src/common/testUseReact/qa.tsx`. Use it for visual checks; agGrid4 overlay/dynamic-column demos are dedicated QA cards.
538
+ Two tabs: Active checks (current work) and Verified archive (`#archive`) - verified/fixed cards move
539
+ there and stay runnable for regression re-checks.
@@ -18,6 +18,7 @@ export * from "./src/components/Dnd";
18
18
  export * from "./src/utils";
19
19
  export * from "./src/grid/agGrid4";
20
20
  export * from "./src/grid/columnState";
21
+ export * from "./src/grid/columnState/columnGrid";
21
22
  export * from "./src/components/Buttons";
22
23
  export * from "./src/components/MyResizeObserver";
23
24
  export * from "./src/components/Other";
package/lib/common/api.js CHANGED
@@ -15,17 +15,21 @@ export * from "./src/styles/styleGrid";
15
15
  export * from "./src/styles/tokens";
16
16
  // 1. BASE LAYER - no internal project dependencies
17
17
  export * from "./updateBy";
18
- // 2. HOOKS - depend only on updateBy
18
+ // 2. HOOKS - depend on updateBy and wenay-common2
19
19
  export * from "./src/hooks";
20
- // 4. DND components - depend on updateBy
20
+ // 4. DND components - depend on updateBy and utils (persistedMaps)
21
21
  export * from "./src/components/Dnd";
22
- // 5. UTILS - now depend on Dnd (memoryStore imports floatingWindowMap, mapResiReact)
22
+ // 5. UTILS - self-contained (persisted maps live in utils/persistedMaps; components import
23
+ // their map from there, so utils no longer reaches into the component layer)
23
24
  export * from "./src/utils";
24
25
  // 5b. GRID (agGrid4): core buffer + headless hook + AgGridTable.
25
26
  export * from "./src/grid/agGrid4";
26
27
  // 5c. GRID column state (createColumnState): persisted order/visibility/width/sort/filter,
27
- // standalone config store + optional two-way ag-grid adapter (attach via onGridReady)
28
+ // standalone config store + optional two-way ag-grid adapter (attach via onGridReady).
29
+ // The columnState barrel is deliberately ag-grid-free; createColumnGrid (ag-grid runtime +
30
+ // Toolbar) ships from its own module so grid-less consumers can import the barrel directly.
28
31
  export * from "./src/grid/columnState";
32
+ export * from "./src/grid/columnState/columnGrid";
29
33
  // 6. BASE COMPONENTS - depend on hooks
30
34
  export * from "./src/components/Buttons";
31
35
  export * from "./src/components/MyResizeObserver";
@@ -1,3 +1,8 @@
1
+ /** @deprecated No consumers in the library; prefer `useDraggableApi` (delta-based, mouse+touch)
2
+ * or `DragBox` for the imperative onX/onY shape. Kept as-is because its semantics are unique
3
+ * (document.body listeners, stopPropagation on every move tick, ABSOLUTE x/y coordinates,
4
+ * no preventDefault) - re-basing it on useDraggableApi would change observable behavior.
5
+ * Candidate for removal in a deliberate breaking version (A7/A3 cleanup inventory). */
1
6
  export declare function DragArea({ children, onY, onX, x, y, last, onStart, onStop, }: {
2
7
  children: React.JSX.Element;
3
8
  onX?: (x: number) => void;
@@ -1,6 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- //
3
+ /** @deprecated No consumers in the library; prefer `useDraggableApi` (delta-based, mouse+touch)
4
+ * or `DragBox` for the imperative onX/onY shape. Kept as-is because its semantics are unique
5
+ * (document.body listeners, stopPropagation on every move tick, ABSOLUTE x/y coordinates,
6
+ * no preventDefault) - re-basing it on useDraggableApi would change observable behavior.
7
+ * Candidate for removal in a deliberate breaking version (A7/A3 cleanup inventory). */
4
8
  export function DragArea({ children, onY, onX, x = 0, y = 0, last, onStart, onStop, }) {
5
9
  const lastC = useRef(null);
6
10
  const lastT = useRef(null);
@@ -1,6 +1,6 @@
1
1
  import React, { ReactNode } from "react";
2
2
  import { type RndResizeCallback } from "react-rnd";
3
- import { ObservableMap } from "../../utils/observableMap";
3
+ import { floatingWindowMap } from "../../utils/persistedMaps";
4
4
  export type FloatingWindowPosition = {
5
5
  x: number;
6
6
  y: number;
@@ -31,6 +31,9 @@ export type FloatingWindowProps = {
31
31
  position?: FloatingWindowPosition;
32
32
  size?: FloatingWindowSize;
33
33
  moveOnlyHeader?: boolean;
34
+ /** Show the close button and handle its click. */
35
+ onClickClose?: () => void;
36
+ /** @deprecated typo alias of {@link onClickClose}; kept for compatibility, `onClickClose` wins when both are set. */
34
37
  onCLickClose?: () => void;
35
38
  header?: React.ReactElement | boolean;
36
39
  overflow?: boolean;
@@ -48,7 +51,7 @@ export type FloatingWindowProps = {
48
51
  children: React.ReactElement | ((update: number) => React.ReactElement);
49
52
  className?: string;
50
53
  };
51
- export type FloatingWindowControllerOptions = Omit<FloatingWindowProps, "children" | "className" | "header" | "moveOnlyHeader" | "overflow" | "onCLickClose">;
54
+ export type FloatingWindowControllerOptions = Omit<FloatingWindowProps, "children" | "className" | "header" | "moveOnlyHeader" | "overflow" | "onCLickClose" | "onClickClose">;
52
55
  export type FloatingWindowController = {
53
56
  position: FloatingWindowPosition;
54
57
  size: FloatingWindowSize;
@@ -64,14 +67,14 @@ export type FloatingWindowController = {
64
67
  onResize: RndResizeCallback;
65
68
  onResizeStop: RndResizeCallback;
66
69
  };
67
- export declare const floatingWindowMap: ObservableMap<string, FloatingWindowSavedGeometry>;
70
+ export { floatingWindowMap };
68
71
  export declare const FloatingWindow: typeof FloatingWindowBase;
69
72
  export declare function useFloatingWindowController({ keyForSave: ks, position, size, zIndex, onUpdate, limit, sizeByWindow }?: FloatingWindowControllerOptions): FloatingWindowController;
70
73
  /**
71
74
  * Wrapper component around react-rnd.
72
75
  * Provides dragging and resizing, an optional header, and a close button.
73
76
  */
74
- export declare function FloatingWindowBase({ children, keyForSave: ks, position, size, overflow, zIndex, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, sizeByWindow }: FloatingWindowProps): import("react/jsx-runtime").JSX.Element;
77
+ export declare function FloatingWindowBase({ children, keyForSave: ks, position, size, overflow, zIndex, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, onClickClose, sizeByWindow }: FloatingWindowProps): import("react/jsx-runtime").JSX.Element;
75
78
  export type DragBoxProps = {
76
79
  /** Child element that should be draggable */
77
80
  children: ReactNode;
@@ -106,4 +109,5 @@ export type DragBoxProps = {
106
109
  * Function only as a hook for parameter changes during movement, although it has its own component (for offset counting).
107
110
  * Returns the distance traveled when moving the child element.
108
111
  */
109
- export declare function DragBox({ children, onX, onY, x, y, right, last, dragging, onStart, onStop }: DragBoxProps): import("react/jsx-runtime").JSX.Element;
112
+ export declare function DragBox({ children, onX, onY, x, y, right, last, dragging: _dragging, // accepted for compatibility, was never read
113
+ onStart, onStop }: DragBoxProps): import("react/jsx-runtime").JSX.Element;