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