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.
- package/README.md +17 -15
- package/doc/EXAMPLE_USAGE.md +969 -969
- package/doc/PROJECT_FUNCTIONALITY.md +403 -403
- package/doc/PROJECT_RULES.md +27 -27
- package/doc/WENAY_REACT2_RENAMES.md +170 -170
- package/doc/changes/v1.0.35.md +18 -18
- package/doc/changes/v1.0.38.md +37 -37
- package/doc/changes/v1.0.39.md +23 -23
- package/doc/changes/v1.0.40.md +14 -14
- package/doc/changes/v1.0.41.md +35 -35
- package/doc/changes/v1.0.42.md +26 -26
- package/doc/changes/v1.0.43.md +10 -10
- package/doc/changes/v1.0.44.md +14 -14
- package/doc/changes/v1.0.45.md +8 -8
- package/doc/changes/v1.0.46.md +11 -11
- package/doc/changes/v1.0.47.md +9 -0
- package/doc/changes/v1.0.48.md +8 -0
- package/doc/examples/README.md +4 -4
- package/doc/examples/peer-call-media.tsx +7 -7
- package/doc/examples/stand.tsx +5 -5
- package/doc/native.md +37 -0
- package/doc/progress/README.md +13 -13
- package/doc/progress/architecture-fix-queue.md +74 -74
- package/doc/progress/column-state-present-gate.md +28 -28
- package/doc/progress/common2-adoption-1.0.73.md +28 -28
- package/doc/progress/common2-adoption-1.0.74.md +24 -24
- package/doc/progress/hook-controller-opportunities.md +363 -363
- package/doc/progress/hook-extraction-audit.md +195 -195
- package/doc/progress/public-surface-normalization.md +351 -351
- package/doc/progress/qa-stand-walkthrough-2026-07-12.md +62 -0
- package/doc/progress/stand-as-examples-audit.md +20 -20
- package/doc/progress/style-system-normalization.md +121 -121
- package/doc/target/README.md +32 -32
- package/doc/target/my.md +122 -122
- package/doc/wenay-react2-rare.md +807 -807
- package/doc/wenay-react2.md +541 -541
- package/lib/common/demo/peerMedia.js +6 -1
- package/lib/common/src/logs/logsController.js +2 -2
- package/lib/common/testUseReact/qa.js +3 -3
- package/lib/native/columnDots.d.ts +41 -0
- package/lib/native/columnDots.js +104 -0
- package/lib/native/columnState.d.ts +69 -0
- package/lib/native/columnState.js +209 -0
- package/lib/native/index.d.ts +3 -0
- package/lib/native/index.js +3 -0
- package/lib/style/menuRight.css +19 -19
- package/lib/style/style.css +23 -23
- package/lib/style/tokens.css +184 -184
- package/package.json +2 -1
- package/doc/wenay-react2-1.0.8.tgz +0 -0
package/doc/wenay-react2-rare.md
CHANGED
|
@@ -1,807 +1,807 @@
|
|
|
1
|
-
# wenay-react2 - EXTENDED / rare surface
|
|
2
|
-
|
|
3
|
-
> Everyday API lives in **`wenay-react2.md`**.
|
|
4
|
-
> This file lists low-level primitives and migration notes.
|
|
5
|
-
> Root import: `import { ... } from "wenay-react2"`.
|
|
6
|
-
|
|
7
|
-
## Migration Rule
|
|
8
|
-
```
|
|
9
|
-
New code teaches and imports the short canonical surface:
|
|
10
|
-
useModal(), useOutside(), useDraggableApi(), useReorder(), useReorderBoard(),
|
|
11
|
-
useAgGrid(), createGridBuffer(), createColumnBuffer(), createUiSlot(), createToolbar()
|
|
12
|
-
|
|
13
|
-
Old names are recorded only in `WENAY_REACT2_RENAMES.md`; do not export new compatibility aliases casually. Existing public APIs should usually remain supported during internal refactors; removals require an explicit migration cut with a separate task, migration notes, changelog, and usage signal. Default style contracts are stricter: do not remove old default classes/styles until the replacement class/token path is shipped and documented.
|
|
14
|
-
Do not add new examples with Get*, FuncJSX, *2/*3, or business-specific helper names.
|
|
15
|
-
If a primitive needs app policy, build a small app wrapper above it.
|
|
16
|
-
If behavior is reusable, expose it first as a headless use*/create* API. The hook/controller may return methods, getters, props/bind helpers, or a small API object; visual components and QA cards should consume that surface instead of becoming the only implementation.
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Canonical method vocabulary:
|
|
20
|
-
```
|
|
21
|
-
open / close / set / replace // modal-like state
|
|
22
|
-
update / remove / clean / sync // data-buffer state
|
|
23
|
-
fit / flush // ag-grid visual/transaction lifecycle
|
|
24
|
-
props / bind // spreadable element props
|
|
25
|
-
get / set / reset / cancel // local hook/controller state
|
|
26
|
-
on -> off // subscriptions
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Root Namespaces
|
|
30
|
-
```
|
|
31
|
-
import { kit } from "wenay-react2"
|
|
32
|
-
|
|
33
|
-
kit.hooks
|
|
34
|
-
kit.dnd
|
|
35
|
-
kit.utils
|
|
36
|
-
kit.grid
|
|
37
|
-
kit.modal
|
|
38
|
-
kit.menu
|
|
39
|
-
kit.logs
|
|
40
|
-
kit.updateBy
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
The root export is still flat. `kit` is useful when a large file needs grouped names.
|
|
44
|
-
|
|
45
|
-
## Modal Low Level
|
|
46
|
-
```
|
|
47
|
-
useModal() // callable controller with show/open/close/set
|
|
48
|
-
createModalElementStore() // imperative JSX store; prefer ModalProvider/useModal
|
|
49
|
-
createModalRenderStore() // function JSX store; prefer ModalProvider/useModal
|
|
50
|
-
inputModal({modal, func, name?, txt?})
|
|
51
|
-
confirmModal({modal, func, password?})
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
`inputModal` and `confirmModal` accept either a setter function or the `useModal()` controller.
|
|
55
|
-
|
|
56
|
-
Internal Overlay (A9, 2026-07-10): `components/Overlay.tsx` is the ONE portal+scrim+outside-click+
|
|
57
|
-
Escape composition; `ModalProvider` and `SettingsDialog` are adapters over it (behavior 1:1,
|
|
58
|
-
SettingsDialog keeps its own two-stage Escape by NOT passing `onEscape`). It is deliberately not
|
|
59
|
-
exported: apps use ModalProvider/useModal/SettingsDialog. The render-slot stores above, the
|
|
60
|
-
LeftModal drawer (gesture-driven, scrim-less), and `ModalWrapper`/Input helpers (backdrop-less,
|
|
61
|
-
hosted inside another modal slot) are NOT overlays and stay separate. This leaf is also where all
|
|
62
|
-
scrim/portal DOM lives - the seam for a future react-native view layer.
|
|
63
|
-
|
|
64
|
-
Left-side modal/menu helpers:
|
|
65
|
-
```
|
|
66
|
-
LeftModal({arr, zIndex})
|
|
67
|
-
ApiLeftMenu
|
|
68
|
-
getApiLeftMenu()
|
|
69
|
-
TestLeft333()
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
These are app-shell style utilities. Prefer local app wrappers for new layouts.
|
|
73
|
-
|
|
74
|
-
## Settings Dialog / UI Slot / Callback Hub Details
|
|
75
|
-
Settings dialog (centered panel ~720x480, max 92vw/82vh; searchable settings tree left, content right):
|
|
76
|
-
```
|
|
77
|
-
type SettingsSection = {key: string, name: string, render: () => ReactNode, children?, parentKey?, searchText?, keywords?}
|
|
78
|
-
|
|
79
|
-
<SettingsDialog
|
|
80
|
-
trigger={...} // wrapper span is clickable
|
|
81
|
-
sections? // static sections, listed first; children form tree nodes
|
|
82
|
-
defaultSection? // falls back to the first section when missing/unmounted
|
|
83
|
-
sectionClassName? // apps pass their own .chip; default .wenayDlgSection
|
|
84
|
-
sectionActiveClassName? // apps pass their own .chipActive; default .wenayDlgSectionActive
|
|
85
|
-
/>
|
|
86
|
-
|
|
87
|
-
useSettingsDialogController({sections?, defaultSection?, sectionClassName?, sectionActiveClassName?})
|
|
88
|
-
registerSettingsSection(s) -> unregister
|
|
89
|
-
getSettingsSections() -> readonly SettingsSection[]
|
|
90
|
-
```
|
|
91
|
-
The registry is a module singleton on updateBy/renderBy (no React context). Registering the same
|
|
92
|
-
`key` replaces the previous section; unregister removes by identity, so a stale unregister after a
|
|
93
|
-
replacement is a no-op. Tree shape comes from nested `children` or flat sections with `parentKey`.
|
|
94
|
-
Search matches section labels, `keywords`, `searchText`, and best-effort text extracted from rendered
|
|
95
|
-
React children; matching descendants stay visible and their ancestors auto-expand. The search input uses
|
|
96
|
-
`createSearchHistory({key:"SettingsDialog.searchHistory"})`: Enter commits the current query, the history
|
|
97
|
-
button recalls stored queries, leaving the search box closes the list, and changes are published through memoryProps -> memoryCache. `useSettingsDialogController` exposes the same open/search/tree/history/nav-resize actions for custom settings chrome. Tree controls
|
|
98
|
-
are one compact dotted cycle button in the search row (expanded -> current branch -> collapsed), hidden when
|
|
99
|
-
the tree has too little hierarchy to need it. Closes on the x, a scrim click, and Escape.
|
|
100
|
-
Styling: `--dlg-scrim / bg / border / radius / shadow / nav-bg / nav-width` tokens (tokens.css,
|
|
101
|
-
mirror `tokens.dlg`), classes `.wenayDlgScrim / .wenayDlg / .wenayDlgNav / .wenayDlgSearch /
|
|
102
|
-
.wenayDlgTree / .wenayDlgContent / .wenayDlgSearchHistory` in style.css; dark defaults, apps re-skin
|
|
103
|
-
via `:root[data-theme]` like `--wnd-*`.
|
|
104
|
-
UI slot:
|
|
105
|
-
```
|
|
106
|
-
createUiSlot({key, places, def}) -> {Slot, PlacementSetting, getPlace, setPlace}
|
|
107
|
-
<slot.PlacementSetting className? activeClassName? /> // defaults .wenaySegBtn / .wenaySegBtnActive
|
|
108
|
-
```
|
|
109
|
-
State lives in `memoryGetOrCreate(key)` -> persisted with the rest of memoryProps; `setPlace`
|
|
110
|
-
announces the in-place mutation via `memoryMarkDirty(key)` and the APP decides when to save
|
|
111
|
-
via `memoryCache.onDirty` (the library never writes storage itself, same as window state).
|
|
112
|
-
A stored place that no longer exists in `places` falls back to `def`. `getPlace()` is not reactive;
|
|
113
|
-
Slot/PlacementSetting subscribe internally via updateBy.
|
|
114
|
-
|
|
115
|
-
Toolbar (createToolbar, `components/Toolbar/Toolbar.tsx`):
|
|
116
|
-
```
|
|
117
|
-
createToolbar({key, items, def?, settingsItem?, resetItem?, source?, sourceMode?}) -> {Bar, Settings, api: {useConfig, useItems, getConfig, setConfig, setOrder, show, setDensity, showSettings, showReset, reset, onChange}}
|
|
118
|
-
<tb.Bar className? settings? reset? popAlign? /> // default classes .wenayTb / .wenayTbItem; popAlign
|
|
119
|
-
// 'right' (default, top-right bars) | 'left'
|
|
120
|
-
<tb.Settings className? activeClassName? /> // density segments default .wenaySegBtn(Active)
|
|
121
|
-
registerToolbarDensity({key, name, renderItem?}) / getToolbarDensities()
|
|
122
|
-
toolbarItemIcon(item) -> ReactNode // item.icon, or first letters of short/title
|
|
123
|
-
```
|
|
124
|
-
Three decoupled layers: serializable config `{order, visible, density}` (single source of truth,
|
|
125
|
-
persisted exactly like createUiSlot: memoryGetOrCreate(key), edits mutate in place + renderBy +
|
|
126
|
-
memoryMarkDirty, the APP saves via memoryCache.onDirty), Bar (visible!==false, in order, at density) and
|
|
127
|
-
a PURE Settings editor over config - the same element works in the Bar's gear popover and in a
|
|
128
|
-
registered settings section, no prop changes. Semantics that are easy to get wrong:
|
|
129
|
-
- Merge of stale persist: unknown keys in stored order/visible are ignored (view-level; raw
|
|
130
|
-
storage is untouched until the next setConfig), items missing from the config are APPENDED
|
|
131
|
-
default-visible - an app update never wipes the user's order/visibility.
|
|
132
|
-
- `fixed` items: always visible, pinned at normalize() to their index in opts.items (checkbox
|
|
133
|
-
disabled, not draggable) - and the drag PREVIEW already respects the pinning, so they never
|
|
134
|
-
move visually and a drop never lands elsewhere than shown.
|
|
135
|
-
- Density is an extensible module registry (like registerSettingsSection): entries may carry
|
|
136
|
-
`renderItem(item)`; item.render(density) always wins; entries without renderItem fall back to
|
|
137
|
-
icon + (short ?? title). A persisted density that is no longer registered falls back to
|
|
138
|
-
def/first. Bar/Settings subscribe to the registry (updateBy), so registering a level updates
|
|
139
|
-
live.
|
|
140
|
-
- Reorder rides the library's own `useReorder` (see Drag / Resize Low Level; this editor is its
|
|
141
|
-
first consumer): the WHOLE row drags, mouse+touch, checkbox excluded, and `move` is the SAME
|
|
142
|
-
simulated commit as normalize() (splice + fixed pinning), so preview and drop agree by
|
|
143
|
-
construction. Neighbours glide via transitioned transforms (`.wenayTbRowShift`), ONE setConfig
|
|
144
|
-
on drop. Plus arrow keys on the focused handle (the drag hook preventDefaults mousedown, so the
|
|
145
|
-
row handler focuses the handle itself). No dnd dependency; FloatingWindow/react-rnd deliberately not
|
|
146
|
-
used (free-floating windows, wrong tool). `touch-action: none` + `user-select: none` on
|
|
147
|
-
draggable rows (`.wenayTbRowGrab`).
|
|
148
|
-
- `api.onChange` is a wenay-common2 `listen` stream; emits the NORMALIZED config after every
|
|
149
|
-
config edit. useConfig subscribes via updateBy; getConfig is a non-reactive snapshot.
|
|
150
|
-
`setOrder(order)` is the focused external-control path for grid/menu reorder sync: it preserves
|
|
151
|
-
current membership, density, and pseudo-control visibility while changing only item order. `show(key,on)`,
|
|
152
|
-
`setDensity(key)`, `showSettings(on)`, and `showReset(on)` are the matching narrow writes; `setConfig` remains the low-level full write.
|
|
153
|
-
- The gear and reset buttons are pseudo-items: reserved visible keys `__settings` and `__reset`,
|
|
154
|
-
NO order slots (they always sit at the bar edge), toggled from separated rows at the bottom of
|
|
155
|
-
the editor (`.wenayTbRowMeta`, outside the drag-slot container on purpose). Hiding the gear in
|
|
156
|
-
its own popover closes the popover - the global settings section is the way back. The reset
|
|
157
|
-
button calls `api.reset()` and restores defaults for order, membership, density and pseudo-control
|
|
158
|
-
visibility. Settings is default-visible; reset is default-hidden unless `resetItem.defaultVisible=true` or `api.showReset(true)` enables it. `resetItem: false` removes that feature. Faces via `settingsItem` / `resetItem`.
|
|
159
|
-
- **The Settings CONTAINER is the client's choice (not the core's).** `tb.Settings` is
|
|
160
|
-
presentation-agnostic - render it anywhere. `<Bar settings>`'s gear opens it in a small inline
|
|
161
|
-
popover anchored to the gear; since the bar reflows when you toggle membership, that popover
|
|
162
|
-
shifts a little (known minor twitch). The core deliberately ships ONLY that inline popover -
|
|
163
|
-
anything richer is a client layer. For a stable, movable editor render `tb.Settings` in your own
|
|
164
|
-
container: a registered settings section (`registerSettingsSection`), or a draggable window -
|
|
165
|
-
`FloatingWindow` (drag by header, close X, viewport-clamped, position persisted via `keyForSave`)
|
|
166
|
-
wrapped in `OutsideClickArea` for close-on-outside-click. Recommended pattern for clients who
|
|
167
|
-
want the modal-with-title-bar-and-close look; QA card 30 demonstrates it (gear -> Settings in a
|
|
168
|
-
FloatingWindow, in the card's own positioned layer so it scrolls with the card). This is separate
|
|
169
|
-
from the row REORDER above, which stays on `useReorder` (a floating window would be the wrong tool
|
|
170
|
-
there).
|
|
171
|
-
- `api.useItems()` is the headless bar: ordered, visibility-filtered `[{item, density, content}]`
|
|
172
|
-
for fully custom markup. Refs-out was rejected deliberately: order lives in the config, so the
|
|
173
|
-
consumer re-renders from this list - the library never re-parents foreign DOM nodes.
|
|
174
|
-
- `icon` is OPTIONAL: `toolbarItemIcon(item)` returns the icon, else the first 3 letters of
|
|
175
|
-
short/title as a text pseudo-icon (icon density only - the label density shows the caption, not
|
|
176
|
-
the letters). Exported so menus (ColumnsMenu compact) share the exact rule.
|
|
177
|
-
- `source?: UiListSource` inverts ownership. Default `sourceMode:"orderVisible"` makes the toolbar a
|
|
178
|
-
VIEW over source order+visibility: setConfig writes item order/visibility THERE (its own change
|
|
179
|
-
flow re-emits `api.onChange`; the toolbar emits itself only if the source has no `onChange`), while
|
|
180
|
-
density and the gear/reset flags stay in the toolbar's own `st` under `key`. `sourceMode:"order"`
|
|
181
|
-
shares only the relative order of keys present in the source; item membership, density,
|
|
182
|
-
pseudo-controls, and extra non-source item positions stay local. On source reorders, source keys
|
|
183
|
-
refill their slots in the local toolbar order, so an extra item like QA card 30's `blockMode` is
|
|
184
|
-
not pushed into `columnState` and does not need a bridge hook. `ext` is per-instance constant, so
|
|
185
|
-
hook-call order in useConfig/Bar/Settings never changes. `columnState.api.listSource` is the
|
|
186
|
-
reference implementer (QA card 31 for order+visibility, QA card 30 for order-only). Without a source
|
|
187
|
-
- v1 non-goals: no overflow/"more" menu (hook point: Bar, after the visible-items map), no
|
|
188
|
-
grouping, no cross-bar drag.
|
|
189
|
-
Styling: `--tb-*` tokens (tokens.css, mirror `tokens.tb`), classes `.wenayTb*` in style.css;
|
|
190
|
-
dark defaults, apps re-skin via `:root[data-theme]` like `--wnd-*` / `--dlg-*`.
|
|
191
|
-
|
|
192
|
-
Callback hub (for single-slot callback APIs `onX(cb | null)` whose subscribers silently
|
|
193
|
-
overwrite each other):
|
|
194
|
-
```
|
|
195
|
-
createCallbackHub<Args>(bind) -> {on, count}
|
|
196
|
-
```
|
|
197
|
-
`bind(emit)` runs lazily, ONCE, on the first `on()` - not at creation time, because the slot may
|
|
198
|
-
still be taken by app initialization. The first callback is registered before bind runs, so it
|
|
199
|
-
also catches synchronous emits. `on(cb) -> off`; off removes only that subscriber.
|
|
200
|
-
|
|
201
|
-
## columnState (createColumnState, `grid/columnState/`)
|
|
202
|
-
A persisted column layer over a keyed column set - order/visibility/width/sort/filter in a
|
|
203
|
-
standalone config store, plus an OPTIONAL two-way ag-grid adapter. Mobile card views consume the
|
|
204
|
-
SAME config with no ag-grid at all. agGrid4 is never modified: this is exactly the app-level
|
|
205
|
-
column wrapper WRAPPER.md postulates, packaged as a primitive; ag-grid enters only as a type
|
|
206
|
-
import + the GridApi handed to grid.attach().
|
|
207
|
-
```
|
|
208
|
-
createColumnState({key, columns: ColumnMeta[], def?, saveMs?=300})
|
|
209
|
-
-> {columns, api, grid: {attach(api), detach()}}
|
|
210
|
-
ColumnMeta = {key, title, short?, icon?, group?, fixed?, defaultVisible?, cardRole?: 'title'|'accent'}
|
|
211
|
-
ColumnsConfig= {v, order, visible, width, sort: {key, dir} | null, filter, groups}
|
|
212
|
-
api: {getConfig, setConfig, useConfig, onChange, reset, show(k,on), move(order), setSort(s|null),
|
|
213
|
-
toggleSort(k), visibleKeys(), getPresent, usePresent, isPresent(k), setPresent(keys|null), getPresentGate, setPresentGate(keys|null), listSource}
|
|
214
|
-
```
|
|
215
|
-
Persist + migration (same DNA as createToolbar/createUiSlot): config lives in `memoryGetOrCreate(key)`,
|
|
216
|
-
edits mutate it in place + renderBy + memoryMarkDirty, the APP saves via `memoryCache.onDirty`.
|
|
217
|
-
`normalize()` is the soft migration - unknown keys dropped, missing columns appended
|
|
218
|
-
default-visible, `fixed` pinned to its descriptor index; `v` covers incompatible shape changes.
|
|
219
|
-
The persisted object's IDENTITY is the subscription key, so `memoryCache.load()` must run BEFORE the grid
|
|
220
|
-
mounts (a grid attached pre-load would apply defaults; QA card 28 gates the grid on `loaded`).
|
|
221
|
-
|
|
222
|
-
Semantics that are easy to get wrong:
|
|
223
|
-
- STICKY sort: `sort` is independent of visibility and of any UI selection, may point at a hidden
|
|
224
|
-
OR grid-absent column, and changes only by an explicit toggle/header click. `readFromGrid` keeps
|
|
225
|
-
it when the live grid lacks that column (the grid cannot express a sort by a column it does not
|
|
226
|
-
have), so hiding or removing the sorted column never silently resets it.
|
|
227
|
-
- Two-way loop protection: store->grid runs under an `applying` flag; grid->store ignores events
|
|
228
|
-
while `applying`, with `source=='api'`, or `finished===false` (resize/move fire per animation
|
|
229
|
-
frame - only the final shape persists), debounced `saveMs`, and a JSON compare before commit. A
|
|
230
|
-
`fixed` column dragged off its slot in the grid: readFromGrid commits, normalize() pins it back,
|
|
231
|
-
and if the order changed the adapter re-applies to the grid (snap-back) so the two never disagree.
|
|
232
|
-
- Presence (`usePresent/isPresent/setPresent/setPresentGate`): runtime-only, NEVER persisted. It is
|
|
233
|
-
the intersection of actual live-grid columns and an optional app-level gate. The adapter maintains
|
|
234
|
-
actual presence on attach and on `gridColumnsChanged`; a column removed from the grid (dynamic
|
|
235
|
-
columnDefs) keeps its config entry and its menu button (rendered disabled), and when the set changes
|
|
236
|
-
the adapter RE-IMPOSES the config so a returning column regains its stored order/width/visibility
|
|
237
|
-
(setting columnDefs resets grid order). `setPresentGate(keys|null)` is for stable-schema app modes:
|
|
238
|
-
it marks gated-out buttons disabled and hides those grid columns through applyColumnState without
|
|
239
|
-
mutating persisted `visible`. No loop: applyColumnState never adds/removes columns. Null actual presence
|
|
240
|
-
and null gate together mean all present; if a gate is set, grid-less consumers still see that gate.
|
|
241
|
-
- `listSource` = the `{order, visible}` slice exposed as a `UiListSource` (Toolbar's
|
|
242
|
-
external-config contract): plug it into `createToolbar({source})` and the toolbar, the icon menu
|
|
243
|
-
and the grid all mirror one config. Its setConfig MERGES visible and re-appends via normalize, so
|
|
244
|
-
an editor over a SUBSET of columns never drops the rest.
|
|
245
|
-
- `filter`/`width` are written by the grid adapter only; `groups` (group key -> enabled
|
|
246
|
-
sub-columns) is modelled now; core group UI is still app-owned. QA card 30 demonstrates an app-level
|
|
247
|
-
mode button over stable grouped `columnDefs`; `presentGate` hides unavailable leaf columns and keeps their buttons disabled.
|
|
248
|
-
`visibleKeys()` additionally gates a grouped column by its group's enabled set.
|
|
249
|
-
- Attach from the consumer's `onGridReady` (AgGridTable forwards it over its own wiring) with
|
|
250
|
-
`autoSizeColumns={false}` (auto-fit at mount would rewrite stored widths); detach from
|
|
251
|
-
`onGridPreDestroyed` - the config outlives the grid (columnBuffer pattern). HMR caveat on the
|
|
252
|
-
stand: hot-reload recreates a module-level controller while the live grid stays attached to the
|
|
253
|
-
OLD instance, so store->grid needs an F5; in a real app the module runs once.
|
|
254
|
-
|
|
255
|
-
High-level kit (`createColumnGrid`, `grid/columnState/columnGrid.tsx`):
|
|
256
|
-
```
|
|
257
|
-
createColumnGrid({key, columnDefs?, columns?, data?, getId?, def?, saveMs?, toolbar?, autoSizeOnColumnCountChange?})
|
|
258
|
-
-> {state, toolbar, api, grid, tableProps, Table, Menu, Dots, Cards, Toolbar, Settings, View}
|
|
259
|
-
useColumnGrid(opts) -> same controller, captured once for component-local setups
|
|
260
|
-
```
|
|
261
|
-
- This is the default convenience layer for ordinary app grids that all need the same column menu.
|
|
262
|
-
It infers `ColumnMeta` from leaf `ColDef`s (`colId` -> `field`, title from `headerName` -> key)
|
|
263
|
-
and merges optional `columns` overrides for `fixed`, `short`, `icon`, `defaultVisible`, `cardRole`,
|
|
264
|
-
and group semantics. Unknown explicit overrides are appended, so grid-less/card-only fields still work.
|
|
265
|
-
- `Table` and `tableProps()` wrap `AgGridTable` and wire `state.grid.attach/detach` automatically.
|
|
266
|
-
Their default `autoSizeColumns=false` is deliberate: the wrapper is for persisted column layout, so
|
|
267
|
-
auto-fit must be opt-in. `autoSizeOnColumnCountChange` is a narrower opt-in: it calls
|
|
268
|
-
`sizeColumnsToFit()` only when `visibleKeys().length` changes after a columnState edit.
|
|
269
|
-
- `View` is a small representation switch (`mode:'table'|'cards'`) with controls
|
|
270
|
-
(`'menu'|'dots'|'toolbar'|false|'auto'`). `auto` resolves to the built-in dots overlay for both
|
|
271
|
-
table and card mode; the wrapper sets dot max to the full column count unless overridden. `Dots` is
|
|
272
|
-
not card-specific; on table mode it edits the same config and the attached grid applies visibility,
|
|
273
|
-
order, and sort through columnState. `data`/`getId` can live on the controller defaults or be
|
|
274
|
-
passed per `View` render.
|
|
275
|
-
- Use raw `createColumnState` plus `createToolbar` when the app needs a custom skin like QA card 30,
|
|
276
|
-
a runtime `presentGate`, special group mode buttons, or nonstandard persistence timing.
|
|
277
|
-
|
|
278
|
-
Icon menu (ColumnsMenu / MenuStrip, `grid/columnState/ColumnsMenu.tsx`):
|
|
279
|
-
```
|
|
280
|
-
<MenuStrip items={MenuStripItem[]} onItem? onMove? move? tail? holdMs?=150 compact? />
|
|
281
|
-
MenuStripItem = {key, title, short?, icon?, state: 'on'|'off'|'disabled', marks?, fixed?}
|
|
282
|
-
<ColumnsMenu state onItem? marks? tail? onTail? reorder?=true holdMs? compact? />
|
|
283
|
-
```
|
|
284
|
-
Two DECOUPLED layers, because "what a click means" is deliberately not the strip's business:
|
|
285
|
-
- `MenuStrip` is pure presentation - ordered buttons in three states plus a `marks` adornment
|
|
286
|
-
("on with extras": sub-columns, naming strategies...); it reports clicks (onItem) and
|
|
287
|
-
drag-reorders (onMove) but interprets neither. Reusable for ANY multi-state button strip.
|
|
288
|
-
`disabled` buttons report no clicks; `fixed` ones do not drag; `tail` = buttons after a divider,
|
|
289
|
-
OUTSIDE the reorder list (mode cyclers/actions). Reorder rides `useReorder` (second consumer
|
|
290
|
-
after the Toolbar editor) with the same fixed-pinning `move`, so preview == drop.
|
|
291
|
-
- `ColumnsMenu` binds it to a columnState: buttons follow config.order (the grid mirror is free -
|
|
292
|
-
both read the same config), state = disabled (absent from the live grid) / on / off, default
|
|
293
|
-
click = toggle visibility (overridable via `onItem` for multi-state columns), drag = api.move.
|
|
294
|
-
It is the compact/presentation surface. For settings-integrated bars that need density,
|
|
295
|
-
pseudo-controls, reset, and global Settings reuse, use `createToolbar({source: cs.api.listSource})`
|
|
296
|
-
as the canonical richer surface and optionally render a compact `ColumnsMenu` beside it.
|
|
297
|
-
- Click-vs-drag guard: a drag that ends on its origin button still fires a browser click; a click
|
|
298
|
-
that travelled >4px from mousedown is dropped, so a snapped-back drag never also toggles.
|
|
299
|
-
- `compact` = icon-only buttons: the icon, or the first letters of short/title as a text
|
|
300
|
-
pseudo-icon (full title in the tooltip), the same rule as `toolbarItemIcon`.
|
|
301
|
-
|
|
302
|
-
Mobile (no ag-grid, no storage - the config alone):
|
|
303
|
-
```
|
|
304
|
-
<ColumnDots state max?=8 className? style? /> // grid/columnState/ColumnDots.tsx
|
|
305
|
-
<CardList<Row> state data getId? renderValue? /> // grid/columnState/CardList.tsx
|
|
306
|
-
```
|
|
307
|
-
- `ColumnDots` is a discrete multi-thumb slider on pointer events (react-range cannot change thumb
|
|
308
|
-
count): a track of marks (one per column), dots on the shown columns. Gestures use a
|
|
309
|
-
dominant-axis test so a horizontal drag never removes and a vertical flick never reorders: drag a
|
|
310
|
-
dot along the track = the column takes another (empty) mark; swipe a dot UP = hide (fixed and the
|
|
311
|
-
last remaining dot stay); tap an empty mark = show (up to `max`); tap a dot without moving =
|
|
312
|
-
select the field; the sort button cycles asc->desc->off on the SELECTED field (sticky - selecting
|
|
313
|
-
another dot does not touch it). 44px touch targets.
|
|
314
|
-
- `CardList` renders rows as blocks: `visibleKeys()` become the card fields (created/removed live
|
|
315
|
-
as dots move), `cardRole:'title'` = the header (else the first visible key), `cardRole:'accent'`
|
|
316
|
-
= a badge, the rest are label+value. It sorts by the sticky `config.sort` itself
|
|
317
|
-
(numeric/locale comparator) even when the sorted column is hidden.
|
|
318
|
-
Styling uses shared classes `.wenayColDots*` / `.wenayCardList*`; runtime geometry remains inline because dot positions and drag transforms are state-derived. QA cards 28 (grid layer + F5 restore), 29 (mobile dots + cards), 30 (toolbar icon menu + grouped sub-column mode button), 31 (Toolbar over columnState.listSource + pseudo-icons), 32 (createColumnGrid wrapper + dots driving table/cards).
|
|
319
|
-
|
|
320
|
-
## Outside / Buttons Compatibility
|
|
321
|
-
```
|
|
322
|
-
useOutsideRef(options) -> ref // use useOutside(options).ref / .props
|
|
323
|
-
OutsideClickArea // alias of OutsideClickArea
|
|
324
|
-
|
|
325
|
-
StyleOtherRow
|
|
326
|
-
StyleOtherColumn
|
|
327
|
-
```
|
|
328
|
-
|
|
329
|
-
`Button`, `OutsideButton`, `HoverButton`, and `AbsoluteButton` are still direct components rather than hook controllers.
|
|
330
|
-
|
|
331
|
-
`Button` persists its open/closed status under `keyForSave` (module-lifetime; same naming as
|
|
332
|
-
FloatingWindow/Resizable/RightMenu); the old `keySave` prop is a deprecated alias (`keyForSave`
|
|
333
|
-
wins when both are set).
|
|
334
|
-
|
|
335
|
-
## Drag / Resize Low Level
|
|
336
|
-
```
|
|
337
|
-
FloatingWindowBase(props) // lower-level react-rnd wrapper
|
|
338
|
-
FloatingWindow(props) // canonical floating window component
|
|
339
|
-
useFloatingWindowController(options) // headless geometry/stack/drag/resize controller for custom chrome
|
|
340
|
-
floatingWindowMap // persisted RND map (ObservableMap)
|
|
341
|
-
FloatingWindowUpdate
|
|
342
|
-
FloatingWindowProps / FloatingWindowController / FloatingWindowSavedGeometry
|
|
343
|
-
|
|
344
|
-
DragBox(props) // delta-drag component; thin adapter over useDraggableApi (A7)
|
|
345
|
-
DragArea(props) // @deprecated: unique semantics kept as-is; prefer useDraggableApi/DragBox
|
|
346
|
-
FResizableReact(props)
|
|
347
|
-
mapResiReact // persisted resize map (ObservableMap)
|
|
348
|
-
OutlineDragDemo()
|
|
349
|
-
```
|
|
350
|
-
|
|
351
|
-
For new pointer logic, prefer:
|
|
352
|
-
```
|
|
353
|
-
const drag = useDraggableApi(...)
|
|
354
|
-
<div {...drag.bind} />
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
`useDraggable(...)` is kept as the simple hook wrapper; `useDraggableApi(...)` is the controller-style shape.
|
|
358
|
-
|
|
359
|
-
Drag primitives after the A7 pass (2026-07-10): `DragBox` is a thin adapter over
|
|
360
|
-
`useDraggableApi({holdMs: 0, trackState: false, onMove})` - same observable contract as its old
|
|
361
|
-
bespoke loop (immediate start, per-tick imperative `onX`/`onY` with the delta from the press
|
|
362
|
-
point, no re-render per move tick, `onStop` only after a real gesture, `last` ref shares the live
|
|
363
|
-
position object; pinned by `__test/dragBox.test.tsx`, QA card 35). `DragArea` stays untouched and
|
|
364
|
-
`@deprecated`: no consumers, and its semantics are unique (document.body listeners,
|
|
365
|
-
`stopPropagation` per move tick, ABSOLUTE coords, no preventDefault) - re-basing it would change
|
|
366
|
-
observable behavior; removal only in a breaking version. `useFloatingWindowController`'s header
|
|
367
|
-
loop and `StickerMenu` deliberately keep their own loops: viewport clamp + `e.buttons===1` release
|
|
368
|
-
recovery + persistence/z-stack, and mount-lifetime listeners + click-vs-drag suppression
|
|
369
|
-
respectively, are load-bearing behavior `useDraggableApi` does not model.
|
|
370
|
-
|
|
371
|
-
Reorder-by-drag (useReorder, `hooks/useReorder.tsx` - extracted from the Toolbar editor, which is
|
|
372
|
-
its first consumer):
|
|
373
|
-
```
|
|
374
|
-
useReorder({order, commit, move?, canDrag?, preview?: 'slots'|'measure', holdMs?})
|
|
375
|
-
-> {listRef, item(key) -> {props, style?, dragging, active}, dragKey, preview}
|
|
376
|
-
```
|
|
377
|
-
A deliberately SMALL reorder for keyed blocks laid out by CSS - the hook never knows the layout
|
|
378
|
-
(vertical list, horizontal bar, wrapped grid all work). Semantics:
|
|
379
|
-
- DOM order never changes mid-drag: the dragged block follows the pointer via transform, the rest
|
|
380
|
-
glide to their preview position (consumer adds the transition on `active && !dragging`), ONE
|
|
381
|
-
`commit(next)` on drop, skipped when nothing moved (a plain click is a no-op).
|
|
382
|
-
- Targeting = nearest slot center measured at drag START - never against the live layout
|
|
383
|
-
(re-measuring moving blocks oscillates at boundaries; designed out). Pointer deltas (viewport
|
|
384
|
-
px) are normalized by the container's scale factor (rect.width / offsetWidth), so
|
|
385
|
-
`transform: scale` / zoomed ancestors do not skew targeting.
|
|
386
|
-
- `move` is the simulated commit: the preview shows exactly `move(order, key, to)` - consumers
|
|
387
|
-
with pinning rules (Toolbar's fixed items) pass their own so preview == drop by construction.
|
|
388
|
-
- preview 'slots' (default) transforms between start slot centers - exact when blocks are
|
|
389
|
-
equal-sized. 'measure' is FLIP: the preview order is applied via CSS `order`, layout offsets
|
|
390
|
-
(offsetLeft/Top) are read and reverted in one synchronous pass (never paints), so wrapping with
|
|
391
|
-
ANY block sizes previews exactly; requires a flex/grid container; measured once per target
|
|
392
|
-
change. Offsets, NEVER getBoundingClientRect: rects include mid-flight TRANSITION values
|
|
393
|
-
(style.transform='none' does not stop a running transition within the same synchronous pass),
|
|
394
|
-
so rect-based re-measures accumulated the previous preview's shifts on every target change and
|
|
395
|
-
the blocks flew apart on target oscillation.
|
|
396
|
-
- Events from `input/button/select/textarea/a` never start a drag; mouse is left-button only;
|
|
397
|
-
touch works (`touch-action: none` on blocks is the consumer's CSS).
|
|
398
|
-
- Non-goals (use a ready-made dnd library instead): nesting, spans/collision packing,
|
|
399
|
-
autoscroll. QA card 26 is the live example. Cross-COLUMN moves live in useReorderBoard below.
|
|
400
|
-
|
|
401
|
-
Board (useReorderBoard, `hooks/useReorderBoard.tsx` - the columns extension of useReorder):
|
|
402
|
-
```
|
|
403
|
-
useReorderBoard({columns: [{key, items}], commit(next), canDrag?, holdMs?,
|
|
404
|
-
onDragStart?, onDragMove?, onOverChange?, onDragEnd?})
|
|
405
|
-
-> {columnRef(col) -> RefCallback, item(key), dragKey, over: {col, index} | null}
|
|
406
|
-
```
|
|
407
|
-
- Columns are plain consumer divs registered via `columnRef(key)` (live callback-ref registry -
|
|
408
|
-
ADDING a column is just consumer state + one more div, spliced at ANY position of the columns
|
|
409
|
-
array incl. the middle); a column's children must be exactly its items, 1:1, in order. The
|
|
410
|
-
column set/geometry is frozen for the duration of one drag.
|
|
411
|
-
- Column gravity is pure consumer CSS (`justify-content: flex-start` packs up, `flex-end` packs
|
|
412
|
-
down) - the hook never knows it, it measures the real layout. Do NOT use `flex-direction:
|
|
413
|
-
column-reverse` (it inverts DOM-vs-visual order; the 1:1 mapping breaks).
|
|
414
|
-
- Targeting: nearest column by clamped rect distance, insertion index = how many of that column's
|
|
415
|
-
START item centers sit above the dragged center (start geometry only - anti-oscillation, same
|
|
416
|
-
rule as useReorder). Preview = simulated commit (movedColumns), so preview == drop.
|
|
417
|
-
- Cross-column FLIP, offset-based like useReorder: the dragged block leaves flow via
|
|
418
|
-
display:none, survivors get preview CSS `order`, and the landing slot becomes a real
|
|
419
|
-
margin gap (draggedHeight + row-gap) on the neighbour - the column's own gravity then decides
|
|
420
|
-
who moves aside (a flex-end column slides the blocks ABOVE the slot up). Apply-read-revert in
|
|
421
|
-
one synchronous pass, cached per target slot.
|
|
422
|
-
- Callbacks: onDragStart (grab), onDragMove (every move, pointer delta in local px),
|
|
423
|
-
onOverChange (only when the target slot changes; compare prev.col != over.col for column
|
|
424
|
-
crossings), onDragEnd (final slot + committed flag; a plain click is committed=false).
|
|
425
|
-
`over`/`dragKey` are also returned reactively for render-time styling (column highlight).
|
|
426
|
-
- QA card 27 is the live example (5 columns, mixed gravity, empty column, add-column).
|
|
427
|
-
|
|
428
|
-
## Grid Row Utilities
|
|
429
|
-
```
|
|
430
|
-
applyGridRows(params)
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
Use `agGrid4` for new tables:
|
|
434
|
-
```
|
|
435
|
-
createGridBuffer()
|
|
436
|
-
useAgGrid()
|
|
437
|
-
AgGridTable
|
|
438
|
-
createColumnBuffer()
|
|
439
|
-
numericComparator()
|
|
440
|
-
colDefCentered
|
|
441
|
-
colDefWrap
|
|
442
|
-
```
|
|
443
|
-
|
|
444
|
-
Important agGrid4 contracts:
|
|
445
|
-
```
|
|
446
|
-
mirror mode: buffer owns row set; sync can add/update/remove grid rows.
|
|
447
|
-
overlay mode: rowData owns row set; sync updates only existing grid rows.
|
|
448
|
-
clean(): clears buffer; mirror also removes grid rows, overlay does not remove declarative rows.
|
|
449
|
-
flush(): React/controller method that calls ag-grid flushAsyncTransactions().
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
Plain declarative `rowData` should preserve AgGridReact defaults unless the caller provides `getRowId`.
|
|
453
|
-
Buffered paths need a stable `getId`.
|
|
454
|
-
|
|
455
|
-
Dynamic columns:
|
|
456
|
-
```
|
|
457
|
-
createColumnBuffer().api.setNames(names) // exact set, dedupe preserving order
|
|
458
|
-
createColumnBuffer().api.apply() // replay last attached apply callback
|
|
459
|
-
createColumnBuffer().control.attach(api, {apply})
|
|
460
|
-
createColumnBuffer().control.detach() // keep names
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
The primitive must not contain product group names, base `columnDefs`, visibility rules, or app naming policy.
|
|
464
|
-
|
|
465
|
-
## Params / Generated Editors
|
|
466
|
-
```
|
|
467
|
-
ParamsEditor({params, onChange, onExpand?, expandStatus?, expandStatusLvl?})
|
|
468
|
-
useParamsEditorController({params, onChange, onExpand?})
|
|
469
|
-
ParamsEdit({params, onSave}) // canonical compact editor
|
|
470
|
-
ParamsArrayEdit({params, onSave}) // array params editor
|
|
471
|
-
ParamRow({param, onClick, type?})
|
|
472
|
-
ParamLabelContent(name)
|
|
473
|
-
ParamToggleLabel(type, name)
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
`ParamsEditor` receives wenay-common2 `Params.IParamsExpandableReadonly` shape. `useParamsEditorController` owns the mutable draft clone, immediate/delayed notify, expand callback, and timer cleanup for custom renderers; `ParamsEditor` remains the compatibility visual wrapper.
|
|
477
|
-
|
|
478
|
-
## Menu Low Level
|
|
479
|
-
```
|
|
480
|
-
Menu({data, coordinate?, zIndex?, className?, menu?, menuElement?})
|
|
481
|
-
MenuElement
|
|
482
|
-
MenuProgress
|
|
483
|
-
MenuItemStrict / MenuItem
|
|
484
|
-
|
|
485
|
-
contextMenu.openAt(eventOrPoint, items, {source?, layerId?}) -> boolean
|
|
486
|
-
contextMenu.openAtPoint({x, y}, items, {source?, layerId?}) -> boolean
|
|
487
|
-
contextMenu.close()
|
|
488
|
-
contextMenu.getState() -> {open, items, point, source?, layerId?, seq}
|
|
489
|
-
contextMenu.stats.getSnapshot() -> {openAt, openAtPoint, legacyLayer, close, replace, empty, sources, layers, actionTotals, actions}
|
|
490
|
-
contextMenu.stats.reset()
|
|
491
|
-
contextMenu.stats.onChange(cb) -> off
|
|
492
|
-
<contextMenu.Layer zIndex? statusOn? other? className?>...</contextMenu.Layer>
|
|
493
|
-
contextMenu.map // legacy queue consumed by Layer; prefer openAt
|
|
494
|
-
createContextMenu({name?}) // custom isolated instance with its own state and stats
|
|
495
|
-
createRightClickMenu() // lower-level legacy right-click factory
|
|
496
|
-
|
|
497
|
-
DropdownMenu({elements, trigger?, classNames?, styles?, style?, position?, verticalPosition?, keyForSave?})
|
|
498
|
-
useRightMenuController({elements, style?, styles?, position?, verticalPosition?, keyForSave?})
|
|
499
|
-
createRightMenuController()
|
|
500
|
-
mapRightMenu // persisted floating-menu state (ObservableMap, RightMenuStore)
|
|
501
|
-
MenuRightPosition / MenuRightVerticalPosition / MenuRightSavedState / MenuRightRenderProps
|
|
502
|
-
MenuRightTrigger / MenuRightClassNames / MenuRightStyles / RightMenuController
|
|
503
|
-
StickerMenu // components/Menu re-export
|
|
504
|
-
```
|
|
505
|
-
|
|
506
|
-
Prefer `contextMenu.openAt(e, items, {source?})` for new right-click integrations. `contextMenu.map` remains for older callers that queue items before Layer handles the right-click, and stays supported through `1.x`; it should not be the primary API in new code. `contextMenu.stats` is local in-memory diagnostics, not hidden analytics: it counts direct `openAt`, `openAtPoint`, legacy Layer queued opens, empty opens, close/replace, source/layer usage, aggregate action outcomes, and keyed action outcomes. It deliberately does not persist, send network requests, or store arbitrary item labels. Per-action stats require explicit `MenuItemStrict.actionKey`; unkeyed actions update `actionTotals` only.
|
|
507
|
-
|
|
508
|
-
`Menu` does not mutate `item.status`. Open/hover state is an internal active index; `status` remains a seed/compatibility value, and custom item renderers receive a view item whose `status` mirrors the current open state.
|
|
509
|
-
|
|
510
|
-
`DropdownMenu` is a floating action menu, not the main context-menu primitive. Its trigger glyph/content and visual classes/styles are caller-owned through `trigger`, `classNames`, and `styles`; the default still renders the old hamburger glyph and CSS classes. `useRightMenuController` is the hook/controller layer behind it for custom views; do not duplicate hover/fixed/submenu timers in product code.
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
## Observe / Listen React Hooks
|
|
514
|
-
Canonical React adapter names:
|
|
515
|
-
```
|
|
516
|
-
useStoreNode(node, {mode?, fallback?, drain?, key?})
|
|
517
|
-
useStoreKeys(node, {fallback?, drain?, key?}) // object-shape changes: add/delete keys
|
|
518
|
-
useStoreChangedPaths(changedPathsListen, {initial?, key?})
|
|
519
|
-
useStoreEach(store, cb, {enabled?=true}) // store.each() wrapper: cb(key, value, ctx) per CHANGED top-level key; undefined = deleted; root replace expands per key; renders nothing itself
|
|
520
|
-
useStoreSelect(selection, {fallback?, drain?, key?})
|
|
521
|
-
useStoreMirror(remote, initial, {mask, current?=true, drain?, key?, auto?=true, partial?, onError?})
|
|
522
|
-
useListenEffect(listen, cb, {current?, key?})
|
|
523
|
-
useListenArgs(listen, {initial?, current?, key?})
|
|
524
|
-
useListenValue(listen, {initial?, current?, key?, map?})
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
These hooks are intentionally in `wenay-react2`, not in `wenay-common2`.
|
|
528
|
-
Do not add React hooks for every helper exposed by `wenay-common2`; read the installed common2 package docs when needed, and document only the React-facing contract here. Hooks are useful only around subscriptions, lifecycle, or external resources.
|
|
529
|
-
Network sync stays explicit through `useStoreMirror`; reading a node must not secretly start RPC/fetch/WebSocket work. When `remote.changedPaths` exists and `partial !== false`, mirror uses common2 partial sync; otherwise it falls back to `remote.changed -> get(mask)`. `initial` seeds the mirror only. Structurally equal inline masks are stable, because StoreMask is expected to be a small plain object/tree.
|
|
530
|
-
|
|
531
|
-
QA stand coverage:
|
|
532
|
-
```
|
|
533
|
-
/__qa/observe-store/get // HTTP snapshot/mask read
|
|
534
|
-
/__qa/observe-store/mutate // HTTP server mutation, including deep leaf and deep key add/delete
|
|
535
|
-
/__qa/observe-store/events // SSE changed stream
|
|
536
|
-
/__qa/observe-store/events-paths // SSE changedPaths stream
|
|
537
|
-
```
|
|
538
|
-
|
|
539
|
-
## Replay React Hooks (details)
|
|
540
|
-
`src/common/src/hooks/useReplay.ts`. Client side of the wenay-common2 Replay stack only.
|
|
541
|
-
```
|
|
542
|
-
useReplaySubscribe(remote, cb, {since?, keepSeq?=true, enabled?=true, onSeq?, onError?, staleMs?, onStale?, policy?, hint?})
|
|
543
|
-
-> {ready, error, stale, seq(), lastTs(), restart(since?)}
|
|
544
|
-
useReplayRouteSubscribe(remote, cb, {since?, keepSeq?=true, enabled?=true, label?, onSeq?, onError?, onRoute?, policy?, hint?})
|
|
545
|
-
-> {ready, error, route, switching, seq(), label(), active(), switchRoute(nextRemote, opts?)}
|
|
546
|
-
useStoreReplaySync(store, remote, sameOpts) -> same controller // Observe.syncStoreReplay wrapper
|
|
547
|
-
useStoreReplayRouteSync(store, remote, routeOpts) -> route controller // Observe.syncStoreReplayRoute wrapper
|
|
548
|
-
useStoreReplayMirror(remote, initial, sameOpts) -> controller & {store} // creates the mirror store in a ref
|
|
549
|
-
useStoreReplayRouteMirror(remote, initial, routeOpts) -> route controller & {store}
|
|
550
|
-
useStoreReplayEach(remote, cb, sameOpts & {initial?, drain?}) -> controller & {store} // per-key fold: Observe.syncStoreReplayEach counterpart
|
|
551
|
-
useReplayFrame(remote, cb, {intervalMs?=300, since?, keepSeq?=true, enabled?=true, hint?, onSeq?, onError?})
|
|
552
|
-
-> {ready, error, seq(), pull(hint?), restart(since?)} // pull-at-own-pace over remote.frame()
|
|
553
|
-
useReplayHistory(history, apply, {head?, reset?, tickMs?=300, autoPlay?=true})
|
|
554
|
-
-> {live, seq, head, pause(), play(), seek({seq?|ts?})}
|
|
555
|
-
```
|
|
556
|
-
Semantics that are easy to get wrong:
|
|
557
|
-
- `cb`/`apply` go through refs: new function identity does NOT resubscribe. Resubscribe identity is `[remote, enabled, epoch, staleMs, policy]` — changing `staleMs` resubscribes (it is subscribe-time config in common2; under keepSeq the reconnect is a journal tail, so it is cheap); `policy` picks the wire surface (line vs frameLine), so it resubscribes too. `onStale` goes through a ref like `cb`.
|
|
558
|
-
- Route hand-off hooks (`useReplayRouteSubscribe`, `useStoreReplayRouteSync`, `useStoreReplayRouteMirror`) wrap common2 `Replay.replayRouteSubscribe` / `Observe.syncStoreReplayRoute`: `switchRoute(nextRemote, {label?, since?, reset?, policy?, hint?})` keeps the old route live, catches up the replacement from the last delivered `seq`, then closes the old route. Failed replacement leaves the old route active and surfaces `error` / `route.phase == "error"`. Changing the `remote` prop is still treated as a fresh subscription boundary; no-gap promotion/demotion is explicit through `switchRoute()`. common2 1.0.65 route handles expose `seq()`, `label()`, and `active()`, but not `isStale()` / `lastTs()`, so route hooks deliberately do not promise freshness yet.
|
|
559
|
-
- Lag policy / hint (frame model, common2 rev2): `policy: 'frame'` rides the server's `frameLine` when the remote has one — on lag the server drops for THIS client and recovers via the line's `frame` lambda (mini-frame); without a frameLine (old server, in-proc `exposeReplay`) common2 silently degrades to `'queue'`, so an in-proc QA of `'frame'` proves nothing — the wire test lives in common2 (`replay/rpc-auto.test.ts`). `hint` is opaque to the transport and goes through a ref: in `useReplaySubscribe` it is captured at subscribe time (used for the catch-up `frame()` call); in `useReplayFrame` it is read on EVERY pull, so a new identity is never missed and never resubscribes.
|
|
560
|
-
- `useReplayFrame` is the pull path: no live socket subscription at all; a timer calls `remote.frame(seq, hint)` and folds envelopes (seq-ascending, seen seq skipped — a keyframe recovery is just an envelope). Fresh start (no `since`) mirrors `replaySubscribe`'s catch-up for since<0: `keyframe()` is polled until the line has one (`frame(-1)` has no tail to return and THROWS on a still-empty line — a mount racing the producer's first event must not stick an error); a sacred line therefore NEEDS an explicit `since` (0 = full tail) — omitted, it waits forever. Overlapping pulls are never issued (a slow `frame()` skips ticks). Errors are LOUD and sticky: a reject (network, sacred line evicted past our seq, remote without `frame()`) stops the timer and sets `error` until `restart()` — deliberately no tail fallback here, that is `replaySubscribe`'s job. Freshness (`staleMs`) does not apply (the consumer owns the cadence).
|
|
561
|
-
- Freshness (`staleMs`): detection is 100% wenay-common2's watchdog (`ReplaySubscribeOpts.staleMs/onStale`, edge-triggered); the hook only mirrors the edges into `stale` state, so a fresh high-frequency line causes zero extra renders. `lastTs()` is a plain getter like `seq()` (producer ts of the last delivered event, 0 before the first delivery / while unsubscribed). Without `staleMs`: no watchdog, no state updates, `stale` stays `false`.
|
|
562
|
-
- Freshness on resubscribe/restart: the hook does NOT reset `stale` to `false` at effect start — it re-syncs from common2 after catch-up (mirrors the `onStale` edge fired during catch-up, plus `isStale()` once `ready` resolves), so a snapshot/tail carrying an old producer ts is stale from the first paint with no fresh-flicker. Caveat: common2's in-proc `keyframe()` stamps `ts` at request time, so a brand-new client on a stalled in-proc line reads as fresh at delivery and flips stale after `staleMs` via the arrival-gap watchdog; old-ts detection kicks in when the tail/keyframe actually carries producer timestamps (real relays, archives).
|
|
563
|
-
- `useReplayHistory` is archive playback — staleness intentionally does not apply there.
|
|
564
|
-
- `keepSeq`: within one mount, a resubscribe (StrictMode double effect, `enabled` toggle, `restart()`) reconnects with `{since: lastSeq}` -> journal tail. A NEW `remote` identity always resets seq (another line's seq is meaningless). A full unmount loses the refs: persist the position in the parent via `onSeq` and pass it back as `since` (QA card 23, client A).
|
|
565
|
-
- `seq()` and `useReplayHistory`'s interval-driven `seq/head` keep high-frequency lines from re-rendering per event. Frames/ticks should fold into a canvas/ref/store, not useState.
|
|
566
|
-
- `useReplayHistory.seek` kills the live subscription imperatively (guarded off, double-call safe), folds `history.at(where)` through `apply` (keyframe + tail; a keyframe fully redefines consumer state, so `reset` is rarely needed), and freezes. `play()` resubscribes `{since: pos}` -> archive tail -> live handover.
|
|
567
|
-
- `useStoreReplayEach` composes existing pieces instead of calling `Observe.syncStoreReplayEach` (which builds a FRESH store per call): the mirror lives in a ref (like `useStoreReplayMirror`), `store.each()` is subscribed in an effect declared BEFORE the sync effect (hook-call order guarantees the per-key subscriber exists when the keyframe applies — the expansion is never missed), then `useStoreReplaySync` drives the wire. Consequences: within one mount every resubscribe (StrictMode, `restart()`, `enabled` toggle, `staleMs`/`policy` change) reconnects by tail ON TOP of kept state and the consumer sees only the diff — no snapshot/`initial` dance (the library one-call needs `{since, initial: snapshot}` for that). After a FULL unmount pass `{since: prev.seq(), initial: prev.store.snapshot()}` like the library contract. `drain` is creation-time (it goes to `createStore`); changing it later does nothing. The fold target must live outside React state (ref/Map/grid api) — the hook renders nothing; `controller.store` is a normal store for `useStoreNode`/`useStoreKeys` extras.
|
|
568
|
-
- Server primitives (`conflateReplay` per connection, `archiveReplay`) intentionally have no hooks: they live where the RPC server is built.
|
|
569
|
-
- Route coordinator (common2 1.0.67 `Replay.createRouteCoordinator`): the coordinator owns promotion/fallback/shadow via policy hooks; consumers subscribe on a pair `link`, whose handle matches our route hooks (`ready, seq(), label(), active()`) minus `switchRoute` — the transport switch is invisible by the seq contract. Our route hooks (`useReplayRouteSubscribe` etc.) remain the manual-`switchRoute` wrapper over `replayRouteSubscribe`; they do NOT sit on top of a coordinator. React surface for coordinator links (subscribe hook + route-state/`promoteDirect` controls for ops UI) is a target-backlog item — do not bend the existing route hooks to accept links implicitly.
|
|
570
|
-
- WebRTC direct path (common2 1.0.68): `createWebRtcConnector({port, rtc, ...})` is the coordinator's direct `RouteConnector`; `rtc` is an injected factory and in the browser that injection (`() => new RTCPeerConnection(cfg)`) belongs to the app/React side — common2 stays lib.dom-free. Signaling rides the EXISTING rpc socket (`createSignalHub` port exposed by `createRpcServerAuto`), so no new connection management appears in React; channel death / revoke surfaces as loud `onError` on the line (never silence), which the existing hooks already map to `error`. Nothing here needs a hook by itself — it matters as the direct transport under a future adapter (target backlog).
|
|
571
|
-
- Peer SDK (common2 1.0.69 `Peer` / `wenay-common2/peer`): the one-call layer over rpc + store + replay + route coordinator — `createPeerClient({remote, account, initial, rtc?})` publishes the own store as a patch line and `peer(account)` returns a live mirrored store + route control; relay and direct share the OWNER's seq space (`createPatchRelayJournal`), so hand-offs are plain seq resumes, no keyframe reset. This is the intended React adoption surface (a `usePeer` adapter is the target-backlog item, superseding the raw coordinator-link hook idea): the mirrored store already plugs into `useStoreNode`/`useStoreKeys` as-is. 1.0.70 ships the oracle suites + `demo/` stand in the package — living examples per subsystem (`replay/peer-sdk.test.ts` is the Peer contract).
|
|
572
|
-
- Media capture (common2 1.0.66 `Media` namespace / `wenay-common2/media`): CONSUMPTION needs no new hooks — `Media.createAudioSource` / `Media.createVideoSource` return `[emit, listen] & control`, with `replay:true` the `listen` is a normal replay line, so `useReplaySubscribe` / `useReplayFrame` cover it; frames are one `Uint8Array` (40-byte fixed header + raw payload, `Media.decodeMediaFrame`) and follow the existing fold-outside-React rule (canvas/AudioContext via ref). The capture `control` side (`start()` resolving to `'idle'|'requesting'|'live'|'denied'|'no-device'|'error'`, `stop()`, `setDevice`, `listDevices`, stats) is a permission/device/lifecycle state machine — exactly hook-shaped; a `useMediaSource` capture hook + QA card is a recorded target goal (supersedes the earlier "wrap it app-locally" stance). Backpressure defaults live in common2 (audio = lossless queue, video `replay:true` = keep-latest frame recovery); the client-side knobs are the same `policy`/`hint` as any replay line.
|
|
573
|
-
|
|
574
|
-
QA cards 23/24/25/26 (`testUseReact/replayVideo.tsx`, all in-proc): synthetic 10fps jpeg-frame producer on `Replay.replayListen({history, current})`; client A = direct `exposeReplay` remote; client B = simulated slow wire (1 envelope per rateMs) behind `conflateReplay({pending: () => buf.length, highWater: 4, lowWater: 1, keyOf: () => "frame"})`; client C = `archiveReplay` + `openHistory` scrubber; client D = freshness (`staleMs: 2000`, `React.memo` + no tick, mounted inside a local `<StrictMode>`; the flat renders counter under growing frames is the no-per-event-render proof; "stall producer" toggles the emit interval, "new client" remounts by key for the stalled-mount case; card 24 has the same via `staleMs: 2500` on the mirror); client E = pull path (`useReplayFrame` over the direct remote with a wrapped counting `frame()`, pace switch 250ms/1s/3s keeps seq). `window.__replayVideoDemo` is exposed for debugging (wire.setRateMs, stats). Node-verified: slow wire delivered 12/36 envelopes yet converged to the last frame with bounded buffer (coalesced tail recovery); `syncStoreReplay` off() freezes the mirror and `{since}` resubscribe catches up by tail. Card 25 = per-key feed (`useStoreReplayEach` over `exposeStoreReplay`): a dict-of-rows store, producer touches ONE random row per tick, the fold target is a plain Map with per-row cb counters — only the mutated row's counter grows, keyframe/`replace` are the only whole-table expansions, delete arrives as `(key, undefined)`. Card 26 = route hand-off (`useReplayRouteSubscribe` over the same video line): one canvas starts on relay, switches direct/relay by `switchRoute`, and failed replacement keeps the previous route alive. Browser QA of throttling-sensitive behavior needs a VISIBLE tab: hidden-tab timer/effect throttling stalls the producer and delays passive effects (known stand caveat).
|
|
575
|
-
|
|
576
|
-
## Logs
|
|
577
|
-
Frequent global logger:
|
|
578
|
-
```
|
|
579
|
-
getLogsApi({limit?, limitPer, varMin?})
|
|
580
|
-
createLogsController({options, state?, onFullChange?, onMiniChange?, onSettingsChange?})
|
|
581
|
-
createLogsControllerState({full?, mini?, settings?})
|
|
582
|
-
logsApi
|
|
583
|
-
PageLogs({update?})
|
|
584
|
-
useMessageEventLogsController({maxVisible?})
|
|
585
|
-
MessageEventLogsView({controller, zIndex?, className?, style?})
|
|
586
|
-
MessageEventLogCard({logs})
|
|
587
|
-
MessageEventLogs({zIndex?}) // compatibility wrapper
|
|
588
|
-
LogsPage({update?})
|
|
589
|
-
useMiniLogsTable({data, onClick?, columnDefs?, defaultColDef?})
|
|
590
|
-
MiniLogsView({controller})
|
|
591
|
-
MiniLogsTable({data, onClick?, columnDefs?, defaultColDef?})
|
|
592
|
-
MiniLogs({data, onClick?}) // compatibility wrapper
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
React-context logger:
|
|
596
|
-
```
|
|
597
|
-
LogsProvider
|
|
598
|
-
useLogsContext()
|
|
599
|
-
useLogsTableController()
|
|
600
|
-
LogsTable()
|
|
601
|
-
useLogsNotificationsController()
|
|
602
|
-
LogsNotifications()
|
|
603
|
-
LogsSettings()
|
|
604
|
-
MainPage()
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
The context logger is a larger UI surface; the global `logsApi` is still the shorter integration point. `createLogsController` is the headless layer for append/limit/settings state; `useMessageEventLogsController` owns the global notification queue/timers/settings, while `MessageEventLogsView` and `MessageEventLogCard` own rendering. `PageLogs`, `MessageEventLogs`, and `LogsPage` remain compatibility UI wrappers. `useLogsTableController` and `useLogsNotificationsController` expose the provider-local table/notification state while `LogsTable` and `LogsNotifications` keep the visual wrappers. Shared logger chrome lives in `src/common/src/logs/logStyles.ts` and is themed through `--logs-*` tokens.
|
|
608
|
-
|
|
609
|
-
Full-page table controller (`useLogsPageTable` -> `LogsPageTableController`): the last logs table
|
|
610
|
-
moved to the controller pattern. Semantics that matter: the grid receives a MOUNT-TIME snapshot of
|
|
611
|
-
the accumulated log map once (`useState` initializer) and afterwards lives on
|
|
612
|
-
`applyTransactionAsync` appends from the mini feed - re-renders do not re-feed `rowData`, so row
|
|
613
|
-
identity is per-append copy exactly as before. The importance filter is ONE method
|
|
614
|
-
(`applyImportanceFilter(min?)`: set -> `setFilterModel` greaterThanOrEqual on `var`, unset ->
|
|
615
|
-
`destroyFilter`), used by both the settings subscription and `onGridReady` (which skips the
|
|
616
|
-
destroy branch - a fresh grid has no filter). Both data subscriptions are `updateBy(obj, cb)`
|
|
617
|
-
WITH a callback: they run imperatively on `renderBy` and never re-render the component.
|
|
618
|
-
`gridProps` is one stable memo - spread it into `AgGridTable`; `fit()` is exposed for the legacy
|
|
619
|
-
`update` prop, which `PageLogs` still honors. QA card 9.
|
|
620
|
-
|
|
621
|
-
## Cache / Memory / Browser Utilities
|
|
622
|
-
```
|
|
623
|
-
browserCacheStorage
|
|
624
|
-
localStorageCache
|
|
625
|
-
restoreDates(obj)
|
|
626
|
-
createCacheMapWithStorage(entries, save)
|
|
627
|
-
createCacheMap(entries)
|
|
628
|
-
|
|
629
|
-
ObservableMap<K,V> extends Map // set/delete/clear announce themselves; touch(key?)
|
|
630
|
-
.onChange(cb) -> off // announces an in-place mutation; MapChangeListener<K>
|
|
631
|
-
memoryCache.onDirty(cb) -> off // instance dirty channel (coalesced, async); DirtyListener
|
|
632
|
-
memoryCache.isDirty()
|
|
633
|
-
memoryCache.markDirty(scope?, key?) // manual announce, for plain-Map entries only
|
|
634
|
-
|
|
635
|
-
memorySet(key, data)
|
|
636
|
-
memoryGet(key)
|
|
637
|
-
memoryGetOrCreate(key, def, options?)
|
|
638
|
-
memoryGetById(key, def, id)
|
|
639
|
-
memoryUpdate(key, mutate)
|
|
640
|
-
memoryMarkDirty(key)
|
|
641
|
-
createSearchHistory({key, max?})
|
|
642
|
-
deepMergeWithMap(target, source)
|
|
643
|
-
structEqual(a, b) // deep equality for plain data trees; JSON.stringify-idiom
|
|
644
|
-
// tolerances (undefined props absent, NaN==NaN) WITHOUT
|
|
645
|
-
// its key-order sensitivity; not for Maps/Sets/cycles
|
|
646
|
-
memoryCache
|
|
647
|
-
memoryMaps
|
|
648
|
-
|
|
649
|
-
ArrayPromise({arr, catchF?, thenF?})
|
|
650
|
-
PageVisibilityProvider
|
|
651
|
-
PageVisibilityContext
|
|
652
|
-
setAutoStepForElement(input, {minStep?, maxStep?})
|
|
653
|
-
|
|
654
|
-
useCacheMapPersistence(cache: CacheMap, delay?=300) -> {isDirty(), flush(), save(), reload()}
|
|
655
|
-
```
|
|
656
|
-
|
|
657
|
-
Layering (2026-07 architecture pass): the persisted maps (`floatingWindowMap`, `mapResiReact`,
|
|
658
|
-
`mapRightMenu`) are DECLARED in the utils leaf `utils/persistedMaps.ts` and re-exported by their
|
|
659
|
-
owning components (FloatingWindow/Resizable/RightMenuStore) - importing `memoryStore`/`memoryCache`
|
|
660
|
-
no longer pulls react-rnd or the Menu tree; only `import type` lines point upward (erased at build).
|
|
661
|
-
Shared order helper `utils/fixedOrder.ts` (`pinFixedOrder`, `movedOrderWithFixed`) is the single
|
|
662
|
-
implementation of the "fixed entries pin to descriptor index" invariant used by columnState,
|
|
663
|
-
ColumnsMenu, and Toolbar (it used to be four inline copies that had to stay byte-identical).
|
|
664
|
-
|
|
665
|
-
Cache helpers are process/browser storage utilities, not React state management. The one React
|
|
666
|
-
entry here is `useCacheMapPersistence` - the documented app contract (`load()` on mount +
|
|
667
|
-
`onDirty -> saveDebounced(delay)`) as a hook. Scope is deliberately exactly that: the
|
|
668
|
-
pagehide/visibilitychange flush backstops stay app-side (they are page-level policy, not
|
|
669
|
-
per-component). Effect identity is `[cache, delay]`; the returned methods are pass-throughs and
|
|
670
|
-
`reload()` is an alias of `load()` - a repeated load MERGES storage on top of the current maps
|
|
671
|
-
(addDataToMap semantics), it is not a reset. QA cards 20/21/25 share one `memoryCache`, so
|
|
672
|
-
several mounted hooks coexist: load() is idempotent-enough (merge) and extra onDirty
|
|
673
|
-
subscribers just debounce the same save.
|
|
674
|
-
|
|
675
|
-
Dirty/save contract: the dirty signal originates in the data layer. The persisted maps are
|
|
676
|
-
`ObservableMap`s, so `set/delete/clear` announce themselves; in-place mutations of stored
|
|
677
|
-
objects are invisible to map methods and are announced with `map.touch(key)` at the commit
|
|
678
|
-
points (FloatingWindow drag/resize stop, FResizableReact resize stop, createUiSlot.setPlace) or,
|
|
679
|
-
app-side, with `memoryUpdate(key, mutate)` / `memoryMarkDirty(key)`. `createCacheMapWithStorage`
|
|
680
|
-
subscribes to the ObservableMaps it owns - the channel is per instance, there is no global
|
|
681
|
-
bus; plain Maps in `arr` stay silent (announce those via `memoryCache.markDirty`). Announcements
|
|
682
|
-
never save anything: scope/key are event metadata, WHAT gets written is decided by the
|
|
683
|
-
save-side serialized-snapshot diff, so a missed announcement degrades to "saved later" and
|
|
684
|
-
an extra one to a no-op write. `isDirty()` is set synchronously; `onDirty` delivery is
|
|
685
|
-
coalesced through a microtask (safe to mutate maps inside render/init paths; not throttled
|
|
686
|
-
in background tabs the way timers are). `memoryCache.load()` ignores its own map events while
|
|
687
|
-
loading and resets dirty after; a save cycle waits out an in-flight load() (a save racing
|
|
688
|
-
a load could overwrite storage with in-memory defaults) and resets the flag at its START
|
|
689
|
-
so a change arriving mid-write survives. Caveat: `flush()` is async - on pagehide
|
|
690
|
-
localStorage writes usually complete but are not guaranteed (Cache API is not); prefer
|
|
691
|
-
visibilitychange->hidden as the primary final save.
|
|
692
|
-
|
|
693
|
-
## Resize Observer
|
|
694
|
-
```
|
|
695
|
-
CResizeObserver
|
|
696
|
-
setResizeableElement(el)
|
|
697
|
-
removeResizeableElement(el)
|
|
698
|
-
ObserveID
|
|
699
|
-
|
|
700
|
-
useResizeObserver<T>(onResize) -> {ref, element()}
|
|
701
|
-
useElementSize<T>() -> {ref, element(), width, height, getSize()}
|
|
702
|
-
```
|
|
703
|
-
|
|
704
|
-
Use the class/imperative pair only when a component must participate in the shared resize
|
|
705
|
-
observer map. The hooks are the React-shaped entry over the SAME module singleton (one native
|
|
706
|
-
`ResizeObserver` per app): `useResizeObserver` returns a callback ref (attach/detach re-observes;
|
|
707
|
-
React null-call on unmount unsubscribes) and takes `onResize` through a ref - new identity
|
|
708
|
-
neither resubscribes nor is missed. The native observer fires once right after `observe`, so the
|
|
709
|
-
first measurement arrives without an explicit initial read. `useElementSize` folds that into
|
|
710
|
-
rounded, equality-guarded `{width, height}` state (a no-op resize does not re-render) plus a live
|
|
711
|
-
`getSize()` getter (exact floats, no render wait). `setResizeableElement`'s auto-shrink path is
|
|
712
|
-
untouched and still class-driven (ParamsEditor inputs). QA card 19 drives both hooks.
|
|
713
|
-
|
|
714
|
-
## Styles
|
|
715
|
-
```
|
|
716
|
-
tokens // TS mirror for inline styles and z-index
|
|
717
|
-
GridStyleDefault()
|
|
718
|
-
StyleGridDefault
|
|
719
|
-
StyleCSSHeadGridEdit(name, rules)
|
|
720
|
-
StyleCSSHeadGrid()
|
|
721
|
-
AgGridClassRule<T>
|
|
722
|
-
```
|
|
723
|
-
|
|
724
|
-
Style entry points:
|
|
725
|
-
- `src/style/tokens.css` - CSS custom properties shipped to consumers.
|
|
726
|
-
- `src/common/src/styles/tokens.ts` - TS mirror for inline styles, modal z-index, and ag-grid theme params. Values must stay aligned with `tokens.css`.
|
|
727
|
-
- `src/style/style.css` - shared component classes and token consumers.
|
|
728
|
-
- `src/style/menuRight.css` - right-menu classes and outline-demo CSS.
|
|
729
|
-
- `src/common/src/styles/styleGrid.ts` and `src/common/src/grid/agGrid4/theme.ts` - ag-grid theme setup from `tokens.grid`.
|
|
730
|
-
- QA stand: `npm run testReact -- --host 127.0.0.1 --port 3002`, entry `src/common/testUseReact/qa.tsx`.
|
|
731
|
-
|
|
732
|
-
Current tokenized prefixes:
|
|
733
|
-
- `--color-*` for base palette.
|
|
734
|
-
- `--menu-*` for mouse context-menu and right-menu visuals.
|
|
735
|
-
- `--wnd-*` for `FloatingWindow` chrome.
|
|
736
|
-
- `--dlg-*` for `SettingsDialog` chrome.
|
|
737
|
-
- `--tb-*` for toolbar chrome.
|
|
738
|
-
- `--logs-*` for global/context logger chrome.
|
|
739
|
-
- `--cols-menu-*` for compact `ColumnsMenu/MenuStrip` chrome.
|
|
740
|
-
- `--cols-dots-*` for `ColumnDots` chrome.
|
|
741
|
-
- `--cols-card-*` for `CardList` chrome.
|
|
742
|
-
- `--wenay-z-modal` for modal/overlay stacking.
|
|
743
|
-
|
|
744
|
-
Normalization rule: new shared CSS should first try an existing token. Add a new token only when a value is reused by a shared primitive or is expected to be theme-overridden by apps. One-off app/demo styles should stay in the demo/app wrapper, not in library tokens. Do not delete a default style/class without a replacement class/token path and changelog entry; visually broken defaults are treated as a compatibility break.
|
|
745
|
-
|
|
746
|
-
Open normalization candidates:
|
|
747
|
-
- `src/style/style.css`: `.msTradeAlt`, `.msTradeActive`, `.newButtonSimple`, `.toIndicatorMenuButton:hover`, submit-button green, and several toolbar row hover/drag literals still use raw colors.
|
|
748
|
-
- `src/common/src/grid/columnState/*`: compact menu/dots/card visuals now use `.wenayColumnGrid*`, `.wenayColsMenu*`, `.wenayColDots*`, `.wenayCardList*` plus `--cols-grid-*`, `--cols-menu-*`, `--cols-dots-*`, and `--cols-card-*`; further changes here should be visual QA only, not a new default palette.
|
|
749
|
-
- `src/common/src/components/ParamsEditor.tsx` and `src/common/src/components/Input.tsx`: if these stay public primitives, define default class/token contracts instead of component-owned visual styling.
|
|
750
|
-
- `src/common/src/styles/commentaryStyles.css`: standalone `.commentary` CSS is not imported by the root style bundle; either import/tokenize it if still used, or mark it as a local component concern.
|
|
751
|
-
|
|
752
|
-
Recently normalized: mouse context-menu item colors through `--menu-*`, `--menu-outline-color` for `OutlineDragDemo`, `--logs-*` for logger chrome, `--dlg-scrim` in `ModalProvider`, compact `ColumnsMenu/MenuStrip` visuals through `.wenayColsMenu*` / `--cols-menu-*`, card-29 mobile primitives through `--cols-dots-*` / `--cols-card-*`, and createColumnGrid overlay through `.wenayColumnGrid*` / `--cols-grid-*`.
|
|
753
|
-
|
|
754
|
-
## Cleanup Inventory
|
|
755
|
-
|
|
756
|
-
Do not delete a public export just because it looks unused inside this repo. External apps may import it. For cleanup, first move an item into this inventory, then decide in a separate breaking version whether it remains public, moves to a demo namespace, or is removed.
|
|
757
|
-
|
|
758
|
-
Suspicious but still public:
|
|
759
|
-
- `OutlineDragDemo`, `RightMenuDemo`, `ChartDemo` - demo/test-style exports. They are useful for the QA stand, but look like demo surface rather than core API.
|
|
760
|
-
- `StickerMenu` - exported from `components/Menu`; visually app-specific and should probably become an app wrapper or be documented as an example component.
|
|
761
|
-
- `logsApi` global logger and the context logger (`LogsProvider`, `LogsTable`, `LogsNotifications`, `LogsSettings`, `MainPage`) overlap. Keep both for now; document one as the short integration path and the other as the larger UI surface.
|
|
762
|
-
- Chart engine primitives (`DataSet`, `Panel`, `Renderer`, `Interaction`, `ChartEngine`, etc.) are very low-level. They are public through `chartEngineReact.tsx`; product apps should wrap them before use.
|
|
763
|
-
- `src/common/src/myChart/chartEngine/chartEngine.ts` is a reference copy next to the public React engine. It is not exported from root; treat it as suspicious maintenance debt, not as public API.
|
|
764
|
-
- `StyleCSSHeadGridEdit` and `StyleCSSHeadGrid` mutate `<head>` directly. They are exported and can have consumers, but new grid styling should prefer ag-grid theme params and tokens.
|
|
765
|
-
|
|
766
|
-
Added by the 2026-07-09 architecture audit (evidence in the audit report / `doc/target/my.md`):
|
|
767
|
-
- `menuR.tsx` (`createRightClickMenu` / `MenuR`) - CONFIRMED dead: no runtime consumer anywhere (the test that supposedly covers it imports `RightMenu.tsx`); re-exported twice from `api.tsx` (`export *` + `kit.menu.rightClick`). Candidate for removal in a deliberate breaking version; its gesture code duplicates `menuMouse.tsx:342-380`.
|
|
768
|
-
- `logsContext.tsx` - a full PARALLEL logs stack (own `LogEntry` shape divergent from `logsController`, raw unguarded `localStorage`, own settings/notifications/table) exported from the barrel next to `logs.tsx`. Decide: deprecate or converge; new code should use `logsApi`/`createLogsController`.
|
|
769
|
-
- `MyChartEngine` - a hardcoded DEMO (random data + setInterval inside `useEffect`, no props but `style`) exported as public API from `chartEngineReact.tsx`; `generateIncrementalData` (Math.random demo util) is public too. Should move to demo namespace or gain a real props contract.
|
|
770
|
-
- ~~`getLogsApi<T>()` shared module state~~ FIXED (A2, v1.0.41): every call now builds its own map/mini/settings (optional `settingsKey` persists per instance); the global `logsApi` explicitly injects the legacy shared state.
|
|
771
|
-
- ~~`FloatingWindow` `onCLickClose` typo~~ ALIASED (A10, v1.0.42): `onClickClose` added, typo prop is a deprecated alias; REMOVAL of the alias still needs a breaking pass.
|
|
772
|
-
- `DragArea` - `@deprecated` (A7, v1.0.42): no consumers; unique semantics kept as-is (see Drag / Resize Low Level). Removal in a deliberate breaking version.
|
|
773
|
-
- `Button` `keySave` prop - deprecated alias of `keyForSave` (A10, v1.0.42); removal in a breaking pass.
|
|
774
|
-
- `contextMenu` raw `map`/legacy-queue path is test-only-live (populated only by `contextMenuStats.test.tsx`); the `bb`/`map.clear()` reopening invariant itself is intentional and stays.
|
|
775
|
-
|
|
776
|
-
## Charts
|
|
777
|
-
Canvas chart:
|
|
778
|
-
```
|
|
779
|
-
createChartCanvas(config) -> IChartCanvas
|
|
780
|
-
ChartDemo()
|
|
781
|
-
```
|
|
782
|
-
|
|
783
|
-
Chart engine:
|
|
784
|
-
```
|
|
785
|
-
createDataSet(params)
|
|
786
|
-
createDataModel()
|
|
787
|
-
createPanelManager()
|
|
788
|
-
createRenderer()
|
|
789
|
-
createInteraction(...)
|
|
790
|
-
createChartEngine(canvas)
|
|
791
|
-
generateIncrementalData(...)
|
|
792
|
-
MyChartEngine
|
|
793
|
-
```
|
|
794
|
-
|
|
795
|
-
The chart engine exports many internal interfaces (`DataPoint`, `DataSet`, `Panel`, `Renderer`, `Transform`,
|
|
796
|
-
`Interaction`, `ChartEngine`). Treat them as low-level until an app wrapper fixes product-level behavior.
|
|
797
|
-
|
|
798
|
-
## Deprecated Naming Smells
|
|
799
|
-
```
|
|
800
|
-
Get* // usually a factory from older style; prefer create* or use*
|
|
801
|
-
*FuncJSX // imperative JSX store; prefer React context/controller
|
|
802
|
-
*2 / *3 // version suffix; document the intended canonical one in brief
|
|
803
|
-
removed old grid update names -> applyGridRows / agGrid4
|
|
804
|
-
```
|
|
805
|
-
|
|
806
|
-
Do not add new generic utilities with app words in their signature. For example, a column primitive should accept
|
|
807
|
-
`names` and `apply`, while a product wrapper decides group ids, column ids, and labels.
|
|
1
|
+
# wenay-react2 - EXTENDED / rare surface
|
|
2
|
+
|
|
3
|
+
> Everyday API lives in **`wenay-react2.md`**.
|
|
4
|
+
> This file lists low-level primitives and migration notes.
|
|
5
|
+
> Root import: `import { ... } from "wenay-react2"`.
|
|
6
|
+
|
|
7
|
+
## Migration Rule
|
|
8
|
+
```
|
|
9
|
+
New code teaches and imports the short canonical surface:
|
|
10
|
+
useModal(), useOutside(), useDraggableApi(), useReorder(), useReorderBoard(),
|
|
11
|
+
useAgGrid(), createGridBuffer(), createColumnBuffer(), createUiSlot(), createToolbar()
|
|
12
|
+
|
|
13
|
+
Old names are recorded only in `WENAY_REACT2_RENAMES.md`; do not export new compatibility aliases casually. Existing public APIs should usually remain supported during internal refactors; removals require an explicit migration cut with a separate task, migration notes, changelog, and usage signal. Default style contracts are stricter: do not remove old default classes/styles until the replacement class/token path is shipped and documented.
|
|
14
|
+
Do not add new examples with Get*, FuncJSX, *2/*3, or business-specific helper names.
|
|
15
|
+
If a primitive needs app policy, build a small app wrapper above it.
|
|
16
|
+
If behavior is reusable, expose it first as a headless use*/create* API. The hook/controller may return methods, getters, props/bind helpers, or a small API object; visual components and QA cards should consume that surface instead of becoming the only implementation.
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Canonical method vocabulary:
|
|
20
|
+
```
|
|
21
|
+
open / close / set / replace // modal-like state
|
|
22
|
+
update / remove / clean / sync // data-buffer state
|
|
23
|
+
fit / flush // ag-grid visual/transaction lifecycle
|
|
24
|
+
props / bind // spreadable element props
|
|
25
|
+
get / set / reset / cancel // local hook/controller state
|
|
26
|
+
on -> off // subscriptions
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Root Namespaces
|
|
30
|
+
```
|
|
31
|
+
import { kit } from "wenay-react2"
|
|
32
|
+
|
|
33
|
+
kit.hooks
|
|
34
|
+
kit.dnd
|
|
35
|
+
kit.utils
|
|
36
|
+
kit.grid
|
|
37
|
+
kit.modal
|
|
38
|
+
kit.menu
|
|
39
|
+
kit.logs
|
|
40
|
+
kit.updateBy
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The root export is still flat. `kit` is useful when a large file needs grouped names.
|
|
44
|
+
|
|
45
|
+
## Modal Low Level
|
|
46
|
+
```
|
|
47
|
+
useModal() // callable controller with show/open/close/set
|
|
48
|
+
createModalElementStore() // imperative JSX store; prefer ModalProvider/useModal
|
|
49
|
+
createModalRenderStore() // function JSX store; prefer ModalProvider/useModal
|
|
50
|
+
inputModal({modal, func, name?, txt?})
|
|
51
|
+
confirmModal({modal, func, password?})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`inputModal` and `confirmModal` accept either a setter function or the `useModal()` controller.
|
|
55
|
+
|
|
56
|
+
Internal Overlay (A9, 2026-07-10): `components/Overlay.tsx` is the ONE portal+scrim+outside-click+
|
|
57
|
+
Escape composition; `ModalProvider` and `SettingsDialog` are adapters over it (behavior 1:1,
|
|
58
|
+
SettingsDialog keeps its own two-stage Escape by NOT passing `onEscape`). It is deliberately not
|
|
59
|
+
exported: apps use ModalProvider/useModal/SettingsDialog. The render-slot stores above, the
|
|
60
|
+
LeftModal drawer (gesture-driven, scrim-less), and `ModalWrapper`/Input helpers (backdrop-less,
|
|
61
|
+
hosted inside another modal slot) are NOT overlays and stay separate. This leaf is also where all
|
|
62
|
+
scrim/portal DOM lives - the seam for a future react-native view layer.
|
|
63
|
+
|
|
64
|
+
Left-side modal/menu helpers:
|
|
65
|
+
```
|
|
66
|
+
LeftModal({arr, zIndex})
|
|
67
|
+
ApiLeftMenu
|
|
68
|
+
getApiLeftMenu()
|
|
69
|
+
TestLeft333()
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
These are app-shell style utilities. Prefer local app wrappers for new layouts.
|
|
73
|
+
|
|
74
|
+
## Settings Dialog / UI Slot / Callback Hub Details
|
|
75
|
+
Settings dialog (centered panel ~720x480, max 92vw/82vh; searchable settings tree left, content right):
|
|
76
|
+
```
|
|
77
|
+
type SettingsSection = {key: string, name: string, render: () => ReactNode, children?, parentKey?, searchText?, keywords?}
|
|
78
|
+
|
|
79
|
+
<SettingsDialog
|
|
80
|
+
trigger={...} // wrapper span is clickable
|
|
81
|
+
sections? // static sections, listed first; children form tree nodes
|
|
82
|
+
defaultSection? // falls back to the first section when missing/unmounted
|
|
83
|
+
sectionClassName? // apps pass their own .chip; default .wenayDlgSection
|
|
84
|
+
sectionActiveClassName? // apps pass their own .chipActive; default .wenayDlgSectionActive
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
useSettingsDialogController({sections?, defaultSection?, sectionClassName?, sectionActiveClassName?})
|
|
88
|
+
registerSettingsSection(s) -> unregister
|
|
89
|
+
getSettingsSections() -> readonly SettingsSection[]
|
|
90
|
+
```
|
|
91
|
+
The registry is a module singleton on updateBy/renderBy (no React context). Registering the same
|
|
92
|
+
`key` replaces the previous section; unregister removes by identity, so a stale unregister after a
|
|
93
|
+
replacement is a no-op. Tree shape comes from nested `children` or flat sections with `parentKey`.
|
|
94
|
+
Search matches section labels, `keywords`, `searchText`, and best-effort text extracted from rendered
|
|
95
|
+
React children; matching descendants stay visible and their ancestors auto-expand. The search input uses
|
|
96
|
+
`createSearchHistory({key:"SettingsDialog.searchHistory"})`: Enter commits the current query, the history
|
|
97
|
+
button recalls stored queries, leaving the search box closes the list, and changes are published through memoryProps -> memoryCache. `useSettingsDialogController` exposes the same open/search/tree/history/nav-resize actions for custom settings chrome. Tree controls
|
|
98
|
+
are one compact dotted cycle button in the search row (expanded -> current branch -> collapsed), hidden when
|
|
99
|
+
the tree has too little hierarchy to need it. Closes on the x, a scrim click, and Escape.
|
|
100
|
+
Styling: `--dlg-scrim / bg / border / radius / shadow / nav-bg / nav-width` tokens (tokens.css,
|
|
101
|
+
mirror `tokens.dlg`), classes `.wenayDlgScrim / .wenayDlg / .wenayDlgNav / .wenayDlgSearch /
|
|
102
|
+
.wenayDlgTree / .wenayDlgContent / .wenayDlgSearchHistory` in style.css; dark defaults, apps re-skin
|
|
103
|
+
via `:root[data-theme]` like `--wnd-*`.
|
|
104
|
+
UI slot:
|
|
105
|
+
```
|
|
106
|
+
createUiSlot({key, places, def}) -> {Slot, PlacementSetting, getPlace, setPlace}
|
|
107
|
+
<slot.PlacementSetting className? activeClassName? /> // defaults .wenaySegBtn / .wenaySegBtnActive
|
|
108
|
+
```
|
|
109
|
+
State lives in `memoryGetOrCreate(key)` -> persisted with the rest of memoryProps; `setPlace`
|
|
110
|
+
announces the in-place mutation via `memoryMarkDirty(key)` and the APP decides when to save
|
|
111
|
+
via `memoryCache.onDirty` (the library never writes storage itself, same as window state).
|
|
112
|
+
A stored place that no longer exists in `places` falls back to `def`. `getPlace()` is not reactive;
|
|
113
|
+
Slot/PlacementSetting subscribe internally via updateBy.
|
|
114
|
+
|
|
115
|
+
Toolbar (createToolbar, `components/Toolbar/Toolbar.tsx`):
|
|
116
|
+
```
|
|
117
|
+
createToolbar({key, items, def?, settingsItem?, resetItem?, source?, sourceMode?}) -> {Bar, Settings, api: {useConfig, useItems, getConfig, setConfig, setOrder, show, setDensity, showSettings, showReset, reset, onChange}}
|
|
118
|
+
<tb.Bar className? settings? reset? popAlign? /> // default classes .wenayTb / .wenayTbItem; popAlign
|
|
119
|
+
// 'right' (default, top-right bars) | 'left'
|
|
120
|
+
<tb.Settings className? activeClassName? /> // density segments default .wenaySegBtn(Active)
|
|
121
|
+
registerToolbarDensity({key, name, renderItem?}) / getToolbarDensities()
|
|
122
|
+
toolbarItemIcon(item) -> ReactNode // item.icon, or first letters of short/title
|
|
123
|
+
```
|
|
124
|
+
Three decoupled layers: serializable config `{order, visible, density}` (single source of truth,
|
|
125
|
+
persisted exactly like createUiSlot: memoryGetOrCreate(key), edits mutate in place + renderBy +
|
|
126
|
+
memoryMarkDirty, the APP saves via memoryCache.onDirty), Bar (visible!==false, in order, at density) and
|
|
127
|
+
a PURE Settings editor over config - the same element works in the Bar's gear popover and in a
|
|
128
|
+
registered settings section, no prop changes. Semantics that are easy to get wrong:
|
|
129
|
+
- Merge of stale persist: unknown keys in stored order/visible are ignored (view-level; raw
|
|
130
|
+
storage is untouched until the next setConfig), items missing from the config are APPENDED
|
|
131
|
+
default-visible - an app update never wipes the user's order/visibility.
|
|
132
|
+
- `fixed` items: always visible, pinned at normalize() to their index in opts.items (checkbox
|
|
133
|
+
disabled, not draggable) - and the drag PREVIEW already respects the pinning, so they never
|
|
134
|
+
move visually and a drop never lands elsewhere than shown.
|
|
135
|
+
- Density is an extensible module registry (like registerSettingsSection): entries may carry
|
|
136
|
+
`renderItem(item)`; item.render(density) always wins; entries without renderItem fall back to
|
|
137
|
+
icon + (short ?? title). A persisted density that is no longer registered falls back to
|
|
138
|
+
def/first. Bar/Settings subscribe to the registry (updateBy), so registering a level updates
|
|
139
|
+
live.
|
|
140
|
+
- Reorder rides the library's own `useReorder` (see Drag / Resize Low Level; this editor is its
|
|
141
|
+
first consumer): the WHOLE row drags, mouse+touch, checkbox excluded, and `move` is the SAME
|
|
142
|
+
simulated commit as normalize() (splice + fixed pinning), so preview and drop agree by
|
|
143
|
+
construction. Neighbours glide via transitioned transforms (`.wenayTbRowShift`), ONE setConfig
|
|
144
|
+
on drop. Plus arrow keys on the focused handle (the drag hook preventDefaults mousedown, so the
|
|
145
|
+
row handler focuses the handle itself). No dnd dependency; FloatingWindow/react-rnd deliberately not
|
|
146
|
+
used (free-floating windows, wrong tool). `touch-action: none` + `user-select: none` on
|
|
147
|
+
draggable rows (`.wenayTbRowGrab`).
|
|
148
|
+
- `api.onChange` is a wenay-common2 `listen` stream; emits the NORMALIZED config after every
|
|
149
|
+
config edit. useConfig subscribes via updateBy; getConfig is a non-reactive snapshot.
|
|
150
|
+
`setOrder(order)` is the focused external-control path for grid/menu reorder sync: it preserves
|
|
151
|
+
current membership, density, and pseudo-control visibility while changing only item order. `show(key,on)`,
|
|
152
|
+
`setDensity(key)`, `showSettings(on)`, and `showReset(on)` are the matching narrow writes; `setConfig` remains the low-level full write.
|
|
153
|
+
- The gear and reset buttons are pseudo-items: reserved visible keys `__settings` and `__reset`,
|
|
154
|
+
NO order slots (they always sit at the bar edge), toggled from separated rows at the bottom of
|
|
155
|
+
the editor (`.wenayTbRowMeta`, outside the drag-slot container on purpose). Hiding the gear in
|
|
156
|
+
its own popover closes the popover - the global settings section is the way back. The reset
|
|
157
|
+
button calls `api.reset()` and restores defaults for order, membership, density and pseudo-control
|
|
158
|
+
visibility. Settings is default-visible; reset is default-hidden unless `resetItem.defaultVisible=true` or `api.showReset(true)` enables it. `resetItem: false` removes that feature. Faces via `settingsItem` / `resetItem`.
|
|
159
|
+
- **The Settings CONTAINER is the client's choice (not the core's).** `tb.Settings` is
|
|
160
|
+
presentation-agnostic - render it anywhere. `<Bar settings>`'s gear opens it in a small inline
|
|
161
|
+
popover anchored to the gear; since the bar reflows when you toggle membership, that popover
|
|
162
|
+
shifts a little (known minor twitch). The core deliberately ships ONLY that inline popover -
|
|
163
|
+
anything richer is a client layer. For a stable, movable editor render `tb.Settings` in your own
|
|
164
|
+
container: a registered settings section (`registerSettingsSection`), or a draggable window -
|
|
165
|
+
`FloatingWindow` (drag by header, close X, viewport-clamped, position persisted via `keyForSave`)
|
|
166
|
+
wrapped in `OutsideClickArea` for close-on-outside-click. Recommended pattern for clients who
|
|
167
|
+
want the modal-with-title-bar-and-close look; QA card 30 demonstrates it (gear -> Settings in a
|
|
168
|
+
FloatingWindow, in the card's own positioned layer so it scrolls with the card). This is separate
|
|
169
|
+
from the row REORDER above, which stays on `useReorder` (a floating window would be the wrong tool
|
|
170
|
+
there).
|
|
171
|
+
- `api.useItems()` is the headless bar: ordered, visibility-filtered `[{item, density, content}]`
|
|
172
|
+
for fully custom markup. Refs-out was rejected deliberately: order lives in the config, so the
|
|
173
|
+
consumer re-renders from this list - the library never re-parents foreign DOM nodes.
|
|
174
|
+
- `icon` is OPTIONAL: `toolbarItemIcon(item)` returns the icon, else the first 3 letters of
|
|
175
|
+
short/title as a text pseudo-icon (icon density only - the label density shows the caption, not
|
|
176
|
+
the letters). Exported so menus (ColumnsMenu compact) share the exact rule.
|
|
177
|
+
- `source?: UiListSource` inverts ownership. Default `sourceMode:"orderVisible"` makes the toolbar a
|
|
178
|
+
VIEW over source order+visibility: setConfig writes item order/visibility THERE (its own change
|
|
179
|
+
flow re-emits `api.onChange`; the toolbar emits itself only if the source has no `onChange`), while
|
|
180
|
+
density and the gear/reset flags stay in the toolbar's own `st` under `key`. `sourceMode:"order"`
|
|
181
|
+
shares only the relative order of keys present in the source; item membership, density,
|
|
182
|
+
pseudo-controls, and extra non-source item positions stay local. On source reorders, source keys
|
|
183
|
+
refill their slots in the local toolbar order, so an extra item like QA card 30's `blockMode` is
|
|
184
|
+
not pushed into `columnState` and does not need a bridge hook. `ext` is per-instance constant, so
|
|
185
|
+
hook-call order in useConfig/Bar/Settings never changes. `columnState.api.listSource` is the
|
|
186
|
+
reference implementer (QA card 31 for order+visibility, QA card 30 for order-only). Without a source
|
|
187
|
+
- v1 non-goals: no overflow/"more" menu (hook point: Bar, after the visible-items map), no
|
|
188
|
+
grouping, no cross-bar drag.
|
|
189
|
+
Styling: `--tb-*` tokens (tokens.css, mirror `tokens.tb`), classes `.wenayTb*` in style.css;
|
|
190
|
+
dark defaults, apps re-skin via `:root[data-theme]` like `--wnd-*` / `--dlg-*`.
|
|
191
|
+
|
|
192
|
+
Callback hub (for single-slot callback APIs `onX(cb | null)` whose subscribers silently
|
|
193
|
+
overwrite each other):
|
|
194
|
+
```
|
|
195
|
+
createCallbackHub<Args>(bind) -> {on, count}
|
|
196
|
+
```
|
|
197
|
+
`bind(emit)` runs lazily, ONCE, on the first `on()` - not at creation time, because the slot may
|
|
198
|
+
still be taken by app initialization. The first callback is registered before bind runs, so it
|
|
199
|
+
also catches synchronous emits. `on(cb) -> off`; off removes only that subscriber.
|
|
200
|
+
|
|
201
|
+
## columnState (createColumnState, `grid/columnState/`)
|
|
202
|
+
A persisted column layer over a keyed column set - order/visibility/width/sort/filter in a
|
|
203
|
+
standalone config store, plus an OPTIONAL two-way ag-grid adapter. Mobile card views consume the
|
|
204
|
+
SAME config with no ag-grid at all. agGrid4 is never modified: this is exactly the app-level
|
|
205
|
+
column wrapper WRAPPER.md postulates, packaged as a primitive; ag-grid enters only as a type
|
|
206
|
+
import + the GridApi handed to grid.attach().
|
|
207
|
+
```
|
|
208
|
+
createColumnState({key, columns: ColumnMeta[], def?, saveMs?=300})
|
|
209
|
+
-> {columns, api, grid: {attach(api), detach()}}
|
|
210
|
+
ColumnMeta = {key, title, short?, icon?, group?, fixed?, defaultVisible?, cardRole?: 'title'|'accent'}
|
|
211
|
+
ColumnsConfig= {v, order, visible, width, sort: {key, dir} | null, filter, groups}
|
|
212
|
+
api: {getConfig, setConfig, useConfig, onChange, reset, show(k,on), move(order), setSort(s|null),
|
|
213
|
+
toggleSort(k), visibleKeys(), getPresent, usePresent, isPresent(k), setPresent(keys|null), getPresentGate, setPresentGate(keys|null), listSource}
|
|
214
|
+
```
|
|
215
|
+
Persist + migration (same DNA as createToolbar/createUiSlot): config lives in `memoryGetOrCreate(key)`,
|
|
216
|
+
edits mutate it in place + renderBy + memoryMarkDirty, the APP saves via `memoryCache.onDirty`.
|
|
217
|
+
`normalize()` is the soft migration - unknown keys dropped, missing columns appended
|
|
218
|
+
default-visible, `fixed` pinned to its descriptor index; `v` covers incompatible shape changes.
|
|
219
|
+
The persisted object's IDENTITY is the subscription key, so `memoryCache.load()` must run BEFORE the grid
|
|
220
|
+
mounts (a grid attached pre-load would apply defaults; QA card 28 gates the grid on `loaded`).
|
|
221
|
+
|
|
222
|
+
Semantics that are easy to get wrong:
|
|
223
|
+
- STICKY sort: `sort` is independent of visibility and of any UI selection, may point at a hidden
|
|
224
|
+
OR grid-absent column, and changes only by an explicit toggle/header click. `readFromGrid` keeps
|
|
225
|
+
it when the live grid lacks that column (the grid cannot express a sort by a column it does not
|
|
226
|
+
have), so hiding or removing the sorted column never silently resets it.
|
|
227
|
+
- Two-way loop protection: store->grid runs under an `applying` flag; grid->store ignores events
|
|
228
|
+
while `applying`, with `source=='api'`, or `finished===false` (resize/move fire per animation
|
|
229
|
+
frame - only the final shape persists), debounced `saveMs`, and a JSON compare before commit. A
|
|
230
|
+
`fixed` column dragged off its slot in the grid: readFromGrid commits, normalize() pins it back,
|
|
231
|
+
and if the order changed the adapter re-applies to the grid (snap-back) so the two never disagree.
|
|
232
|
+
- Presence (`usePresent/isPresent/setPresent/setPresentGate`): runtime-only, NEVER persisted. It is
|
|
233
|
+
the intersection of actual live-grid columns and an optional app-level gate. The adapter maintains
|
|
234
|
+
actual presence on attach and on `gridColumnsChanged`; a column removed from the grid (dynamic
|
|
235
|
+
columnDefs) keeps its config entry and its menu button (rendered disabled), and when the set changes
|
|
236
|
+
the adapter RE-IMPOSES the config so a returning column regains its stored order/width/visibility
|
|
237
|
+
(setting columnDefs resets grid order). `setPresentGate(keys|null)` is for stable-schema app modes:
|
|
238
|
+
it marks gated-out buttons disabled and hides those grid columns through applyColumnState without
|
|
239
|
+
mutating persisted `visible`. No loop: applyColumnState never adds/removes columns. Null actual presence
|
|
240
|
+
and null gate together mean all present; if a gate is set, grid-less consumers still see that gate.
|
|
241
|
+
- `listSource` = the `{order, visible}` slice exposed as a `UiListSource` (Toolbar's
|
|
242
|
+
external-config contract): plug it into `createToolbar({source})` and the toolbar, the icon menu
|
|
243
|
+
and the grid all mirror one config. Its setConfig MERGES visible and re-appends via normalize, so
|
|
244
|
+
an editor over a SUBSET of columns never drops the rest.
|
|
245
|
+
- `filter`/`width` are written by the grid adapter only; `groups` (group key -> enabled
|
|
246
|
+
sub-columns) is modelled now; core group UI is still app-owned. QA card 30 demonstrates an app-level
|
|
247
|
+
mode button over stable grouped `columnDefs`; `presentGate` hides unavailable leaf columns and keeps their buttons disabled.
|
|
248
|
+
`visibleKeys()` additionally gates a grouped column by its group's enabled set.
|
|
249
|
+
- Attach from the consumer's `onGridReady` (AgGridTable forwards it over its own wiring) with
|
|
250
|
+
`autoSizeColumns={false}` (auto-fit at mount would rewrite stored widths); detach from
|
|
251
|
+
`onGridPreDestroyed` - the config outlives the grid (columnBuffer pattern). HMR caveat on the
|
|
252
|
+
stand: hot-reload recreates a module-level controller while the live grid stays attached to the
|
|
253
|
+
OLD instance, so store->grid needs an F5; in a real app the module runs once.
|
|
254
|
+
|
|
255
|
+
High-level kit (`createColumnGrid`, `grid/columnState/columnGrid.tsx`):
|
|
256
|
+
```
|
|
257
|
+
createColumnGrid({key, columnDefs?, columns?, data?, getId?, def?, saveMs?, toolbar?, autoSizeOnColumnCountChange?})
|
|
258
|
+
-> {state, toolbar, api, grid, tableProps, Table, Menu, Dots, Cards, Toolbar, Settings, View}
|
|
259
|
+
useColumnGrid(opts) -> same controller, captured once for component-local setups
|
|
260
|
+
```
|
|
261
|
+
- This is the default convenience layer for ordinary app grids that all need the same column menu.
|
|
262
|
+
It infers `ColumnMeta` from leaf `ColDef`s (`colId` -> `field`, title from `headerName` -> key)
|
|
263
|
+
and merges optional `columns` overrides for `fixed`, `short`, `icon`, `defaultVisible`, `cardRole`,
|
|
264
|
+
and group semantics. Unknown explicit overrides are appended, so grid-less/card-only fields still work.
|
|
265
|
+
- `Table` and `tableProps()` wrap `AgGridTable` and wire `state.grid.attach/detach` automatically.
|
|
266
|
+
Their default `autoSizeColumns=false` is deliberate: the wrapper is for persisted column layout, so
|
|
267
|
+
auto-fit must be opt-in. `autoSizeOnColumnCountChange` is a narrower opt-in: it calls
|
|
268
|
+
`sizeColumnsToFit()` only when `visibleKeys().length` changes after a columnState edit.
|
|
269
|
+
- `View` is a small representation switch (`mode:'table'|'cards'`) with controls
|
|
270
|
+
(`'menu'|'dots'|'toolbar'|false|'auto'`). `auto` resolves to the built-in dots overlay for both
|
|
271
|
+
table and card mode; the wrapper sets dot max to the full column count unless overridden. `Dots` is
|
|
272
|
+
not card-specific; on table mode it edits the same config and the attached grid applies visibility,
|
|
273
|
+
order, and sort through columnState. `data`/`getId` can live on the controller defaults or be
|
|
274
|
+
passed per `View` render.
|
|
275
|
+
- Use raw `createColumnState` plus `createToolbar` when the app needs a custom skin like QA card 30,
|
|
276
|
+
a runtime `presentGate`, special group mode buttons, or nonstandard persistence timing.
|
|
277
|
+
|
|
278
|
+
Icon menu (ColumnsMenu / MenuStrip, `grid/columnState/ColumnsMenu.tsx`):
|
|
279
|
+
```
|
|
280
|
+
<MenuStrip items={MenuStripItem[]} onItem? onMove? move? tail? holdMs?=150 compact? />
|
|
281
|
+
MenuStripItem = {key, title, short?, icon?, state: 'on'|'off'|'disabled', marks?, fixed?}
|
|
282
|
+
<ColumnsMenu state onItem? marks? tail? onTail? reorder?=true holdMs? compact? />
|
|
283
|
+
```
|
|
284
|
+
Two DECOUPLED layers, because "what a click means" is deliberately not the strip's business:
|
|
285
|
+
- `MenuStrip` is pure presentation - ordered buttons in three states plus a `marks` adornment
|
|
286
|
+
("on with extras": sub-columns, naming strategies...); it reports clicks (onItem) and
|
|
287
|
+
drag-reorders (onMove) but interprets neither. Reusable for ANY multi-state button strip.
|
|
288
|
+
`disabled` buttons report no clicks; `fixed` ones do not drag; `tail` = buttons after a divider,
|
|
289
|
+
OUTSIDE the reorder list (mode cyclers/actions). Reorder rides `useReorder` (second consumer
|
|
290
|
+
after the Toolbar editor) with the same fixed-pinning `move`, so preview == drop.
|
|
291
|
+
- `ColumnsMenu` binds it to a columnState: buttons follow config.order (the grid mirror is free -
|
|
292
|
+
both read the same config), state = disabled (absent from the live grid) / on / off, default
|
|
293
|
+
click = toggle visibility (overridable via `onItem` for multi-state columns), drag = api.move.
|
|
294
|
+
It is the compact/presentation surface. For settings-integrated bars that need density,
|
|
295
|
+
pseudo-controls, reset, and global Settings reuse, use `createToolbar({source: cs.api.listSource})`
|
|
296
|
+
as the canonical richer surface and optionally render a compact `ColumnsMenu` beside it.
|
|
297
|
+
- Click-vs-drag guard: a drag that ends on its origin button still fires a browser click; a click
|
|
298
|
+
that travelled >4px from mousedown is dropped, so a snapped-back drag never also toggles.
|
|
299
|
+
- `compact` = icon-only buttons: the icon, or the first letters of short/title as a text
|
|
300
|
+
pseudo-icon (full title in the tooltip), the same rule as `toolbarItemIcon`.
|
|
301
|
+
|
|
302
|
+
Mobile (no ag-grid, no storage - the config alone):
|
|
303
|
+
```
|
|
304
|
+
<ColumnDots state max?=8 className? style? /> // grid/columnState/ColumnDots.tsx
|
|
305
|
+
<CardList<Row> state data getId? renderValue? /> // grid/columnState/CardList.tsx
|
|
306
|
+
```
|
|
307
|
+
- `ColumnDots` is a discrete multi-thumb slider on pointer events (react-range cannot change thumb
|
|
308
|
+
count): a track of marks (one per column), dots on the shown columns. Gestures use a
|
|
309
|
+
dominant-axis test so a horizontal drag never removes and a vertical flick never reorders: drag a
|
|
310
|
+
dot along the track = the column takes another (empty) mark; swipe a dot UP = hide (fixed and the
|
|
311
|
+
last remaining dot stay); tap an empty mark = show (up to `max`); tap a dot without moving =
|
|
312
|
+
select the field; the sort button cycles asc->desc->off on the SELECTED field (sticky - selecting
|
|
313
|
+
another dot does not touch it). 44px touch targets.
|
|
314
|
+
- `CardList` renders rows as blocks: `visibleKeys()` become the card fields (created/removed live
|
|
315
|
+
as dots move), `cardRole:'title'` = the header (else the first visible key), `cardRole:'accent'`
|
|
316
|
+
= a badge, the rest are label+value. It sorts by the sticky `config.sort` itself
|
|
317
|
+
(numeric/locale comparator) even when the sorted column is hidden.
|
|
318
|
+
Styling uses shared classes `.wenayColDots*` / `.wenayCardList*`; runtime geometry remains inline because dot positions and drag transforms are state-derived. QA cards 28 (grid layer + F5 restore), 29 (mobile dots + cards), 30 (toolbar icon menu + grouped sub-column mode button), 31 (Toolbar over columnState.listSource + pseudo-icons), 32 (createColumnGrid wrapper + dots driving table/cards).
|
|
319
|
+
|
|
320
|
+
## Outside / Buttons Compatibility
|
|
321
|
+
```
|
|
322
|
+
useOutsideRef(options) -> ref // use useOutside(options).ref / .props
|
|
323
|
+
OutsideClickArea // alias of OutsideClickArea
|
|
324
|
+
|
|
325
|
+
StyleOtherRow
|
|
326
|
+
StyleOtherColumn
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
`Button`, `OutsideButton`, `HoverButton`, and `AbsoluteButton` are still direct components rather than hook controllers.
|
|
330
|
+
|
|
331
|
+
`Button` persists its open/closed status under `keyForSave` (module-lifetime; same naming as
|
|
332
|
+
FloatingWindow/Resizable/RightMenu); the old `keySave` prop is a deprecated alias (`keyForSave`
|
|
333
|
+
wins when both are set).
|
|
334
|
+
|
|
335
|
+
## Drag / Resize Low Level
|
|
336
|
+
```
|
|
337
|
+
FloatingWindowBase(props) // lower-level react-rnd wrapper
|
|
338
|
+
FloatingWindow(props) // canonical floating window component
|
|
339
|
+
useFloatingWindowController(options) // headless geometry/stack/drag/resize controller for custom chrome
|
|
340
|
+
floatingWindowMap // persisted RND map (ObservableMap)
|
|
341
|
+
FloatingWindowUpdate
|
|
342
|
+
FloatingWindowProps / FloatingWindowController / FloatingWindowSavedGeometry
|
|
343
|
+
|
|
344
|
+
DragBox(props) // delta-drag component; thin adapter over useDraggableApi (A7)
|
|
345
|
+
DragArea(props) // @deprecated: unique semantics kept as-is; prefer useDraggableApi/DragBox
|
|
346
|
+
FResizableReact(props)
|
|
347
|
+
mapResiReact // persisted resize map (ObservableMap)
|
|
348
|
+
OutlineDragDemo()
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
For new pointer logic, prefer:
|
|
352
|
+
```
|
|
353
|
+
const drag = useDraggableApi(...)
|
|
354
|
+
<div {...drag.bind} />
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
`useDraggable(...)` is kept as the simple hook wrapper; `useDraggableApi(...)` is the controller-style shape.
|
|
358
|
+
|
|
359
|
+
Drag primitives after the A7 pass (2026-07-10): `DragBox` is a thin adapter over
|
|
360
|
+
`useDraggableApi({holdMs: 0, trackState: false, onMove})` - same observable contract as its old
|
|
361
|
+
bespoke loop (immediate start, per-tick imperative `onX`/`onY` with the delta from the press
|
|
362
|
+
point, no re-render per move tick, `onStop` only after a real gesture, `last` ref shares the live
|
|
363
|
+
position object; pinned by `__test/dragBox.test.tsx`, QA card 35). `DragArea` stays untouched and
|
|
364
|
+
`@deprecated`: no consumers, and its semantics are unique (document.body listeners,
|
|
365
|
+
`stopPropagation` per move tick, ABSOLUTE coords, no preventDefault) - re-basing it would change
|
|
366
|
+
observable behavior; removal only in a breaking version. `useFloatingWindowController`'s header
|
|
367
|
+
loop and `StickerMenu` deliberately keep their own loops: viewport clamp + `e.buttons===1` release
|
|
368
|
+
recovery + persistence/z-stack, and mount-lifetime listeners + click-vs-drag suppression
|
|
369
|
+
respectively, are load-bearing behavior `useDraggableApi` does not model.
|
|
370
|
+
|
|
371
|
+
Reorder-by-drag (useReorder, `hooks/useReorder.tsx` - extracted from the Toolbar editor, which is
|
|
372
|
+
its first consumer):
|
|
373
|
+
```
|
|
374
|
+
useReorder({order, commit, move?, canDrag?, preview?: 'slots'|'measure', holdMs?})
|
|
375
|
+
-> {listRef, item(key) -> {props, style?, dragging, active}, dragKey, preview}
|
|
376
|
+
```
|
|
377
|
+
A deliberately SMALL reorder for keyed blocks laid out by CSS - the hook never knows the layout
|
|
378
|
+
(vertical list, horizontal bar, wrapped grid all work). Semantics:
|
|
379
|
+
- DOM order never changes mid-drag: the dragged block follows the pointer via transform, the rest
|
|
380
|
+
glide to their preview position (consumer adds the transition on `active && !dragging`), ONE
|
|
381
|
+
`commit(next)` on drop, skipped when nothing moved (a plain click is a no-op).
|
|
382
|
+
- Targeting = nearest slot center measured at drag START - never against the live layout
|
|
383
|
+
(re-measuring moving blocks oscillates at boundaries; designed out). Pointer deltas (viewport
|
|
384
|
+
px) are normalized by the container's scale factor (rect.width / offsetWidth), so
|
|
385
|
+
`transform: scale` / zoomed ancestors do not skew targeting.
|
|
386
|
+
- `move` is the simulated commit: the preview shows exactly `move(order, key, to)` - consumers
|
|
387
|
+
with pinning rules (Toolbar's fixed items) pass their own so preview == drop by construction.
|
|
388
|
+
- preview 'slots' (default) transforms between start slot centers - exact when blocks are
|
|
389
|
+
equal-sized. 'measure' is FLIP: the preview order is applied via CSS `order`, layout offsets
|
|
390
|
+
(offsetLeft/Top) are read and reverted in one synchronous pass (never paints), so wrapping with
|
|
391
|
+
ANY block sizes previews exactly; requires a flex/grid container; measured once per target
|
|
392
|
+
change. Offsets, NEVER getBoundingClientRect: rects include mid-flight TRANSITION values
|
|
393
|
+
(style.transform='none' does not stop a running transition within the same synchronous pass),
|
|
394
|
+
so rect-based re-measures accumulated the previous preview's shifts on every target change and
|
|
395
|
+
the blocks flew apart on target oscillation.
|
|
396
|
+
- Events from `input/button/select/textarea/a` never start a drag; mouse is left-button only;
|
|
397
|
+
touch works (`touch-action: none` on blocks is the consumer's CSS).
|
|
398
|
+
- Non-goals (use a ready-made dnd library instead): nesting, spans/collision packing,
|
|
399
|
+
autoscroll. QA card 26 is the live example. Cross-COLUMN moves live in useReorderBoard below.
|
|
400
|
+
|
|
401
|
+
Board (useReorderBoard, `hooks/useReorderBoard.tsx` - the columns extension of useReorder):
|
|
402
|
+
```
|
|
403
|
+
useReorderBoard({columns: [{key, items}], commit(next), canDrag?, holdMs?,
|
|
404
|
+
onDragStart?, onDragMove?, onOverChange?, onDragEnd?})
|
|
405
|
+
-> {columnRef(col) -> RefCallback, item(key), dragKey, over: {col, index} | null}
|
|
406
|
+
```
|
|
407
|
+
- Columns are plain consumer divs registered via `columnRef(key)` (live callback-ref registry -
|
|
408
|
+
ADDING a column is just consumer state + one more div, spliced at ANY position of the columns
|
|
409
|
+
array incl. the middle); a column's children must be exactly its items, 1:1, in order. The
|
|
410
|
+
column set/geometry is frozen for the duration of one drag.
|
|
411
|
+
- Column gravity is pure consumer CSS (`justify-content: flex-start` packs up, `flex-end` packs
|
|
412
|
+
down) - the hook never knows it, it measures the real layout. Do NOT use `flex-direction:
|
|
413
|
+
column-reverse` (it inverts DOM-vs-visual order; the 1:1 mapping breaks).
|
|
414
|
+
- Targeting: nearest column by clamped rect distance, insertion index = how many of that column's
|
|
415
|
+
START item centers sit above the dragged center (start geometry only - anti-oscillation, same
|
|
416
|
+
rule as useReorder). Preview = simulated commit (movedColumns), so preview == drop.
|
|
417
|
+
- Cross-column FLIP, offset-based like useReorder: the dragged block leaves flow via
|
|
418
|
+
display:none, survivors get preview CSS `order`, and the landing slot becomes a real
|
|
419
|
+
margin gap (draggedHeight + row-gap) on the neighbour - the column's own gravity then decides
|
|
420
|
+
who moves aside (a flex-end column slides the blocks ABOVE the slot up). Apply-read-revert in
|
|
421
|
+
one synchronous pass, cached per target slot.
|
|
422
|
+
- Callbacks: onDragStart (grab), onDragMove (every move, pointer delta in local px),
|
|
423
|
+
onOverChange (only when the target slot changes; compare prev.col != over.col for column
|
|
424
|
+
crossings), onDragEnd (final slot + committed flag; a plain click is committed=false).
|
|
425
|
+
`over`/`dragKey` are also returned reactively for render-time styling (column highlight).
|
|
426
|
+
- QA card 27 is the live example (5 columns, mixed gravity, empty column, add-column).
|
|
427
|
+
|
|
428
|
+
## Grid Row Utilities
|
|
429
|
+
```
|
|
430
|
+
applyGridRows(params)
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
Use `agGrid4` for new tables:
|
|
434
|
+
```
|
|
435
|
+
createGridBuffer()
|
|
436
|
+
useAgGrid()
|
|
437
|
+
AgGridTable
|
|
438
|
+
createColumnBuffer()
|
|
439
|
+
numericComparator()
|
|
440
|
+
colDefCentered
|
|
441
|
+
colDefWrap
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
Important agGrid4 contracts:
|
|
445
|
+
```
|
|
446
|
+
mirror mode: buffer owns row set; sync can add/update/remove grid rows.
|
|
447
|
+
overlay mode: rowData owns row set; sync updates only existing grid rows.
|
|
448
|
+
clean(): clears buffer; mirror also removes grid rows, overlay does not remove declarative rows.
|
|
449
|
+
flush(): React/controller method that calls ag-grid flushAsyncTransactions().
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Plain declarative `rowData` should preserve AgGridReact defaults unless the caller provides `getRowId`.
|
|
453
|
+
Buffered paths need a stable `getId`.
|
|
454
|
+
|
|
455
|
+
Dynamic columns:
|
|
456
|
+
```
|
|
457
|
+
createColumnBuffer().api.setNames(names) // exact set, dedupe preserving order
|
|
458
|
+
createColumnBuffer().api.apply() // replay last attached apply callback
|
|
459
|
+
createColumnBuffer().control.attach(api, {apply})
|
|
460
|
+
createColumnBuffer().control.detach() // keep names
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The primitive must not contain product group names, base `columnDefs`, visibility rules, or app naming policy.
|
|
464
|
+
|
|
465
|
+
## Params / Generated Editors
|
|
466
|
+
```
|
|
467
|
+
ParamsEditor({params, onChange, onExpand?, expandStatus?, expandStatusLvl?})
|
|
468
|
+
useParamsEditorController({params, onChange, onExpand?})
|
|
469
|
+
ParamsEdit({params, onSave}) // canonical compact editor
|
|
470
|
+
ParamsArrayEdit({params, onSave}) // array params editor
|
|
471
|
+
ParamRow({param, onClick, type?})
|
|
472
|
+
ParamLabelContent(name)
|
|
473
|
+
ParamToggleLabel(type, name)
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
`ParamsEditor` receives wenay-common2 `Params.IParamsExpandableReadonly` shape. `useParamsEditorController` owns the mutable draft clone, immediate/delayed notify, expand callback, and timer cleanup for custom renderers; `ParamsEditor` remains the compatibility visual wrapper.
|
|
477
|
+
|
|
478
|
+
## Menu Low Level
|
|
479
|
+
```
|
|
480
|
+
Menu({data, coordinate?, zIndex?, className?, menu?, menuElement?})
|
|
481
|
+
MenuElement
|
|
482
|
+
MenuProgress
|
|
483
|
+
MenuItemStrict / MenuItem
|
|
484
|
+
|
|
485
|
+
contextMenu.openAt(eventOrPoint, items, {source?, layerId?}) -> boolean
|
|
486
|
+
contextMenu.openAtPoint({x, y}, items, {source?, layerId?}) -> boolean
|
|
487
|
+
contextMenu.close()
|
|
488
|
+
contextMenu.getState() -> {open, items, point, source?, layerId?, seq}
|
|
489
|
+
contextMenu.stats.getSnapshot() -> {openAt, openAtPoint, legacyLayer, close, replace, empty, sources, layers, actionTotals, actions}
|
|
490
|
+
contextMenu.stats.reset()
|
|
491
|
+
contextMenu.stats.onChange(cb) -> off
|
|
492
|
+
<contextMenu.Layer zIndex? statusOn? other? className?>...</contextMenu.Layer>
|
|
493
|
+
contextMenu.map // legacy queue consumed by Layer; prefer openAt
|
|
494
|
+
createContextMenu({name?}) // custom isolated instance with its own state and stats
|
|
495
|
+
createRightClickMenu() // lower-level legacy right-click factory
|
|
496
|
+
|
|
497
|
+
DropdownMenu({elements, trigger?, classNames?, styles?, style?, position?, verticalPosition?, keyForSave?})
|
|
498
|
+
useRightMenuController({elements, style?, styles?, position?, verticalPosition?, keyForSave?})
|
|
499
|
+
createRightMenuController()
|
|
500
|
+
mapRightMenu // persisted floating-menu state (ObservableMap, RightMenuStore)
|
|
501
|
+
MenuRightPosition / MenuRightVerticalPosition / MenuRightSavedState / MenuRightRenderProps
|
|
502
|
+
MenuRightTrigger / MenuRightClassNames / MenuRightStyles / RightMenuController
|
|
503
|
+
StickerMenu // components/Menu re-export
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Prefer `contextMenu.openAt(e, items, {source?})` for new right-click integrations. `contextMenu.map` remains for older callers that queue items before Layer handles the right-click, and stays supported through `1.x`; it should not be the primary API in new code. `contextMenu.stats` is local in-memory diagnostics, not hidden analytics: it counts direct `openAt`, `openAtPoint`, legacy Layer queued opens, empty opens, close/replace, source/layer usage, aggregate action outcomes, and keyed action outcomes. It deliberately does not persist, send network requests, or store arbitrary item labels. Per-action stats require explicit `MenuItemStrict.actionKey`; unkeyed actions update `actionTotals` only.
|
|
507
|
+
|
|
508
|
+
`Menu` does not mutate `item.status`. Open/hover state is an internal active index; `status` remains a seed/compatibility value, and custom item renderers receive a view item whose `status` mirrors the current open state.
|
|
509
|
+
|
|
510
|
+
`DropdownMenu` is a floating action menu, not the main context-menu primitive. Its trigger glyph/content and visual classes/styles are caller-owned through `trigger`, `classNames`, and `styles`; the default still renders the old hamburger glyph and CSS classes. `useRightMenuController` is the hook/controller layer behind it for custom views; do not duplicate hover/fixed/submenu timers in product code.
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
## Observe / Listen React Hooks
|
|
514
|
+
Canonical React adapter names:
|
|
515
|
+
```
|
|
516
|
+
useStoreNode(node, {mode?, fallback?, drain?, key?})
|
|
517
|
+
useStoreKeys(node, {fallback?, drain?, key?}) // object-shape changes: add/delete keys
|
|
518
|
+
useStoreChangedPaths(changedPathsListen, {initial?, key?})
|
|
519
|
+
useStoreEach(store, cb, {enabled?=true}) // store.each() wrapper: cb(key, value, ctx) per CHANGED top-level key; undefined = deleted; root replace expands per key; renders nothing itself
|
|
520
|
+
useStoreSelect(selection, {fallback?, drain?, key?})
|
|
521
|
+
useStoreMirror(remote, initial, {mask, current?=true, drain?, key?, auto?=true, partial?, onError?})
|
|
522
|
+
useListenEffect(listen, cb, {current?, key?})
|
|
523
|
+
useListenArgs(listen, {initial?, current?, key?})
|
|
524
|
+
useListenValue(listen, {initial?, current?, key?, map?})
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
These hooks are intentionally in `wenay-react2`, not in `wenay-common2`.
|
|
528
|
+
Do not add React hooks for every helper exposed by `wenay-common2`; read the installed common2 package docs when needed, and document only the React-facing contract here. Hooks are useful only around subscriptions, lifecycle, or external resources.
|
|
529
|
+
Network sync stays explicit through `useStoreMirror`; reading a node must not secretly start RPC/fetch/WebSocket work. When `remote.changedPaths` exists and `partial !== false`, mirror uses common2 partial sync; otherwise it falls back to `remote.changed -> get(mask)`. `initial` seeds the mirror only. Structurally equal inline masks are stable, because StoreMask is expected to be a small plain object/tree.
|
|
530
|
+
|
|
531
|
+
QA stand coverage:
|
|
532
|
+
```
|
|
533
|
+
/__qa/observe-store/get // HTTP snapshot/mask read
|
|
534
|
+
/__qa/observe-store/mutate // HTTP server mutation, including deep leaf and deep key add/delete
|
|
535
|
+
/__qa/observe-store/events // SSE changed stream
|
|
536
|
+
/__qa/observe-store/events-paths // SSE changedPaths stream
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
## Replay React Hooks (details)
|
|
540
|
+
`src/common/src/hooks/useReplay.ts`. Client side of the wenay-common2 Replay stack only.
|
|
541
|
+
```
|
|
542
|
+
useReplaySubscribe(remote, cb, {since?, keepSeq?=true, enabled?=true, onSeq?, onError?, staleMs?, onStale?, policy?, hint?})
|
|
543
|
+
-> {ready, error, stale, seq(), lastTs(), restart(since?)}
|
|
544
|
+
useReplayRouteSubscribe(remote, cb, {since?, keepSeq?=true, enabled?=true, label?, onSeq?, onError?, onRoute?, policy?, hint?})
|
|
545
|
+
-> {ready, error, route, switching, seq(), label(), active(), switchRoute(nextRemote, opts?)}
|
|
546
|
+
useStoreReplaySync(store, remote, sameOpts) -> same controller // Observe.syncStoreReplay wrapper
|
|
547
|
+
useStoreReplayRouteSync(store, remote, routeOpts) -> route controller // Observe.syncStoreReplayRoute wrapper
|
|
548
|
+
useStoreReplayMirror(remote, initial, sameOpts) -> controller & {store} // creates the mirror store in a ref
|
|
549
|
+
useStoreReplayRouteMirror(remote, initial, routeOpts) -> route controller & {store}
|
|
550
|
+
useStoreReplayEach(remote, cb, sameOpts & {initial?, drain?}) -> controller & {store} // per-key fold: Observe.syncStoreReplayEach counterpart
|
|
551
|
+
useReplayFrame(remote, cb, {intervalMs?=300, since?, keepSeq?=true, enabled?=true, hint?, onSeq?, onError?})
|
|
552
|
+
-> {ready, error, seq(), pull(hint?), restart(since?)} // pull-at-own-pace over remote.frame()
|
|
553
|
+
useReplayHistory(history, apply, {head?, reset?, tickMs?=300, autoPlay?=true})
|
|
554
|
+
-> {live, seq, head, pause(), play(), seek({seq?|ts?})}
|
|
555
|
+
```
|
|
556
|
+
Semantics that are easy to get wrong:
|
|
557
|
+
- `cb`/`apply` go through refs: new function identity does NOT resubscribe. Resubscribe identity is `[remote, enabled, epoch, staleMs, policy]` — changing `staleMs` resubscribes (it is subscribe-time config in common2; under keepSeq the reconnect is a journal tail, so it is cheap); `policy` picks the wire surface (line vs frameLine), so it resubscribes too. `onStale` goes through a ref like `cb`.
|
|
558
|
+
- Route hand-off hooks (`useReplayRouteSubscribe`, `useStoreReplayRouteSync`, `useStoreReplayRouteMirror`) wrap common2 `Replay.replayRouteSubscribe` / `Observe.syncStoreReplayRoute`: `switchRoute(nextRemote, {label?, since?, reset?, policy?, hint?})` keeps the old route live, catches up the replacement from the last delivered `seq`, then closes the old route. Failed replacement leaves the old route active and surfaces `error` / `route.phase == "error"`. Changing the `remote` prop is still treated as a fresh subscription boundary; no-gap promotion/demotion is explicit through `switchRoute()`. common2 1.0.65 route handles expose `seq()`, `label()`, and `active()`, but not `isStale()` / `lastTs()`, so route hooks deliberately do not promise freshness yet.
|
|
559
|
+
- Lag policy / hint (frame model, common2 rev2): `policy: 'frame'` rides the server's `frameLine` when the remote has one — on lag the server drops for THIS client and recovers via the line's `frame` lambda (mini-frame); without a frameLine (old server, in-proc `exposeReplay`) common2 silently degrades to `'queue'`, so an in-proc QA of `'frame'` proves nothing — the wire test lives in common2 (`replay/rpc-auto.test.ts`). `hint` is opaque to the transport and goes through a ref: in `useReplaySubscribe` it is captured at subscribe time (used for the catch-up `frame()` call); in `useReplayFrame` it is read on EVERY pull, so a new identity is never missed and never resubscribes.
|
|
560
|
+
- `useReplayFrame` is the pull path: no live socket subscription at all; a timer calls `remote.frame(seq, hint)` and folds envelopes (seq-ascending, seen seq skipped — a keyframe recovery is just an envelope). Fresh start (no `since`) mirrors `replaySubscribe`'s catch-up for since<0: `keyframe()` is polled until the line has one (`frame(-1)` has no tail to return and THROWS on a still-empty line — a mount racing the producer's first event must not stick an error); a sacred line therefore NEEDS an explicit `since` (0 = full tail) — omitted, it waits forever. Overlapping pulls are never issued (a slow `frame()` skips ticks). Errors are LOUD and sticky: a reject (network, sacred line evicted past our seq, remote without `frame()`) stops the timer and sets `error` until `restart()` — deliberately no tail fallback here, that is `replaySubscribe`'s job. Freshness (`staleMs`) does not apply (the consumer owns the cadence).
|
|
561
|
+
- Freshness (`staleMs`): detection is 100% wenay-common2's watchdog (`ReplaySubscribeOpts.staleMs/onStale`, edge-triggered); the hook only mirrors the edges into `stale` state, so a fresh high-frequency line causes zero extra renders. `lastTs()` is a plain getter like `seq()` (producer ts of the last delivered event, 0 before the first delivery / while unsubscribed). Without `staleMs`: no watchdog, no state updates, `stale` stays `false`.
|
|
562
|
+
- Freshness on resubscribe/restart: the hook does NOT reset `stale` to `false` at effect start — it re-syncs from common2 after catch-up (mirrors the `onStale` edge fired during catch-up, plus `isStale()` once `ready` resolves), so a snapshot/tail carrying an old producer ts is stale from the first paint with no fresh-flicker. Caveat: common2's in-proc `keyframe()` stamps `ts` at request time, so a brand-new client on a stalled in-proc line reads as fresh at delivery and flips stale after `staleMs` via the arrival-gap watchdog; old-ts detection kicks in when the tail/keyframe actually carries producer timestamps (real relays, archives).
|
|
563
|
+
- `useReplayHistory` is archive playback — staleness intentionally does not apply there.
|
|
564
|
+
- `keepSeq`: within one mount, a resubscribe (StrictMode double effect, `enabled` toggle, `restart()`) reconnects with `{since: lastSeq}` -> journal tail. A NEW `remote` identity always resets seq (another line's seq is meaningless). A full unmount loses the refs: persist the position in the parent via `onSeq` and pass it back as `since` (QA card 23, client A).
|
|
565
|
+
- `seq()` and `useReplayHistory`'s interval-driven `seq/head` keep high-frequency lines from re-rendering per event. Frames/ticks should fold into a canvas/ref/store, not useState.
|
|
566
|
+
- `useReplayHistory.seek` kills the live subscription imperatively (guarded off, double-call safe), folds `history.at(where)` through `apply` (keyframe + tail; a keyframe fully redefines consumer state, so `reset` is rarely needed), and freezes. `play()` resubscribes `{since: pos}` -> archive tail -> live handover.
|
|
567
|
+
- `useStoreReplayEach` composes existing pieces instead of calling `Observe.syncStoreReplayEach` (which builds a FRESH store per call): the mirror lives in a ref (like `useStoreReplayMirror`), `store.each()` is subscribed in an effect declared BEFORE the sync effect (hook-call order guarantees the per-key subscriber exists when the keyframe applies — the expansion is never missed), then `useStoreReplaySync` drives the wire. Consequences: within one mount every resubscribe (StrictMode, `restart()`, `enabled` toggle, `staleMs`/`policy` change) reconnects by tail ON TOP of kept state and the consumer sees only the diff — no snapshot/`initial` dance (the library one-call needs `{since, initial: snapshot}` for that). After a FULL unmount pass `{since: prev.seq(), initial: prev.store.snapshot()}` like the library contract. `drain` is creation-time (it goes to `createStore`); changing it later does nothing. The fold target must live outside React state (ref/Map/grid api) — the hook renders nothing; `controller.store` is a normal store for `useStoreNode`/`useStoreKeys` extras.
|
|
568
|
+
- Server primitives (`conflateReplay` per connection, `archiveReplay`) intentionally have no hooks: they live where the RPC server is built.
|
|
569
|
+
- Route coordinator (common2 1.0.67 `Replay.createRouteCoordinator`): the coordinator owns promotion/fallback/shadow via policy hooks; consumers subscribe on a pair `link`, whose handle matches our route hooks (`ready, seq(), label(), active()`) minus `switchRoute` — the transport switch is invisible by the seq contract. Our route hooks (`useReplayRouteSubscribe` etc.) remain the manual-`switchRoute` wrapper over `replayRouteSubscribe`; they do NOT sit on top of a coordinator. React surface for coordinator links (subscribe hook + route-state/`promoteDirect` controls for ops UI) is a target-backlog item — do not bend the existing route hooks to accept links implicitly.
|
|
570
|
+
- WebRTC direct path (common2 1.0.68): `createWebRtcConnector({port, rtc, ...})` is the coordinator's direct `RouteConnector`; `rtc` is an injected factory and in the browser that injection (`() => new RTCPeerConnection(cfg)`) belongs to the app/React side — common2 stays lib.dom-free. Signaling rides the EXISTING rpc socket (`createSignalHub` port exposed by `createRpcServerAuto`), so no new connection management appears in React; channel death / revoke surfaces as loud `onError` on the line (never silence), which the existing hooks already map to `error`. Nothing here needs a hook by itself — it matters as the direct transport under a future adapter (target backlog).
|
|
571
|
+
- Peer SDK (common2 1.0.69 `Peer` / `wenay-common2/peer`): the one-call layer over rpc + store + replay + route coordinator — `createPeerClient({remote, account, initial, rtc?})` publishes the own store as a patch line and `peer(account)` returns a live mirrored store + route control; relay and direct share the OWNER's seq space (`createPatchRelayJournal`), so hand-offs are plain seq resumes, no keyframe reset. This is the intended React adoption surface (a `usePeer` adapter is the target-backlog item, superseding the raw coordinator-link hook idea): the mirrored store already plugs into `useStoreNode`/`useStoreKeys` as-is. 1.0.70 ships the oracle suites + `demo/` stand in the package — living examples per subsystem (`replay/peer-sdk.test.ts` is the Peer contract).
|
|
572
|
+
- Media capture (common2 1.0.66 `Media` namespace / `wenay-common2/media`): CONSUMPTION needs no new hooks — `Media.createAudioSource` / `Media.createVideoSource` return `[emit, listen] & control`, with `replay:true` the `listen` is a normal replay line, so `useReplaySubscribe` / `useReplayFrame` cover it; frames are one `Uint8Array` (40-byte fixed header + raw payload, `Media.decodeMediaFrame`) and follow the existing fold-outside-React rule (canvas/AudioContext via ref). The capture `control` side (`start()` resolving to `'idle'|'requesting'|'live'|'denied'|'no-device'|'error'`, `stop()`, `setDevice`, `listDevices`, stats) is a permission/device/lifecycle state machine — exactly hook-shaped; a `useMediaSource` capture hook + QA card is a recorded target goal (supersedes the earlier "wrap it app-locally" stance). Backpressure defaults live in common2 (audio = lossless queue, video `replay:true` = keep-latest frame recovery); the client-side knobs are the same `policy`/`hint` as any replay line.
|
|
573
|
+
|
|
574
|
+
QA cards 23/24/25/26 (`testUseReact/replayVideo.tsx`, all in-proc): synthetic 10fps jpeg-frame producer on `Replay.replayListen({history, current})`; client A = direct `exposeReplay` remote; client B = simulated slow wire (1 envelope per rateMs) behind `conflateReplay({pending: () => buf.length, highWater: 4, lowWater: 1, keyOf: () => "frame"})`; client C = `archiveReplay` + `openHistory` scrubber; client D = freshness (`staleMs: 2000`, `React.memo` + no tick, mounted inside a local `<StrictMode>`; the flat renders counter under growing frames is the no-per-event-render proof; "stall producer" toggles the emit interval, "new client" remounts by key for the stalled-mount case; card 24 has the same via `staleMs: 2500` on the mirror); client E = pull path (`useReplayFrame` over the direct remote with a wrapped counting `frame()`, pace switch 250ms/1s/3s keeps seq). `window.__replayVideoDemo` is exposed for debugging (wire.setRateMs, stats). Node-verified: slow wire delivered 12/36 envelopes yet converged to the last frame with bounded buffer (coalesced tail recovery); `syncStoreReplay` off() freezes the mirror and `{since}` resubscribe catches up by tail. Card 25 = per-key feed (`useStoreReplayEach` over `exposeStoreReplay`): a dict-of-rows store, producer touches ONE random row per tick, the fold target is a plain Map with per-row cb counters — only the mutated row's counter grows, keyframe/`replace` are the only whole-table expansions, delete arrives as `(key, undefined)`. Card 26 = route hand-off (`useReplayRouteSubscribe` over the same video line): one canvas starts on relay, switches direct/relay by `switchRoute`, and failed replacement keeps the previous route alive. Browser QA of throttling-sensitive behavior needs a VISIBLE tab: hidden-tab timer/effect throttling stalls the producer and delays passive effects (known stand caveat).
|
|
575
|
+
|
|
576
|
+
## Logs
|
|
577
|
+
Frequent global logger:
|
|
578
|
+
```
|
|
579
|
+
getLogsApi({limit?, limitPer, varMin?})
|
|
580
|
+
createLogsController({options, state?, onFullChange?, onMiniChange?, onSettingsChange?})
|
|
581
|
+
createLogsControllerState({full?, mini?, settings?})
|
|
582
|
+
logsApi
|
|
583
|
+
PageLogs({update?})
|
|
584
|
+
useMessageEventLogsController({maxVisible?})
|
|
585
|
+
MessageEventLogsView({controller, zIndex?, className?, style?})
|
|
586
|
+
MessageEventLogCard({logs})
|
|
587
|
+
MessageEventLogs({zIndex?}) // compatibility wrapper
|
|
588
|
+
LogsPage({update?})
|
|
589
|
+
useMiniLogsTable({data, onClick?, columnDefs?, defaultColDef?})
|
|
590
|
+
MiniLogsView({controller})
|
|
591
|
+
MiniLogsTable({data, onClick?, columnDefs?, defaultColDef?})
|
|
592
|
+
MiniLogs({data, onClick?}) // compatibility wrapper
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
React-context logger:
|
|
596
|
+
```
|
|
597
|
+
LogsProvider
|
|
598
|
+
useLogsContext()
|
|
599
|
+
useLogsTableController()
|
|
600
|
+
LogsTable()
|
|
601
|
+
useLogsNotificationsController()
|
|
602
|
+
LogsNotifications()
|
|
603
|
+
LogsSettings()
|
|
604
|
+
MainPage()
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
The context logger is a larger UI surface; the global `logsApi` is still the shorter integration point. `createLogsController` is the headless layer for append/limit/settings state; `useMessageEventLogsController` owns the global notification queue/timers/settings, while `MessageEventLogsView` and `MessageEventLogCard` own rendering. `PageLogs`, `MessageEventLogs`, and `LogsPage` remain compatibility UI wrappers. `useLogsTableController` and `useLogsNotificationsController` expose the provider-local table/notification state while `LogsTable` and `LogsNotifications` keep the visual wrappers. Shared logger chrome lives in `src/common/src/logs/logStyles.ts` and is themed through `--logs-*` tokens.
|
|
608
|
+
|
|
609
|
+
Full-page table controller (`useLogsPageTable` -> `LogsPageTableController`): the last logs table
|
|
610
|
+
moved to the controller pattern. Semantics that matter: the grid receives a MOUNT-TIME snapshot of
|
|
611
|
+
the accumulated log map once (`useState` initializer) and afterwards lives on
|
|
612
|
+
`applyTransactionAsync` appends from the mini feed - re-renders do not re-feed `rowData`, so row
|
|
613
|
+
identity is per-append copy exactly as before. The importance filter is ONE method
|
|
614
|
+
(`applyImportanceFilter(min?)`: set -> `setFilterModel` greaterThanOrEqual on `var`, unset ->
|
|
615
|
+
`destroyFilter`), used by both the settings subscription and `onGridReady` (which skips the
|
|
616
|
+
destroy branch - a fresh grid has no filter). Both data subscriptions are `updateBy(obj, cb)`
|
|
617
|
+
WITH a callback: they run imperatively on `renderBy` and never re-render the component.
|
|
618
|
+
`gridProps` is one stable memo - spread it into `AgGridTable`; `fit()` is exposed for the legacy
|
|
619
|
+
`update` prop, which `PageLogs` still honors. QA card 9.
|
|
620
|
+
|
|
621
|
+
## Cache / Memory / Browser Utilities
|
|
622
|
+
```
|
|
623
|
+
browserCacheStorage
|
|
624
|
+
localStorageCache
|
|
625
|
+
restoreDates(obj)
|
|
626
|
+
createCacheMapWithStorage(entries, save)
|
|
627
|
+
createCacheMap(entries)
|
|
628
|
+
|
|
629
|
+
ObservableMap<K,V> extends Map // set/delete/clear announce themselves; touch(key?)
|
|
630
|
+
.onChange(cb) -> off // announces an in-place mutation; MapChangeListener<K>
|
|
631
|
+
memoryCache.onDirty(cb) -> off // instance dirty channel (coalesced, async); DirtyListener
|
|
632
|
+
memoryCache.isDirty()
|
|
633
|
+
memoryCache.markDirty(scope?, key?) // manual announce, for plain-Map entries only
|
|
634
|
+
|
|
635
|
+
memorySet(key, data)
|
|
636
|
+
memoryGet(key)
|
|
637
|
+
memoryGetOrCreate(key, def, options?)
|
|
638
|
+
memoryGetById(key, def, id)
|
|
639
|
+
memoryUpdate(key, mutate)
|
|
640
|
+
memoryMarkDirty(key)
|
|
641
|
+
createSearchHistory({key, max?})
|
|
642
|
+
deepMergeWithMap(target, source)
|
|
643
|
+
structEqual(a, b) // deep equality for plain data trees; JSON.stringify-idiom
|
|
644
|
+
// tolerances (undefined props absent, NaN==NaN) WITHOUT
|
|
645
|
+
// its key-order sensitivity; not for Maps/Sets/cycles
|
|
646
|
+
memoryCache
|
|
647
|
+
memoryMaps
|
|
648
|
+
|
|
649
|
+
ArrayPromise({arr, catchF?, thenF?})
|
|
650
|
+
PageVisibilityProvider
|
|
651
|
+
PageVisibilityContext
|
|
652
|
+
setAutoStepForElement(input, {minStep?, maxStep?})
|
|
653
|
+
|
|
654
|
+
useCacheMapPersistence(cache: CacheMap, delay?=300) -> {isDirty(), flush(), save(), reload()}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
Layering (2026-07 architecture pass): the persisted maps (`floatingWindowMap`, `mapResiReact`,
|
|
658
|
+
`mapRightMenu`) are DECLARED in the utils leaf `utils/persistedMaps.ts` and re-exported by their
|
|
659
|
+
owning components (FloatingWindow/Resizable/RightMenuStore) - importing `memoryStore`/`memoryCache`
|
|
660
|
+
no longer pulls react-rnd or the Menu tree; only `import type` lines point upward (erased at build).
|
|
661
|
+
Shared order helper `utils/fixedOrder.ts` (`pinFixedOrder`, `movedOrderWithFixed`) is the single
|
|
662
|
+
implementation of the "fixed entries pin to descriptor index" invariant used by columnState,
|
|
663
|
+
ColumnsMenu, and Toolbar (it used to be four inline copies that had to stay byte-identical).
|
|
664
|
+
|
|
665
|
+
Cache helpers are process/browser storage utilities, not React state management. The one React
|
|
666
|
+
entry here is `useCacheMapPersistence` - the documented app contract (`load()` on mount +
|
|
667
|
+
`onDirty -> saveDebounced(delay)`) as a hook. Scope is deliberately exactly that: the
|
|
668
|
+
pagehide/visibilitychange flush backstops stay app-side (they are page-level policy, not
|
|
669
|
+
per-component). Effect identity is `[cache, delay]`; the returned methods are pass-throughs and
|
|
670
|
+
`reload()` is an alias of `load()` - a repeated load MERGES storage on top of the current maps
|
|
671
|
+
(addDataToMap semantics), it is not a reset. QA cards 20/21/25 share one `memoryCache`, so
|
|
672
|
+
several mounted hooks coexist: load() is idempotent-enough (merge) and extra onDirty
|
|
673
|
+
subscribers just debounce the same save.
|
|
674
|
+
|
|
675
|
+
Dirty/save contract: the dirty signal originates in the data layer. The persisted maps are
|
|
676
|
+
`ObservableMap`s, so `set/delete/clear` announce themselves; in-place mutations of stored
|
|
677
|
+
objects are invisible to map methods and are announced with `map.touch(key)` at the commit
|
|
678
|
+
points (FloatingWindow drag/resize stop, FResizableReact resize stop, createUiSlot.setPlace) or,
|
|
679
|
+
app-side, with `memoryUpdate(key, mutate)` / `memoryMarkDirty(key)`. `createCacheMapWithStorage`
|
|
680
|
+
subscribes to the ObservableMaps it owns - the channel is per instance, there is no global
|
|
681
|
+
bus; plain Maps in `arr` stay silent (announce those via `memoryCache.markDirty`). Announcements
|
|
682
|
+
never save anything: scope/key are event metadata, WHAT gets written is decided by the
|
|
683
|
+
save-side serialized-snapshot diff, so a missed announcement degrades to "saved later" and
|
|
684
|
+
an extra one to a no-op write. `isDirty()` is set synchronously; `onDirty` delivery is
|
|
685
|
+
coalesced through a microtask (safe to mutate maps inside render/init paths; not throttled
|
|
686
|
+
in background tabs the way timers are). `memoryCache.load()` ignores its own map events while
|
|
687
|
+
loading and resets dirty after; a save cycle waits out an in-flight load() (a save racing
|
|
688
|
+
a load could overwrite storage with in-memory defaults) and resets the flag at its START
|
|
689
|
+
so a change arriving mid-write survives. Caveat: `flush()` is async - on pagehide
|
|
690
|
+
localStorage writes usually complete but are not guaranteed (Cache API is not); prefer
|
|
691
|
+
visibilitychange->hidden as the primary final save.
|
|
692
|
+
|
|
693
|
+
## Resize Observer
|
|
694
|
+
```
|
|
695
|
+
CResizeObserver
|
|
696
|
+
setResizeableElement(el)
|
|
697
|
+
removeResizeableElement(el)
|
|
698
|
+
ObserveID
|
|
699
|
+
|
|
700
|
+
useResizeObserver<T>(onResize) -> {ref, element()}
|
|
701
|
+
useElementSize<T>() -> {ref, element(), width, height, getSize()}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
Use the class/imperative pair only when a component must participate in the shared resize
|
|
705
|
+
observer map. The hooks are the React-shaped entry over the SAME module singleton (one native
|
|
706
|
+
`ResizeObserver` per app): `useResizeObserver` returns a callback ref (attach/detach re-observes;
|
|
707
|
+
React null-call on unmount unsubscribes) and takes `onResize` through a ref - new identity
|
|
708
|
+
neither resubscribes nor is missed. The native observer fires once right after `observe`, so the
|
|
709
|
+
first measurement arrives without an explicit initial read. `useElementSize` folds that into
|
|
710
|
+
rounded, equality-guarded `{width, height}` state (a no-op resize does not re-render) plus a live
|
|
711
|
+
`getSize()` getter (exact floats, no render wait). `setResizeableElement`'s auto-shrink path is
|
|
712
|
+
untouched and still class-driven (ParamsEditor inputs). QA card 19 drives both hooks.
|
|
713
|
+
|
|
714
|
+
## Styles
|
|
715
|
+
```
|
|
716
|
+
tokens // TS mirror for inline styles and z-index
|
|
717
|
+
GridStyleDefault()
|
|
718
|
+
StyleGridDefault
|
|
719
|
+
StyleCSSHeadGridEdit(name, rules)
|
|
720
|
+
StyleCSSHeadGrid()
|
|
721
|
+
AgGridClassRule<T>
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
Style entry points:
|
|
725
|
+
- `src/style/tokens.css` - CSS custom properties shipped to consumers.
|
|
726
|
+
- `src/common/src/styles/tokens.ts` - TS mirror for inline styles, modal z-index, and ag-grid theme params. Values must stay aligned with `tokens.css`.
|
|
727
|
+
- `src/style/style.css` - shared component classes and token consumers.
|
|
728
|
+
- `src/style/menuRight.css` - right-menu classes and outline-demo CSS.
|
|
729
|
+
- `src/common/src/styles/styleGrid.ts` and `src/common/src/grid/agGrid4/theme.ts` - ag-grid theme setup from `tokens.grid`.
|
|
730
|
+
- QA stand: `npm run testReact -- --host 127.0.0.1 --port 3002`, entry `src/common/testUseReact/qa.tsx`.
|
|
731
|
+
|
|
732
|
+
Current tokenized prefixes:
|
|
733
|
+
- `--color-*` for base palette.
|
|
734
|
+
- `--menu-*` for mouse context-menu and right-menu visuals.
|
|
735
|
+
- `--wnd-*` for `FloatingWindow` chrome.
|
|
736
|
+
- `--dlg-*` for `SettingsDialog` chrome.
|
|
737
|
+
- `--tb-*` for toolbar chrome.
|
|
738
|
+
- `--logs-*` for global/context logger chrome.
|
|
739
|
+
- `--cols-menu-*` for compact `ColumnsMenu/MenuStrip` chrome.
|
|
740
|
+
- `--cols-dots-*` for `ColumnDots` chrome.
|
|
741
|
+
- `--cols-card-*` for `CardList` chrome.
|
|
742
|
+
- `--wenay-z-modal` for modal/overlay stacking.
|
|
743
|
+
|
|
744
|
+
Normalization rule: new shared CSS should first try an existing token. Add a new token only when a value is reused by a shared primitive or is expected to be theme-overridden by apps. One-off app/demo styles should stay in the demo/app wrapper, not in library tokens. Do not delete a default style/class without a replacement class/token path and changelog entry; visually broken defaults are treated as a compatibility break.
|
|
745
|
+
|
|
746
|
+
Open normalization candidates:
|
|
747
|
+
- `src/style/style.css`: `.msTradeAlt`, `.msTradeActive`, `.newButtonSimple`, `.toIndicatorMenuButton:hover`, submit-button green, and several toolbar row hover/drag literals still use raw colors.
|
|
748
|
+
- `src/common/src/grid/columnState/*`: compact menu/dots/card visuals now use `.wenayColumnGrid*`, `.wenayColsMenu*`, `.wenayColDots*`, `.wenayCardList*` plus `--cols-grid-*`, `--cols-menu-*`, `--cols-dots-*`, and `--cols-card-*`; further changes here should be visual QA only, not a new default palette.
|
|
749
|
+
- `src/common/src/components/ParamsEditor.tsx` and `src/common/src/components/Input.tsx`: if these stay public primitives, define default class/token contracts instead of component-owned visual styling.
|
|
750
|
+
- `src/common/src/styles/commentaryStyles.css`: standalone `.commentary` CSS is not imported by the root style bundle; either import/tokenize it if still used, or mark it as a local component concern.
|
|
751
|
+
|
|
752
|
+
Recently normalized: mouse context-menu item colors through `--menu-*`, `--menu-outline-color` for `OutlineDragDemo`, `--logs-*` for logger chrome, `--dlg-scrim` in `ModalProvider`, compact `ColumnsMenu/MenuStrip` visuals through `.wenayColsMenu*` / `--cols-menu-*`, card-29 mobile primitives through `--cols-dots-*` / `--cols-card-*`, and createColumnGrid overlay through `.wenayColumnGrid*` / `--cols-grid-*`.
|
|
753
|
+
|
|
754
|
+
## Cleanup Inventory
|
|
755
|
+
|
|
756
|
+
Do not delete a public export just because it looks unused inside this repo. External apps may import it. For cleanup, first move an item into this inventory, then decide in a separate breaking version whether it remains public, moves to a demo namespace, or is removed.
|
|
757
|
+
|
|
758
|
+
Suspicious but still public:
|
|
759
|
+
- `OutlineDragDemo`, `RightMenuDemo`, `ChartDemo` - demo/test-style exports. They are useful for the QA stand, but look like demo surface rather than core API.
|
|
760
|
+
- `StickerMenu` - exported from `components/Menu`; visually app-specific and should probably become an app wrapper or be documented as an example component.
|
|
761
|
+
- `logsApi` global logger and the context logger (`LogsProvider`, `LogsTable`, `LogsNotifications`, `LogsSettings`, `MainPage`) overlap. Keep both for now; document one as the short integration path and the other as the larger UI surface.
|
|
762
|
+
- Chart engine primitives (`DataSet`, `Panel`, `Renderer`, `Interaction`, `ChartEngine`, etc.) are very low-level. They are public through `chartEngineReact.tsx`; product apps should wrap them before use.
|
|
763
|
+
- `src/common/src/myChart/chartEngine/chartEngine.ts` is a reference copy next to the public React engine. It is not exported from root; treat it as suspicious maintenance debt, not as public API.
|
|
764
|
+
- `StyleCSSHeadGridEdit` and `StyleCSSHeadGrid` mutate `<head>` directly. They are exported and can have consumers, but new grid styling should prefer ag-grid theme params and tokens.
|
|
765
|
+
|
|
766
|
+
Added by the 2026-07-09 architecture audit (evidence in the audit report / `doc/target/my.md`):
|
|
767
|
+
- `menuR.tsx` (`createRightClickMenu` / `MenuR`) - CONFIRMED dead: no runtime consumer anywhere (the test that supposedly covers it imports `RightMenu.tsx`); re-exported twice from `api.tsx` (`export *` + `kit.menu.rightClick`). Candidate for removal in a deliberate breaking version; its gesture code duplicates `menuMouse.tsx:342-380`.
|
|
768
|
+
- `logsContext.tsx` - a full PARALLEL logs stack (own `LogEntry` shape divergent from `logsController`, raw unguarded `localStorage`, own settings/notifications/table) exported from the barrel next to `logs.tsx`. Decide: deprecate or converge; new code should use `logsApi`/`createLogsController`.
|
|
769
|
+
- `MyChartEngine` - a hardcoded DEMO (random data + setInterval inside `useEffect`, no props but `style`) exported as public API from `chartEngineReact.tsx`; `generateIncrementalData` (Math.random demo util) is public too. Should move to demo namespace or gain a real props contract.
|
|
770
|
+
- ~~`getLogsApi<T>()` shared module state~~ FIXED (A2, v1.0.41): every call now builds its own map/mini/settings (optional `settingsKey` persists per instance); the global `logsApi` explicitly injects the legacy shared state.
|
|
771
|
+
- ~~`FloatingWindow` `onCLickClose` typo~~ ALIASED (A10, v1.0.42): `onClickClose` added, typo prop is a deprecated alias; REMOVAL of the alias still needs a breaking pass.
|
|
772
|
+
- `DragArea` - `@deprecated` (A7, v1.0.42): no consumers; unique semantics kept as-is (see Drag / Resize Low Level). Removal in a deliberate breaking version.
|
|
773
|
+
- `Button` `keySave` prop - deprecated alias of `keyForSave` (A10, v1.0.42); removal in a breaking pass.
|
|
774
|
+
- `contextMenu` raw `map`/legacy-queue path is test-only-live (populated only by `contextMenuStats.test.tsx`); the `bb`/`map.clear()` reopening invariant itself is intentional and stays.
|
|
775
|
+
|
|
776
|
+
## Charts
|
|
777
|
+
Canvas chart:
|
|
778
|
+
```
|
|
779
|
+
createChartCanvas(config) -> IChartCanvas
|
|
780
|
+
ChartDemo()
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
Chart engine:
|
|
784
|
+
```
|
|
785
|
+
createDataSet(params)
|
|
786
|
+
createDataModel()
|
|
787
|
+
createPanelManager()
|
|
788
|
+
createRenderer()
|
|
789
|
+
createInteraction(...)
|
|
790
|
+
createChartEngine(canvas)
|
|
791
|
+
generateIncrementalData(...)
|
|
792
|
+
MyChartEngine
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
The chart engine exports many internal interfaces (`DataPoint`, `DataSet`, `Panel`, `Renderer`, `Transform`,
|
|
796
|
+
`Interaction`, `ChartEngine`). Treat them as low-level until an app wrapper fixes product-level behavior.
|
|
797
|
+
|
|
798
|
+
## Deprecated Naming Smells
|
|
799
|
+
```
|
|
800
|
+
Get* // usually a factory from older style; prefer create* or use*
|
|
801
|
+
*FuncJSX // imperative JSX store; prefer React context/controller
|
|
802
|
+
*2 / *3 // version suffix; document the intended canonical one in brief
|
|
803
|
+
removed old grid update names -> applyGridRows / agGrid4
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
Do not add new generic utilities with app words in their signature. For example, a column primitive should accept
|
|
807
|
+
`names` and `apply`, while a product wrapper decides group ids, column ids, and labels.
|