wenay-react2 1.0.45 → 1.0.46
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 +4 -0
- package/doc/changes/v1.0.46.md +16 -0
- package/doc/examples/README.md +6 -0
- package/doc/examples/peer-call-media.tsx +8 -0
- package/doc/examples/stand.tsx +6 -0
- package/doc/progress/common2-adoption-1.0.74.md +24 -0
- package/doc/progress/stand-as-examples-audit.md +22 -0
- package/doc/target/my.md +12 -0
- package/doc/wenay-react2.md +1 -1
- package/lib/common/demo/peerMedia.d.ts +6 -0
- package/lib/common/demo/peerMedia.js +128 -0
- package/lib/common/src/components/Toolbar/Toolbar.d.ts +4 -0
- package/lib/common/src/components/Toolbar/Toolbar.js +16 -7
- package/lib/common/src/components/index.d.ts +12 -0
- package/lib/common/src/components/index.js +12 -0
- package/lib/common/src/grid/columnState/CardList.d.ts +2 -0
- package/lib/common/src/grid/columnState/CardList.js +1 -1
- package/lib/common/src/grid/columnState/ColumnsMenu.d.ts +2 -0
- package/lib/common/src/grid/columnState/ColumnsMenu.js +3 -2
- package/lib/common/src/grid/columnState/columnState.d.ts +15 -0
- package/lib/common/src/grid/columnState/columnState.js +53 -5
- package/lib/common/src/hooks/index.d.ts +1 -0
- package/lib/common/src/hooks/index.js +1 -0
- package/lib/common/src/hooks/usePeerCall.d.ts +68 -0
- package/lib/common/src/hooks/usePeerCall.js +79 -0
- package/lib/common/src/hooks/useReorder.d.ts +2 -0
- package/lib/common/src/hooks/useReorder.js +3 -1
- package/lib/common/src/utils/memoryStore.d.ts +2 -2
- package/lib/common/testUseReact/qa.d.ts +1 -0
- package/lib/common/testUseReact/qa.js +1148 -0
- package/lib/common/testUseReact/replayVideo.d.ts +14 -0
- package/lib/common/testUseReact/replayVideo.js +311 -0
- package/lib/common/testUseReact/testParams.d.ts +1 -0
- package/lib/common/testUseReact/testParams.js +15 -0
- package/lib/common/testUseReact/useGrid.d.ts +2 -0
- package/lib/common/testUseReact/useGrid.js +62 -0
- package/lib/style/style.css +24 -0
- package/package.json +5 -3
|
@@ -0,0 +1,1148 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
/* qa.tsx - ONE board for manual library checks.
|
|
3
|
+
*
|
|
4
|
+
* Run: npm run testReact -- --host 127.0.0.1 --port 3002
|
|
5
|
+
* Each card: a live element + what to do + what is expected.
|
|
6
|
+
* Use the ✓/✗ buttons to mark results as you go. These are also the acceptance criteria for changes from REFACTOR_PLAN.md.
|
|
7
|
+
*/
|
|
8
|
+
import React, { useState, useMemo, useEffect, useLayoutEffect, useRef } from "react";
|
|
9
|
+
import { Menu, contextMenu, renderBy, updateBy, logsApi, MiniLogsTable, ParamsEdit, ParamsArrayEdit, ParamsEditor, ModalProvider, useModal, useKeyboard, keyboard, useAgGrid, AgGridTable, createGridBuffer, createColumnBuffer, createColumnState, createColumnGrid, ColumnsMenu, ColumnDots, CardList, useStoreMirror, useStoreNode, useStoreKeys, useStoreSelect, useStoreChangedPaths, useListenEffect, useListenArgs, useListenValue, SettingsDialog, registerSettingsSection, createUiSlot, createCallbackHub, createToolbar, registerToolbarDensity, useReorder, useReorderBoard, memoryCache, useCacheMapPersistence, useResizeObserver, useElementSize, useMediaSource, usePeer } from "../api";
|
|
10
|
+
import { listen as createListen, Observe, Params, Media, Peer } from "wenay-common2";
|
|
11
|
+
import { Button, HoverButton, OutsideClickArea } from "../src/hooks";
|
|
12
|
+
import { FloatingWindow } from "../src/components";
|
|
13
|
+
import { DragBox } from "../src/components/Dnd/FloatingWindow";
|
|
14
|
+
import { MyChartEngine } from "../src/myChart/chartEngine/chartEngineReact";
|
|
15
|
+
import { GridExample, tt } from "./useGrid";
|
|
16
|
+
import { TestParams } from "./testParams";
|
|
17
|
+
import { ReplayVideoDemo, ReplayRouteDemo, ReplayStoreDemo, ReplayStoreEachDemo } from "./replayVideo";
|
|
18
|
+
import { PeerCallDemo, PeerPresenceDemo, MediaRelayAclDemo, MediaRelayAudioDemo, PeerCallVideoAudioDemo } from "../demo/peerMedia";
|
|
19
|
+
/* ---------- card wrapper ---------- */
|
|
20
|
+
const card = { border: "1px solid #d0d7de", borderRadius: 10, margin: "14px 0", background: "#fff", overflow: "hidden", fontFamily: "system-ui, sans-serif" };
|
|
21
|
+
const head = { display: "flex", alignItems: "center", gap: 10, padding: "10px 14px", background: "#f6f8fa", borderBottom: "1px solid #d0d7de" };
|
|
22
|
+
const badge = { width: 24, height: 24, borderRadius: 12, background: "#0969da", color: "#fff", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 13, fontWeight: 700 };
|
|
23
|
+
const stageS = { padding: 14, position: "relative" };
|
|
24
|
+
const row = { padding: "6px 14px", fontSize: 13, lineHeight: 1.5, borderTop: "1px dashed #e1e4e8" };
|
|
25
|
+
const btn = (on, color) => ({ border: `1px solid ${color}`, background: on ? color : "#fff", color: on ? "#fff" : color, borderRadius: 6, padding: "3px 8px", fontSize: 12, cursor: "pointer" });
|
|
26
|
+
function Check(p) {
|
|
27
|
+
const [ok, setOk] = useState(null);
|
|
28
|
+
return (_jsxs("section", { style: { ...card, outline: ok === true ? "2px solid #1a7f37" : ok === false ? "2px solid #cf222e" : "none" }, children: [_jsxs("div", { style: head, children: [_jsx("span", { style: badge, children: p.n }), _jsx("b", { style: { flex: 1 }, children: p.title }), _jsx("button", { style: btn(ok === true, "#1a7f37"), onClick: () => setOk(true), children: "\u2713 works" }), _jsx("button", { style: btn(ok === false, "#cf222e"), onClick: () => setOk(false), children: "\u2717 bug" })] }), _jsx("div", { style: { ...stageS, minHeight: p.tall ? 340 : 80 }, children: p.children }), _jsxs("div", { style: row, children: [_jsx("b", { children: "Do:" }), " ", p.do] }), _jsxs("div", { style: { ...row, color: "#1a7f37" }, children: [_jsx("b", { children: "Expected:" }), " ", p.expect] }), p.note && _jsxs("div", { style: { ...row, color: "#9a6700" }, children: [_jsx("b", { children: "Note:" }), " ", p.note] })] }));
|
|
29
|
+
}
|
|
30
|
+
/* ---------- 1. Reactivity: updateBy / renderBy ---------- */
|
|
31
|
+
const shared = { count: 0 };
|
|
32
|
+
const Subscriber = () => { updateBy(shared); return _jsxs("span", { style: { fontSize: 22, fontWeight: 700 }, children: ["count = ", shared.count] }); };
|
|
33
|
+
const KeyDownDemo = () => {
|
|
34
|
+
const api = useKeyboard();
|
|
35
|
+
const [last, setLast] = useState(keyboard.get());
|
|
36
|
+
useEffect(() => keyboard.on((key) => setLast(key)), []);
|
|
37
|
+
return _jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }, children: [_jsxs("span", { children: ["Last key: ", _jsx("b", { style: { fontSize: 22 }, children: last || "-" })] }), _jsx("button", { onClick: () => api.reset(), children: "reset via API" })] });
|
|
38
|
+
};
|
|
39
|
+
const ReactivityDemo = () => (_jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center" }, children: [_jsx(Subscriber, {}), _jsx("button", { onClick: () => { shared.count++; renderBy(shared); }, children: "+1 and renderBy" }), _jsx("button", { onClick: () => { shared.count++; }, children: "+1 WITHOUT renderBy" }), _jsx("button", { onClick: () => renderBy(shared), children: "renderBy only" })] }));
|
|
40
|
+
// Outside click via OutsideClickArea directly: display:inline-block keeps the close zone wrapped around
|
|
41
|
+
// the content, with no full-width strip like Button+outClick.
|
|
42
|
+
// The popup uses position:absolute; otherwise it expands the wrapper rectangle, and a click to the right of the button
|
|
43
|
+
// (within the popup width) lands inside the wrapper itself, so contains() treats it as inside.
|
|
44
|
+
const OutsideDemo = () => {
|
|
45
|
+
const [open, setOpen] = useState(false);
|
|
46
|
+
return (_jsxs(OutsideClickArea, { status: open, outsideClick: () => setOpen(false), style: { display: "inline-block", position: "relative" }, children: [_jsx("div", { onClick: () => setOpen(v => !v), style: { display: "inline-block", padding: "6px 12px", border: "1px solid #6e7781", borderRadius: 6, cursor: "pointer", background: open ? "#6e7781" : "#fff", color: open ? "#fff" : "#000" }, children: "open" }), open && _jsx("div", { style: { position: "absolute", top: "100%", left: 0, marginTop: 8, padding: 16, width: 220, border: "1px solid #6e7781", borderRadius: 8, background: "#fafbfc", zIndex: 5 }, children: "Closes on clicks anywhere except this panel and the button" })] }));
|
|
47
|
+
};
|
|
48
|
+
// Logs: add a record with time:Date and check the time column, which used to be always empty.
|
|
49
|
+
const LogsDemo = () => {
|
|
50
|
+
const PageLogs = logsApi.React.PageLogs;
|
|
51
|
+
const MessageLogs = logsApi.React.Message;
|
|
52
|
+
const [miniClick, setMiniClick] = useState("none");
|
|
53
|
+
const miniRows = useMemo(() => [
|
|
54
|
+
{ time: new Date("2026-01-01T10:00:00"), id: "mini", var: 1, txt: "mini one", address: "qa" },
|
|
55
|
+
{ time: new Date("2026-01-01T10:00:01"), id: "mini", var: 2, txt: "mini two", address: "qa" },
|
|
56
|
+
], []);
|
|
57
|
+
return (_jsxs("div", { style: { position: "relative" }, children: [_jsx(MessageLogs, { zIndex: 80 }), _jsx("button", { style: { marginBottom: 8 }, onClick: () => logsApi.addLogs({ id: "demo", var: 1, time: new Date(), txt: "log " + new Date().toLocaleTimeString() }), children: "add log" }), _jsx("div", { style: { height: 260 }, children: _jsx(PageLogs, {}) }), _jsxs("div", { style: { marginTop: 12, fontSize: 12 }, children: ["MiniLogs click: ", miniClick] }), _jsx("div", { style: { height: 170, marginTop: 6 }, children: _jsx(MiniLogsTable, { data: miniRows, onClick: e => setMiniClick(String(e.data?.txt ?? "empty")) }) })] }));
|
|
58
|
+
};
|
|
59
|
+
// ParamsArrayEdit, which used to save pre-edit values, vs ParamsEdit, which is correct.
|
|
60
|
+
const paramsDefSave = new class extends Params.CParams {
|
|
61
|
+
test = { value: 1, range: { min: 1, max: 10, step: 1 } };
|
|
62
|
+
test2 = { value: 1, range: { min: 1, max: 10, step: 1 } };
|
|
63
|
+
};
|
|
64
|
+
const simpleSave = Params.GetSimpleParams(paramsDefSave);
|
|
65
|
+
const makeInfos = () => Params.mergeParamValuesToInfos(paramsDefSave, simpleSave);
|
|
66
|
+
const fmt = (d) => { try {
|
|
67
|
+
return JSON.stringify(d, null, 2);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return String(d);
|
|
71
|
+
} };
|
|
72
|
+
const ParamsSaveDemo = () => {
|
|
73
|
+
const [saved3, setSaved3] = useState("(not saved yet)");
|
|
74
|
+
const [saved2, setSaved2] = useState("(not saved yet)");
|
|
75
|
+
return (_jsxs("div", { style: { display: "flex", gap: 24 }, children: [_jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, marginBottom: 6, color: "#cf222e" }, children: "ParamsArrayEdit - expected BUG" }), _jsx(ParamsArrayEdit, { params: async () => [makeInfos()], onSave: (d) => { console.log("ParamsArrayEdit → onSave:", d); setSaved3(fmt(d)); } }), _jsx("div", { style: { fontSize: 12, marginTop: 6 }, children: "what was sent to onSave:" }), _jsx("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, maxHeight: 150, overflow: "auto", fontSize: 11 }, children: saved3 })] }), _jsxs("div", { style: { flex: 1, minWidth: 0 }, children: [_jsx("div", { style: { fontWeight: 700, marginBottom: 6, color: "#1a7f37" }, children: "ParamsEdit - correct" }), _jsx(ParamsEdit, { params: async () => makeInfos(), onSave: (d) => { console.log("ParamsEdit → onSave:", d); setSaved2(fmt(d)); } }), _jsx("div", { style: { fontSize: 12, marginTop: 6 }, children: "what was sent to onSave:" }), _jsx("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, maxHeight: 150, overflow: "auto", fontSize: 11 }, children: saved2 })] })] }));
|
|
76
|
+
};
|
|
77
|
+
const DebounceDemo = () => {
|
|
78
|
+
const [count, setCount] = useState(0);
|
|
79
|
+
const infos = useMemo(() => makeInfos(), []);
|
|
80
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { marginBottom: 8 }, children: ["onChange called: ", _jsx("b", { style: { fontSize: 18 }, children: count }), " times ", _jsx("button", { onClick: () => setCount(0), children: "reset" })] }), _jsx(ParamsEditor, { params: infos, onChange: () => setCount((c) => c + 1) })] }));
|
|
81
|
+
};
|
|
82
|
+
const agQABuffer = {}; // module-level buffer that survives grid remounts
|
|
83
|
+
const agQACols = [
|
|
84
|
+
{ field: "name", headerName: "Name" },
|
|
85
|
+
{ field: "price", headerName: "Price" },
|
|
86
|
+
];
|
|
87
|
+
const rndPrice = () => +(Math.random() * 1000).toFixed(2);
|
|
88
|
+
const AgGrid4Inner = () => {
|
|
89
|
+
const grid = useAgGrid({ externalBuffer: agQABuffer });
|
|
90
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => grid.update({ newData: [{ id: "tsla", name: "Tesla", price: rndPrice() }] }), children: "add/update Tesla" }), _jsx("button", { onClick: () => grid.update({ newData: [{ id: "aapl", name: "Apple", price: rndPrice() }] }), children: "add/update Apple" }), _jsx("button", { onClick: () => grid.update({ removeData: [{ id: "tsla" }] }), children: "remove Tesla" }), _jsx("button", { onClick: () => grid.fit(), children: "fit via API" })] }), _jsx("div", { style: { height: 220 }, children: _jsx(AgGridTable, { controller: grid, columnDefs: agQACols }) })] }));
|
|
91
|
+
};
|
|
92
|
+
const AgGrid4Demo = () => {
|
|
93
|
+
const [on, setOn] = useState(true);
|
|
94
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => setOn(v => !v), children: on ? "unmount grid" : "mount grid" }), _jsx("button", { onClick: () => { agQABuffer["msft"] = { id: "msft", name: "Microsoft (outside the grid)", price: rndPrice() }; }, children: "write MSFT directly to the buffer" })] }), on ? _jsx(AgGrid4Inner, {}) : _jsx("div", { style: { padding: 20, color: "#57606a" }, children: "grid is unmounted - buffer lives in the module" })] }));
|
|
95
|
+
};
|
|
96
|
+
const overlayColumns = [
|
|
97
|
+
{ field: "id", width: 90 },
|
|
98
|
+
{ field: "name" },
|
|
99
|
+
{ field: "price", headerName: "Stream price" },
|
|
100
|
+
{ field: "note" },
|
|
101
|
+
];
|
|
102
|
+
const AgGrid4OverlayDemo = () => {
|
|
103
|
+
const [showC, setShowC] = useState(false);
|
|
104
|
+
const [core] = useState(() => createGridBuffer({
|
|
105
|
+
getId: row => String(row.id),
|
|
106
|
+
mode: "overlay",
|
|
107
|
+
pushDefaults: { add: false },
|
|
108
|
+
}));
|
|
109
|
+
const grid = useAgGrid({ core });
|
|
110
|
+
const rows = useMemo(() => [
|
|
111
|
+
{ id: "a", name: "Alpha", note: "owned by rowData" },
|
|
112
|
+
{ id: "b", name: "Beta", note: "owned by rowData" },
|
|
113
|
+
...(showC ? [{ id: "c", name: "Gamma", note: "appears only after rowData adds it" }] : []),
|
|
114
|
+
], [showC]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
core.api.sync();
|
|
117
|
+
}, [core, rows]);
|
|
118
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => core.api.updateData({ newData: [{ id: "a", price: rndPrice() }] }), children: "stream update A" }), _jsx("button", { onClick: () => core.api.updateData({ newData: [{ id: "c", price: 777 }] }), children: "stream C before rowData" }), _jsx("button", { onClick: () => setShowC(v => !v), children: showC ? "remove C from rowData" : "add C to rowData" }), _jsx("button", { onClick: () => grid.sync(), children: "sync overlay" })] }), _jsx("div", { style: { height: 230 }, children: _jsx(AgGridTable, { controller: grid, rowData: rows, columnDefs: overlayColumns, getRowId: p => p.data.id }) })] }));
|
|
119
|
+
};
|
|
120
|
+
const dynamicRows = [
|
|
121
|
+
{ id: "btc", symbol: "BTCUSDT", alpha: 11, beta: 21, gamma: 31 },
|
|
122
|
+
{ id: "eth", symbol: "ETHUSDT", alpha: 12, beta: 22, gamma: 32 },
|
|
123
|
+
];
|
|
124
|
+
const dynamicBaseCols = [
|
|
125
|
+
{ field: "symbol", width: 130 },
|
|
126
|
+
{ headerName: "Dynamic", groupId: "qa-dynamic", children: [] },
|
|
127
|
+
];
|
|
128
|
+
const buildDynamicCol = (name) => ({
|
|
129
|
+
colId: `dynamic:${name}`,
|
|
130
|
+
field: name,
|
|
131
|
+
headerName: name,
|
|
132
|
+
width: 100,
|
|
133
|
+
});
|
|
134
|
+
function buildDynamicColumnDefs(names) {
|
|
135
|
+
return dynamicBaseCols.map(col => "children" in col && col.groupId == "qa-dynamic"
|
|
136
|
+
? { ...col, children: names.map(buildDynamicCol) }
|
|
137
|
+
: col);
|
|
138
|
+
}
|
|
139
|
+
const AgGrid4ColumnBufferDemo = () => {
|
|
140
|
+
const [columns] = useState(() => createColumnBuffer());
|
|
141
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => columns.api.setNames(["alpha"]), children: "alpha" }), _jsx("button", { onClick: () => columns.api.setNames(["alpha", "beta"]), children: "alpha + beta" }), _jsx("button", { onClick: () => columns.api.setNames(["gamma", "alpha", "gamma"]), children: "gamma + alpha" }), _jsx("button", { onClick: () => columns.api.setNames([]), children: "clear dynamic" })] }), _jsx("div", { style: { height: 230 }, children: _jsx(AgGridTable, { rowData: dynamicRows, getRowId: p => p.data.id, columnDefs: dynamicBaseCols, onGridReady: event => columns.control.attach(event.api, {
|
|
142
|
+
apply: ({ api, names }) => api.setGridOption("columnDefs", buildDynamicColumnDefs(names)),
|
|
143
|
+
}), onGridPreDestroyed: () => columns.control.detach() }) })] }));
|
|
144
|
+
};
|
|
145
|
+
const colStateRows = [
|
|
146
|
+
{ id: "a", name: "Alpha", price: 10.5, qty: 3 },
|
|
147
|
+
{ id: "b", name: "Beta", price: 22.1, qty: 7 },
|
|
148
|
+
{ id: "c", name: "Gamma", price: 5.8, qty: 1 },
|
|
149
|
+
];
|
|
150
|
+
const colStateCols = [
|
|
151
|
+
{ colId: "name", field: "name" },
|
|
152
|
+
{ colId: "price", field: "price" },
|
|
153
|
+
{ colId: "qty", field: "qty", headerName: "Qty" },
|
|
154
|
+
];
|
|
155
|
+
// module-level like a real app wrapper: the config survives grid remounts and reloads
|
|
156
|
+
const qaColumnState = createColumnState({
|
|
157
|
+
key: "qa28.columnState",
|
|
158
|
+
columns: [
|
|
159
|
+
{ key: "name", title: "Name", fixed: true },
|
|
160
|
+
{ key: "price", title: "Price" },
|
|
161
|
+
{ key: "qty", title: "Qty" },
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
const ColumnStateDemo = () => {
|
|
165
|
+
const [on, setOn] = useState(true);
|
|
166
|
+
const cfg = qaColumnState.api.useConfig();
|
|
167
|
+
return (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", gap: 8, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => setOn(v => !v), children: on ? "unmount grid" : "mount grid" }), _jsx("button", { onClick: () => qaColumnState.api.show("price", cfg.visible.price == false), children: cfg.visible.price == false ? "show price" : "hide price" }), _jsxs("button", { onClick: () => qaColumnState.api.toggleSort("price"), children: ["sort price: ", cfg.sort?.key == "price" ? cfg.sort.dir : "off"] }), _jsx("button", { onClick: () => qaColumnState.api.reset(), children: "reset" })] }), on ? (_jsx("div", { style: { height: 190 }, children: _jsx(AgGridTable, { rowData: colStateRows, getRowId: p => p.data.id, columnDefs: colStateCols, autoSizeColumns: false, onGridReady: e => qaColumnState.grid.attach(e.api), onGridPreDestroyed: () => qaColumnState.grid.detach() }) })) : (_jsx("div", { style: { padding: 20, color: "#57606a" }, children: "grid is unmounted - config lives in the module" })), _jsx("pre", { style: { fontSize: 11, background: "#f6f8fa", padding: 6, borderRadius: 6, margin: "8px 0 0", overflow: "auto" }, children: JSON.stringify({ order: cfg.order, visible: cfg.visible, width: cfg.width, sort: cfg.sort }, null, 1) })] }));
|
|
168
|
+
};
|
|
169
|
+
const mobRows = [
|
|
170
|
+
{ id: "btc", name: "BTCUSDT", price: 64230.5, qty: 3, ver: "v2", note: "spot", base: "USDT", change: "+1.8%", risk: "low" },
|
|
171
|
+
{ id: "eth", name: "ETHUSDT", price: 3120.2, qty: 12, ver: "v3", note: "spot", base: "USDT", change: "+0.7%", risk: "mid" },
|
|
172
|
+
{ id: "sol", name: "SOLUSDT", price: 148.9, qty: 40, ver: "v1", note: "futures", base: "USDT", change: "-2.1%", risk: "high" },
|
|
173
|
+
{ id: "ada", name: "ADAUSDT", price: 0.44, qty: 900, ver: "v2", note: "spot", base: "USDT", change: "+0.2%", risk: "low" },
|
|
174
|
+
{ id: "dot", name: "DOTUSDT", price: 6.8, qty: 150, ver: "v4", note: "futures", base: "USDT", change: "-0.9%", risk: "mid" },
|
|
175
|
+
{ id: "xrp", name: "XRPUSDT", price: 0.52, qty: 700, ver: "v1", note: "spot", base: "USDT", change: "+3.0%", risk: "mid" },
|
|
176
|
+
];
|
|
177
|
+
const qaMobColumns = createColumnState({
|
|
178
|
+
key: "qa29.mobileColumns",
|
|
179
|
+
columns: [
|
|
180
|
+
{ key: "name", title: "Symbol", short: "sym", fixed: true, cardRole: "title" },
|
|
181
|
+
{ key: "price", title: "Price", short: "price" },
|
|
182
|
+
{ key: "qty", title: "Quantity", short: "qty", defaultVisible: false },
|
|
183
|
+
{ key: "ver", title: "Version", short: "ver", cardRole: "accent", defaultVisible: false },
|
|
184
|
+
{ key: "note", title: "Note", short: "note", defaultVisible: false },
|
|
185
|
+
{ key: "base", title: "Base", short: "base", defaultVisible: false },
|
|
186
|
+
{ key: "change", title: "Change", short: "chg", defaultVisible: false },
|
|
187
|
+
{ key: "risk", title: "Risk", short: "risk", defaultVisible: false },
|
|
188
|
+
],
|
|
189
|
+
});
|
|
190
|
+
const qa29MobileCss = `
|
|
191
|
+
.qa29MobileShell {
|
|
192
|
+
position: relative;
|
|
193
|
+
max-width: 420px;
|
|
194
|
+
}
|
|
195
|
+
.qa29MobileCards .wenayCardListItem:last-child {
|
|
196
|
+
padding-bottom: 92px;
|
|
197
|
+
}
|
|
198
|
+
.qa29MobileDots {
|
|
199
|
+
position: absolute;
|
|
200
|
+
left: 10px;
|
|
201
|
+
right: 10px;
|
|
202
|
+
bottom: 10px;
|
|
203
|
+
z-index: 3;
|
|
204
|
+
padding: 6px 8px 8px;
|
|
205
|
+
border: 1px solid transparent;
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
background: transparent;
|
|
208
|
+
box-shadow: none;
|
|
209
|
+
backdrop-filter: none;
|
|
210
|
+
}
|
|
211
|
+
.qa29MobileDots .wenayColDotsHead {
|
|
212
|
+
justify-content: flex-end;
|
|
213
|
+
gap: 0;
|
|
214
|
+
margin-bottom: 2px;
|
|
215
|
+
font-size: 11px;
|
|
216
|
+
}
|
|
217
|
+
.qa29MobileDots .wenayColDotsTrack {
|
|
218
|
+
height: 48px;
|
|
219
|
+
margin: 0 16px;
|
|
220
|
+
}
|
|
221
|
+
.qa29MobileDots .wenayColDotsMeta,
|
|
222
|
+
.qa29MobileDots .wenayColDotsSpacer {
|
|
223
|
+
display: none;
|
|
224
|
+
}
|
|
225
|
+
.qa29MobileDots .wenayColDotsSort {
|
|
226
|
+
width: 32px;
|
|
227
|
+
height: 28px;
|
|
228
|
+
padding: 0;
|
|
229
|
+
font-size: 0;
|
|
230
|
+
opacity: 0.82;
|
|
231
|
+
background: transparent;
|
|
232
|
+
}
|
|
233
|
+
.qa29MobileDots .wenayColDotsSort::before {
|
|
234
|
+
content: "⇅";
|
|
235
|
+
font-size: 16px;
|
|
236
|
+
line-height: 1;
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
const MobileColumnsDemo = () => (_jsxs("div", { className: "qa29MobileShell", children: [_jsx("style", { children: qa29MobileCss }), _jsx(CardList, { state: qaMobColumns, data: mobRows, getId: r => r.id, layout: "compact", className: "qa29MobileCards" }), _jsx(ColumnDots, { state: qaMobColumns, max: 8, className: "qa29MobileDots" })] }));
|
|
240
|
+
const menuRows = [
|
|
241
|
+
{ id: "a", name: "Alpha", price: 10.5, qty: 3, fee: 0, note: "", blockText: "red", blockZero: 0, blockEmpty: "" },
|
|
242
|
+
{ id: "b", name: "Beta", price: 22.1, qty: 0, fee: 0, note: "", blockText: "green", blockZero: 0, blockEmpty: "" },
|
|
243
|
+
{ id: "c", name: "Gamma", price: 5.8, qty: 1, fee: 0, note: "", blockText: "blue", blockZero: 0, blockEmpty: "" },
|
|
244
|
+
];
|
|
245
|
+
const menuBaseColDefs = [
|
|
246
|
+
{ colId: "name", field: "name" },
|
|
247
|
+
{ colId: "price", field: "price" },
|
|
248
|
+
{ colId: "qty", field: "qty", headerName: "Qty" },
|
|
249
|
+
{ colId: "fee", field: "fee" },
|
|
250
|
+
{ colId: "note", field: "note" },
|
|
251
|
+
];
|
|
252
|
+
const qa30BlockGroup = "modeBlock";
|
|
253
|
+
const qa30BlockColumns = [
|
|
254
|
+
{ colId: "blockText", field: "blockText", headerName: "Values" },
|
|
255
|
+
{ colId: "blockZero", field: "blockZero", headerName: "Zeros" },
|
|
256
|
+
{ colId: "blockEmpty", field: "blockEmpty", headerName: "Empty" },
|
|
257
|
+
];
|
|
258
|
+
const qaMenuState = createColumnState({
|
|
259
|
+
key: "qa30.columnsMenu",
|
|
260
|
+
columns: [
|
|
261
|
+
{ key: "name", title: "Name", fixed: true },
|
|
262
|
+
{ key: "price", title: "Price (has sub-fields)", short: "price" },
|
|
263
|
+
{ key: "qty", title: "Quantity", short: "qty" },
|
|
264
|
+
{ key: "fee", title: "Fee", short: "fee" },
|
|
265
|
+
{ key: "note", title: "Note", short: "note" },
|
|
266
|
+
{ key: "blockText", title: "Block / Values", short: "val", group: qa30BlockGroup },
|
|
267
|
+
{ key: "blockZero", title: "Block / Zeros", short: "zero", group: qa30BlockGroup },
|
|
268
|
+
{ key: "blockEmpty", title: "Block / Empty", short: "empty", group: qa30BlockGroup },
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
// The grouped block's mode is an app-level layer above columnState: columnDefs
|
|
272
|
+
// stay stable, and a runtime presentGate hides/disables unavailable leaf columns.
|
|
273
|
+
const qa30BlockModeState = { i: 0 };
|
|
274
|
+
const menuValues = (key) => menuRows.map(r => r[key]);
|
|
275
|
+
const menuColumnEmpty = (key, zeroIsEmpty) => menuValues(key).every(v => v == null || v === "" || (zeroIsEmpty && v === 0));
|
|
276
|
+
const qa30BlockModes = [
|
|
277
|
+
{ hint: "mode 1/4: show all 3 sub-columns", show: (_) => true },
|
|
278
|
+
{ hint: "mode 2/4: hide empty sub-columns, 0 counts as empty", show: (key) => !menuColumnEmpty(key, true) },
|
|
279
|
+
{ hint: "mode 3/4: hide empty sub-columns, 0 is a value", show: (key) => !menuColumnEmpty(key, false) },
|
|
280
|
+
{ hint: "mode 4/4: skip the whole group block", show: (_) => false },
|
|
281
|
+
];
|
|
282
|
+
function qa30BlockMode() {
|
|
283
|
+
return qa30BlockModes[qa30BlockModeState.i];
|
|
284
|
+
}
|
|
285
|
+
function qa30BlockVisible(mode = qa30BlockMode()) {
|
|
286
|
+
return qa30BlockColumns.some(d => mode.show(String(d.colId)));
|
|
287
|
+
}
|
|
288
|
+
const qa30MenuColumnDefs = [
|
|
289
|
+
...menuBaseColDefs,
|
|
290
|
+
{ headerName: "Mode block", groupId: qa30BlockGroup, children: qa30BlockColumns },
|
|
291
|
+
];
|
|
292
|
+
const qa30BaseColumnKeys = menuBaseColDefs.map(d => String(d.colId));
|
|
293
|
+
function qa30PresentKeys(mode = qa30BlockMode()) {
|
|
294
|
+
return [
|
|
295
|
+
...qa30BaseColumnKeys,
|
|
296
|
+
...qa30BlockColumns.filter(d => mode.show(String(d.colId))).map(d => String(d.colId)),
|
|
297
|
+
];
|
|
298
|
+
}
|
|
299
|
+
function applyQa30BlockMode(mode = qa30BlockMode()) {
|
|
300
|
+
qaMenuState.api.setPresentGate(qa30PresentKeys(mode));
|
|
301
|
+
}
|
|
302
|
+
function nextQa30BlockMode() {
|
|
303
|
+
qa30BlockModeState.i = (qa30BlockModeState.i + 1) % qa30BlockModes.length;
|
|
304
|
+
applyQa30BlockMode();
|
|
305
|
+
renderBy(qa30BlockModeState);
|
|
306
|
+
}
|
|
307
|
+
const qa30BlockModeItem = { title: "Block display mode" };
|
|
308
|
+
const qa30SquareButtonBase = {
|
|
309
|
+
boxSizing: "border-box",
|
|
310
|
+
display: "inline-flex",
|
|
311
|
+
alignItems: "center",
|
|
312
|
+
justifyContent: "center",
|
|
313
|
+
gap: 3,
|
|
314
|
+
padding: "7px 11px",
|
|
315
|
+
borderRadius: 0,
|
|
316
|
+
margin: 0,
|
|
317
|
+
fontSize: 13,
|
|
318
|
+
lineHeight: "16px",
|
|
319
|
+
fontWeight: 400,
|
|
320
|
+
userSelect: "none",
|
|
321
|
+
whiteSpace: "nowrap",
|
|
322
|
+
transition: "background-color 120ms ease, border-color 120ms ease, color 120ms ease",
|
|
323
|
+
};
|
|
324
|
+
const qa30SquareGlyphStyle = {
|
|
325
|
+
display: "inline-flex",
|
|
326
|
+
alignItems: "center",
|
|
327
|
+
justifyContent: "center",
|
|
328
|
+
minWidth: 0,
|
|
329
|
+
fontWeight: 400,
|
|
330
|
+
letterSpacing: 0,
|
|
331
|
+
};
|
|
332
|
+
function qa30SquareGlyph(item, density) {
|
|
333
|
+
if (density == "icon") {
|
|
334
|
+
if (item.icon != null)
|
|
335
|
+
return _jsx("span", { style: qa30SquareGlyphStyle, children: item.icon });
|
|
336
|
+
return _jsx("span", { style: qa30SquareGlyphStyle, children: (item.short ?? item.title).slice(0, 3).toUpperCase() });
|
|
337
|
+
}
|
|
338
|
+
const text = density == "label" ? (item.short ?? item.title) : item.title;
|
|
339
|
+
return _jsx("span", { style: qa30SquareGlyphStyle, children: text });
|
|
340
|
+
}
|
|
341
|
+
const Qa30SquareButton = (p) => {
|
|
342
|
+
const [hover, setHover] = useState(false);
|
|
343
|
+
const interactive = (!!p.onClick || p.clickable) && !p.disabled;
|
|
344
|
+
const state = p.disabled
|
|
345
|
+
? { background: "#0b1020", border: "1px dashed #526179", color: "#738096", opacity: 0.72 }
|
|
346
|
+
: p.pressed
|
|
347
|
+
? { background: "#f8fafc", border: "1px solid #f8fafc", color: "#0f172a" }
|
|
348
|
+
: hover
|
|
349
|
+
? { background: "#172238", border: "1px solid #f8fafc", color: "#f8fafc" }
|
|
350
|
+
: { background: "#111a2c", border: "1px solid #34445f", color: "#d8e7ff" };
|
|
351
|
+
return _jsx("div", { title: p.title, role: p.onClick && !p.disabled ? "button" : undefined, tabIndex: p.onClick && !p.disabled ? 0 : undefined, "aria-pressed": p.pressed, onMouseEnter: () => setHover(true), onMouseLeave: () => setHover(false), onClick: p.onClick && !p.disabled ? p.onClick : undefined, onKeyDown: p.onClick && !p.disabled ? e => {
|
|
352
|
+
if (e.key != "Enter" && e.key != " ")
|
|
353
|
+
return;
|
|
354
|
+
e.preventDefault();
|
|
355
|
+
p.onClick?.(e);
|
|
356
|
+
} : undefined, style: {
|
|
357
|
+
...qa30SquareButtonBase,
|
|
358
|
+
...state,
|
|
359
|
+
cursor: interactive ? "pointer" : "default",
|
|
360
|
+
...(p.fixed ? { borderColor: "#9db3d1" } : null),
|
|
361
|
+
}, children: p.children });
|
|
362
|
+
};
|
|
363
|
+
const Qa30ModeDots = () => {
|
|
364
|
+
const count = qa30BlockModeState.i + 1;
|
|
365
|
+
return _jsx("span", { "aria-hidden": true, style: { display: "inline-flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 1, marginLeft: 1 }, children: Array.from({ length: count }, (_, i) => _jsx("span", { style: { width: 3, height: 3, borderRadius: 0, background: "currentColor", opacity: 0.95 } }, i)) });
|
|
366
|
+
};
|
|
367
|
+
const BlockModeButton = (p) => {
|
|
368
|
+
updateBy(qa30BlockModeState);
|
|
369
|
+
const mode = qa30BlockMode();
|
|
370
|
+
const visible = qa30BlockVisible(mode);
|
|
371
|
+
return _jsxs(Qa30SquareButton, { title: visible ? mode.hint : `${mode.hint}; block is not displayed`, pressed: visible, clickable: true, children: [qa30SquareGlyph(qa30BlockModeItem, p.density), _jsx(Qa30ModeDots, {})] });
|
|
372
|
+
};
|
|
373
|
+
// Client skin over the toolbar config: createToolbar owns order/membership/density,
|
|
374
|
+
// while this stand draws the face as square-edged content tiles. State: pressed = column
|
|
375
|
+
// visible in the grid, unpressed = hidden, dashed = removed by the block mode
|
|
376
|
+
// (present=false). Click toggles the column's grid visibility.
|
|
377
|
+
const MenuStateButton = (p) => {
|
|
378
|
+
const cfg = p.state.api.useConfig();
|
|
379
|
+
const present = p.state.api.usePresent();
|
|
380
|
+
const col = p.state.columns.find(c => c.key === p.colKey);
|
|
381
|
+
if (!col)
|
|
382
|
+
return null;
|
|
383
|
+
const disabled = !!present && !present[p.colKey];
|
|
384
|
+
const on = cfg.visible[p.colKey] != false;
|
|
385
|
+
return _jsx(Qa30SquareButton, { title: col.title, pressed: on, disabled: disabled, fixed: col.fixed, clickable: !disabled && !col.fixed, children: qa30SquareGlyph(col, p.density) });
|
|
386
|
+
};
|
|
387
|
+
// A "card 25" toolbar (createToolbar) whose ITEMS are those on/off buttons.
|
|
388
|
+
// sourceMode:'order' lets columnState own only the real-column order while the
|
|
389
|
+
// toolbar keeps MENU MEMBERSHIP local. The button's pressed/unpressed state is
|
|
390
|
+
// still the grid visibility from qaMenuState, and blockMode is a local extra item
|
|
391
|
+
// that is not pushed into the grid order.
|
|
392
|
+
const qa30MenuToolbar = createToolbar({
|
|
393
|
+
key: "qa30.menuToolbar",
|
|
394
|
+
items: [
|
|
395
|
+
...qaMenuState.columns.map(c => ({
|
|
396
|
+
key: c.key, title: c.title, short: c.short, icon: c.icon, fixed: c.fixed,
|
|
397
|
+
onClick: () => {
|
|
398
|
+
const present = qaMenuState.api.getPresent();
|
|
399
|
+
if (c.fixed || (!!present && !present[c.key]))
|
|
400
|
+
return;
|
|
401
|
+
const cfg = qaMenuState.api.getConfig();
|
|
402
|
+
qaMenuState.api.show(c.key, cfg.visible[c.key] == false);
|
|
403
|
+
},
|
|
404
|
+
render: (density) => _jsx(MenuStateButton, { state: qaMenuState, colKey: c.key, density: density }),
|
|
405
|
+
})),
|
|
406
|
+
{ key: "blockMode", title: "Block display mode", onClick: () => nextQa30BlockMode(), render: (density) => _jsx(BlockModeButton, { density: density }) },
|
|
407
|
+
],
|
|
408
|
+
source: qaMenuState.api.listSource,
|
|
409
|
+
sourceMode: "order",
|
|
410
|
+
});
|
|
411
|
+
const Qa30ResetIcon = () => _jsx("svg", { width: "12", height: "12", viewBox: "0 0 16 16", "aria-hidden": "true", children: _jsx("path", { d: "M3.2 5.5 A5.2 5.2 0 1 1 4.1 11.7 M3.2 5.5 H6.4 M3.2 5.5 V2.3", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) });
|
|
412
|
+
const qa30ToolbarSkinCss = `
|
|
413
|
+
.qa30MenuSkin {
|
|
414
|
+
display: inline-flex;
|
|
415
|
+
align-items: stretch;
|
|
416
|
+
flex-wrap: wrap;
|
|
417
|
+
gap: 1px;
|
|
418
|
+
background: transparent;
|
|
419
|
+
border: 0;
|
|
420
|
+
border-radius: 0;
|
|
421
|
+
color: inherit;
|
|
422
|
+
}
|
|
423
|
+
.qa30MenuSkin .wenayTbItem {
|
|
424
|
+
padding: 0;
|
|
425
|
+
border-radius: 0;
|
|
426
|
+
gap: 0;
|
|
427
|
+
align-items: stretch;
|
|
428
|
+
cursor: default;
|
|
429
|
+
}
|
|
430
|
+
.qa30MenuSkin .wenayTbItem:hover {
|
|
431
|
+
background: transparent;
|
|
432
|
+
}
|
|
433
|
+
.qa30MenuSkin .qa30MenuTile {
|
|
434
|
+
will-change: transform;
|
|
435
|
+
}
|
|
436
|
+
`;
|
|
437
|
+
const Qa30AnimatedMenuBar = () => {
|
|
438
|
+
const items = qa30MenuToolbar.api.useItems();
|
|
439
|
+
const tileRefs = useRef(new Map());
|
|
440
|
+
const prevRects = useRef(new Map());
|
|
441
|
+
const layoutKey = items.map(x => `${x.item.key}:${x.density}`).join("|");
|
|
442
|
+
useLayoutEffect(() => {
|
|
443
|
+
const prev = prevRects.current;
|
|
444
|
+
const next = new Map();
|
|
445
|
+
tileRefs.current.forEach((node, key) => {
|
|
446
|
+
const rect = node.getBoundingClientRect();
|
|
447
|
+
next.set(key, { left: rect.left, top: rect.top });
|
|
448
|
+
const old = prev.get(key);
|
|
449
|
+
if (old == null)
|
|
450
|
+
return;
|
|
451
|
+
const dx = old.left - rect.left;
|
|
452
|
+
const dy = old.top - rect.top;
|
|
453
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
|
|
454
|
+
return;
|
|
455
|
+
node.style.transition = "none";
|
|
456
|
+
node.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
457
|
+
node.getBoundingClientRect();
|
|
458
|
+
requestAnimationFrame(() => {
|
|
459
|
+
node.style.transition = "transform 180ms ease";
|
|
460
|
+
node.style.transform = "";
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
prevRects.current = next;
|
|
464
|
+
}, [layoutKey]);
|
|
465
|
+
return _jsx("div", { className: "wenayTb qa30MenuSkin", children: items.map(x => _jsx("div", { ref: node => {
|
|
466
|
+
if (node)
|
|
467
|
+
tileRefs.current.set(x.item.key, node);
|
|
468
|
+
else
|
|
469
|
+
tileRefs.current.delete(x.item.key);
|
|
470
|
+
}, className: "wenayTbItem qa30MenuTile", title: x.density == "icon" ? x.item.title : undefined, onClick: x.item.onClick, children: x.content }, x.item.key)) });
|
|
471
|
+
};
|
|
472
|
+
const ColumnsMenuDemo = () => {
|
|
473
|
+
const [win, setWin] = useState(false); // Settings window open? (client-owned container, see below)
|
|
474
|
+
const cfg = qa30MenuToolbar.api.useConfig();
|
|
475
|
+
const resetOn = cfg.visible["__reset"] != false;
|
|
476
|
+
return (
|
|
477
|
+
// position: relative -> the Settings window (its own layer, position: absolute) is
|
|
478
|
+
// anchored to THIS card, so it scrolls with the card instead of running off-screen.
|
|
479
|
+
// FloatingWindow's --wnd-* theme tokens are UNDECLARED by default (the window renders
|
|
480
|
+
// transparent) - declare them HERE for a dark modal look. Scoped to this card ON
|
|
481
|
+
// PURPOSE: stand-only styling, not a global tokens change (a client brings its own).
|
|
482
|
+
_jsxs("div", { style: {
|
|
483
|
+
position: "relative",
|
|
484
|
+
"--wnd-bg": "var(--color-bg-dark)",
|
|
485
|
+
"--wnd-border": "1px solid var(--color-border-common)",
|
|
486
|
+
"--wnd-radius": "8px",
|
|
487
|
+
"--wnd-shadow": "0 10px 30px rgba(0,0,0,0.55)",
|
|
488
|
+
"--wnd-header-bg": "rgba(255,255,255,0.06)",
|
|
489
|
+
"--wnd-header-border": "1px solid var(--color-border-common)",
|
|
490
|
+
"--wnd-header-color": "var(--color-text-base)",
|
|
491
|
+
}, children: [_jsxs("div", { style: { background: "#26354f", border: "1px solid #40516d", borderRadius: 0, padding: 1, width: "fit-content", maxWidth: "100%", marginBottom: 10, display: "inline-flex", alignItems: "stretch", gap: 1, flexWrap: "wrap" }, children: [_jsx("style", { children: qa30ToolbarSkinCss }), _jsx(Qa30AnimatedMenuBar, {}), resetOn && _jsx(Qa30SquareButton, { title: "Reset toolbar", onClick: () => qa30MenuToolbar.api.reset(), children: _jsx(Qa30ResetIcon, {}) }), _jsxs(OutsideClickArea, { status: win, outsideClick: () => setWin(false), style: { display: "inline-flex" }, children: [_jsx(Qa30SquareButton, { title: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043C\u0435\u043D\u044E", pressed: win, onClick: () => setWin(v => !v), children: _jsxs("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("circle", { cx: "12", cy: "12", r: "3" }), _jsx("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 8 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H2a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 8a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H8a1.65 1.65 0 0 0 1-1.51V2a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V8a1.65 1.65 0 0 0 1.51 1H22a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })] }) }), win && (_jsx(FloatingWindow, { keyForSave: "qa30.menuSettingsWin", position: { x: 0, y: 44 }, size: { width: 300, height: 320 }, zIndex: 40, header: _jsx("span", { style: { padding: "0 10px", fontSize: 12, lineHeight: "26px", color: "#cdd6e4" }, children: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043C\u0435\u043D\u044E" }), onCLickClose: () => setWin(false), children: _jsx("div", { style: { padding: 12, color: "var(--color-text-base)", height: "100%", boxSizing: "border-box", overflow: "auto" }, children: _jsx(qa30MenuToolbar.Settings, {}) }) }))] })] }), _jsx("div", { style: { height: 190 }, children: _jsx(AgGridTable, { rowData: menuRows, getRowId: pp => pp.data.id, columnDefs: qa30MenuColumnDefs, autoSizeColumns: false, onGridReady: e => {
|
|
492
|
+
qaMenuState.grid.attach(e.api);
|
|
493
|
+
applyQa30BlockMode();
|
|
494
|
+
}, onGridPreDestroyed: () => qaMenuState.grid.detach() }) })] }));
|
|
495
|
+
};
|
|
496
|
+
const tbColRows = [
|
|
497
|
+
{ id: "a", name: "Alpha", price: 10.5, qty: 3, note: "spot" },
|
|
498
|
+
{ id: "b", name: "Beta", price: 22.1, qty: 7, note: "swap" },
|
|
499
|
+
{ id: "c", name: "Gamma", price: 5.8, qty: 1, note: "spot" },
|
|
500
|
+
];
|
|
501
|
+
const tbColDefs = [
|
|
502
|
+
{ colId: "name", field: "name" },
|
|
503
|
+
{ colId: "price", field: "price" },
|
|
504
|
+
{ colId: "qty", field: "qty", headerName: "Qty" },
|
|
505
|
+
{ colId: "note", field: "note" },
|
|
506
|
+
];
|
|
507
|
+
const qaTbColState = createColumnState({
|
|
508
|
+
key: "qa31.tbColumns",
|
|
509
|
+
columns: [
|
|
510
|
+
{ key: "name", title: "Name", fixed: true },
|
|
511
|
+
{ key: "price", title: "Price", icon: _jsx("span", { children: "\uD83D\uDCB0" }) },
|
|
512
|
+
{ key: "qty", title: "Quantity", short: "qty" },
|
|
513
|
+
{ key: "note", title: "Note", short: "note" },
|
|
514
|
+
],
|
|
515
|
+
});
|
|
516
|
+
// the toolbar does NOT own order/visibility here: it runs on the columnState
|
|
517
|
+
// config (source) - grid, toolbar, menu and Settings all edit the same thing.
|
|
518
|
+
// Items without an icon get a letter pseudo-icon in icon density.
|
|
519
|
+
const qaTbOverColumns = createToolbar({
|
|
520
|
+
key: "qa31.toolbar",
|
|
521
|
+
items: qaTbColState.columns.map(c => ({ key: c.key, title: c.title, short: c.short, icon: c.icon, fixed: c.fixed })),
|
|
522
|
+
source: qaTbColState.api.listSource,
|
|
523
|
+
});
|
|
524
|
+
const ToolbarColumnsDemo = () => (_jsxs("div", { children: [_jsxs("div", { style: { display: "flex", alignItems: "center", gap: 18, marginBottom: 8, flexWrap: "wrap" }, children: [_jsx(qaTbOverColumns.Bar, { settings: true, popAlign: "left" }), _jsx(ColumnsMenu, { state: qaTbColState, compact: true })] }), _jsx("div", { style: { height: 170 }, children: _jsx(AgGridTable, { rowData: tbColRows, getRowId: pp => pp.data.id, columnDefs: tbColDefs, autoSizeColumns: false, onGridReady: e => qaTbColState.grid.attach(e.api), onGridPreDestroyed: () => qaTbColState.grid.detach() }) }), _jsx("div", { style: { marginTop: 8, maxWidth: 320 }, children: _jsx(qaTbOverColumns.Settings, {}) })] }));
|
|
525
|
+
/* ---------- 32. createColumnGrid: auto kit over table/cards/menu/dots ---------- */
|
|
526
|
+
const qaAutoColumnGrid = createColumnGrid({
|
|
527
|
+
key: "qa32.columnGrid",
|
|
528
|
+
columnDefs: tbColDefs,
|
|
529
|
+
data: tbColRows,
|
|
530
|
+
getId: r => r.id,
|
|
531
|
+
autoSizeOnColumnCountChange: true,
|
|
532
|
+
columns: [
|
|
533
|
+
{ key: "name", fixed: true, cardRole: "title" },
|
|
534
|
+
{ key: "price", icon: _jsx("span", { children: "PX" }), cardRole: "accent" },
|
|
535
|
+
{ key: "qty", short: "qty" },
|
|
536
|
+
{ key: "note", short: "note", defaultVisible: false },
|
|
537
|
+
],
|
|
538
|
+
});
|
|
539
|
+
const ColumnGridKitDemo = () => {
|
|
540
|
+
const [mode, setMode] = useState("table");
|
|
541
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { style: btn(mode == "table", "#0969da"), onClick: () => setMode("table"), children: "table" }), _jsx("button", { style: btn(mode == "cards", "#0969da"), onClick: () => setMode("cards"), children: "cards" })] }), _jsx(qaAutoColumnGrid.View, { mode: mode, tableHeight: 180, table: { getRowId: pp => pp.data.id } })] });
|
|
542
|
+
};
|
|
543
|
+
const observeLocalMask = { count: true, meta: { status: true }, items: { a: true } };
|
|
544
|
+
const ObserveStoreLocalDemo = () => {
|
|
545
|
+
const store = useMemo(() => Observe.createStore({
|
|
546
|
+
count: 0,
|
|
547
|
+
meta: { status: "idle" },
|
|
548
|
+
items: { a: 1, b: 2 },
|
|
549
|
+
}), []);
|
|
550
|
+
const count = useStoreNode(store.node.at("count"));
|
|
551
|
+
const status = useStoreNode(store.node.meta.status);
|
|
552
|
+
const itemKeys = useStoreKeys(store.node.items);
|
|
553
|
+
const selection = useStoreSelect(useMemo(() => store.update(observeLocalMask), [store]), { drain: "micro" });
|
|
554
|
+
const [emit, listen] = useMemo(() => createListen(), []);
|
|
555
|
+
const listenArgs = useListenArgs(listen, { initial: [0, "initial"] });
|
|
556
|
+
const listenValue = useListenValue(listen, { initial: 0, map: (n) => n });
|
|
557
|
+
function mutatePlainState() {
|
|
558
|
+
store.state.meta.status = "plain " + new Date().toLocaleTimeString();
|
|
559
|
+
void Observe.flushReactive(store.state);
|
|
560
|
+
}
|
|
561
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => count.replace((count.value ?? 0) + 1), children: "node.at(\"count\") +1" }), _jsx("button", { onClick: () => status.replace("replace " + new Date().toLocaleTimeString()), children: "replace status" }), _jsx("button", { onClick: () => { store.state.items.c = Date.now(); void Observe.flushReactive(store.state); }, children: "add key c" }), _jsx("button", { onClick: () => { delete store.state.items.b; void Observe.flushReactive(store.state); }, children: "delete key b" }), _jsx("button", { onClick: mutatePlainState, children: "plain state mutation + flush" }), _jsx("button", { onClick: () => emit(Date.now(), status.value ?? "-"), children: "emit listen" }), _jsx("button", { onClick: () => store.replace({ count: 0, meta: { status: "reset" }, items: { a: 1, b: 2 } }), children: "replace whole store" })] }), _jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }, children: [_jsxs("span", { children: ["count exists: ", _jsx("b", { children: String(count.exists) })] }), _jsxs("span", { children: ["count value: ", _jsx("b", { children: count.value })] }), _jsxs("span", { children: ["status: ", _jsx("b", { children: status.value })] }), _jsxs("span", { children: ["item keys: ", _jsx("b", { children: itemKeys.stringKeys.join(",") })] }), _jsxs("span", { children: ["listen value: ", _jsx("b", { children: listenValue })] })] }), _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }, children: [_jsxs("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, overflow: "auto", fontSize: 11 }, children: ["selection\\n", JSON.stringify(selection.value, null, 2)] }), _jsxs("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, overflow: "auto", fontSize: 11 }, children: ["listen args\\n", JSON.stringify(listenArgs, null, 2)] })] })] });
|
|
562
|
+
};
|
|
563
|
+
const observeMirrorMask = { value: true, nested: { label: true }, updatedAt: true, events: true, bag: true, deep: { level1: { level2: { leaf: true, counters: true } } } };
|
|
564
|
+
const observeLabelMask = { nested: { label: true }, events: true };
|
|
565
|
+
function createSseChangedListen(url) {
|
|
566
|
+
const listeners = new Set();
|
|
567
|
+
let source = null;
|
|
568
|
+
function notify() {
|
|
569
|
+
listeners.forEach(listener => listener());
|
|
570
|
+
}
|
|
571
|
+
function ensureSource() {
|
|
572
|
+
if (source || typeof EventSource == "undefined")
|
|
573
|
+
return;
|
|
574
|
+
source = new EventSource(url);
|
|
575
|
+
source.addEventListener("changed", notify);
|
|
576
|
+
source.onerror = () => {
|
|
577
|
+
// EventSource reconnects itself; the visible QA state comes from fetch errors/successes.
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
on(cb) {
|
|
582
|
+
listeners.add(cb);
|
|
583
|
+
ensureSource();
|
|
584
|
+
return () => {
|
|
585
|
+
listeners.delete(cb);
|
|
586
|
+
if (listeners.size == 0) {
|
|
587
|
+
source?.close();
|
|
588
|
+
source = null;
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
},
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
function createSseChangedPathsListen(url) {
|
|
595
|
+
const listeners = new Set();
|
|
596
|
+
let source = null;
|
|
597
|
+
function ensureSource() {
|
|
598
|
+
if (source || typeof EventSource == "undefined")
|
|
599
|
+
return;
|
|
600
|
+
source = new EventSource(url);
|
|
601
|
+
source.addEventListener("changedPaths", event => {
|
|
602
|
+
const change = JSON.parse(event.data);
|
|
603
|
+
listeners.forEach(listener => listener(change));
|
|
604
|
+
});
|
|
605
|
+
source.onerror = () => { };
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
on(cb) {
|
|
609
|
+
listeners.add(cb);
|
|
610
|
+
ensureSource();
|
|
611
|
+
return () => {
|
|
612
|
+
listeners.delete(cb);
|
|
613
|
+
if (listeners.size == 0) {
|
|
614
|
+
source?.close();
|
|
615
|
+
source = null;
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
async function postObserveMutation(body) {
|
|
622
|
+
const res = await fetch("/__qa/observe-store/mutate", {
|
|
623
|
+
method: "POST",
|
|
624
|
+
headers: { "content-type": "application/json" },
|
|
625
|
+
body: JSON.stringify(body),
|
|
626
|
+
});
|
|
627
|
+
if (!res.ok)
|
|
628
|
+
throw new Error(await res.text());
|
|
629
|
+
return res.json();
|
|
630
|
+
}
|
|
631
|
+
const ObserveStoreMirrorDemo = () => {
|
|
632
|
+
const remote = useMemo(() => {
|
|
633
|
+
const changed = createSseChangedListen("/__qa/observe-store/events");
|
|
634
|
+
const changedPaths = createSseChangedPathsListen("/__qa/observe-store/events-paths");
|
|
635
|
+
return {
|
|
636
|
+
changed,
|
|
637
|
+
changedPaths,
|
|
638
|
+
async get(mask) {
|
|
639
|
+
const url = new URL("/__qa/observe-store/get", window.location.origin);
|
|
640
|
+
if (mask !== undefined)
|
|
641
|
+
url.searchParams.set("mask", JSON.stringify(mask));
|
|
642
|
+
const res = await fetch(url);
|
|
643
|
+
if (!res.ok)
|
|
644
|
+
throw new Error(await res.text());
|
|
645
|
+
return res.json();
|
|
646
|
+
},
|
|
647
|
+
};
|
|
648
|
+
}, []);
|
|
649
|
+
const initial = useMemo(() => ({
|
|
650
|
+
value: 0,
|
|
651
|
+
nested: { label: "client initial" },
|
|
652
|
+
updatedAt: "",
|
|
653
|
+
events: 0,
|
|
654
|
+
bag: {},
|
|
655
|
+
deep: { level1: { level2: { leaf: "client deep initial", counters: {} } } },
|
|
656
|
+
}), []);
|
|
657
|
+
const mirror = useStoreMirror(remote, initial, { mask: observeMirrorMask, current: true, drain: 50 });
|
|
658
|
+
const value = useStoreNode(mirror.store.node.value);
|
|
659
|
+
const label = useStoreNode(mirror.store.node.nested.label);
|
|
660
|
+
const bagKeys = useStoreKeys(mirror.store.node.bag);
|
|
661
|
+
const deepLeaf = useStoreNode(mirror.store.node.deep.level1.level2.leaf);
|
|
662
|
+
const deepCounterKeys = useStoreKeys(mirror.store.node.deep.level1.level2.counters);
|
|
663
|
+
const labelSelection = useStoreSelect(useMemo(() => mirror.store.update(observeLabelMask), [mirror.store]), { drain: 50 });
|
|
664
|
+
const pathEvents = useStoreChangedPaths(remote.changedPaths);
|
|
665
|
+
const [pushes, setPushes] = useState(0);
|
|
666
|
+
const [lastPost, setLastPost] = useState("-");
|
|
667
|
+
useListenEffect(remote.changed, () => setPushes(v => v + 1));
|
|
668
|
+
async function mutate(body) {
|
|
669
|
+
setLastPost("posting...");
|
|
670
|
+
try {
|
|
671
|
+
const data = await postObserveMutation(body);
|
|
672
|
+
setLastPost(JSON.stringify(data));
|
|
673
|
+
}
|
|
674
|
+
catch (e) {
|
|
675
|
+
setLastPost(String(e));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { onClick: () => mutate({ type: "inc" }), children: "server +1" }), _jsx("button", { onClick: () => mutate({ type: "label", label: "srv " + new Date().toLocaleTimeString() }), children: "server label" }), _jsx("button", { onClick: () => mutate({ type: "bag-add", key: "c", value: Date.now() }), children: "server add key c" }), _jsx("button", { onClick: () => mutate({ type: "bag-delete", key: "b" }), children: "server delete key b" }), _jsx("button", { onClick: () => mutate({ type: "deep-leaf", value: "deep " + new Date().toLocaleTimeString() }), children: "server deep leaf" }), _jsx("button", { onClick: () => mutate({ type: "deep-add", key: "z", value: Date.now() }), children: "server deep add z" }), _jsx("button", { onClick: () => mutate({ type: "deep-delete", key: "y" }), children: "server deep delete y" }), _jsx("button", { onClick: () => mutate({ type: "reset" }), children: "server reset" }), _jsx("button", { onClick: () => value.replace((value.value ?? 0) + 1000), children: "local mirror +1000" }), _jsx("button", { onClick: () => mirror.sync(), children: "manual sync" }), _jsx("button", { onClick: () => mirror.stop(), children: "stop sync" })] }), _jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }, children: [_jsxs("span", { children: ["ready: ", _jsx("b", { children: String(mirror.ready) })] }), _jsxs("span", { children: ["syncing: ", _jsx("b", { children: String(mirror.syncing) })] }), _jsxs("span", { children: ["SSE pushes: ", _jsx("b", { children: pushes })] }), _jsxs("span", { children: ["path pushes: ", _jsx("b", { children: pathEvents.count })] }), _jsxs("span", { children: ["node value: ", _jsx("b", { children: value.value })] }), _jsxs("span", { children: ["node label: ", _jsx("b", { children: label.value })] }), _jsxs("span", { children: ["bag keys: ", _jsx("b", { children: bagKeys.stringKeys.join(",") })] }), _jsxs("span", { children: ["deep leaf: ", _jsx("b", { children: deepLeaf.value })] }), _jsxs("span", { children: ["deep keys: ", _jsx("b", { children: deepCounterKeys.stringKeys.join(",") })] })] }), mirror.error != null && _jsxs("div", { style: { color: "#cf222e" }, children: ["error: ", String(mirror.error)] }), _jsxs("div", { style: { display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }, children: [_jsxs("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, overflow: "auto", fontSize: 11 }, children: ["mirror.value\\n", JSON.stringify(mirror.value, null, 2)] }), _jsxs("pre", { style: { background: "#f6f8fa", padding: 8, borderRadius: 6, overflow: "auto", fontSize: 11 }, children: ["label selection\\n", JSON.stringify(labelSelection.value, null, 2), "\\n\\nchanged paths\\n", JSON.stringify(pathEvents.paths), "\\n\\nlast POST\\n", lastPost] })] })] });
|
|
679
|
+
};
|
|
680
|
+
/* ---------- 13. ModalProvider / useModal ---------- */
|
|
681
|
+
const ModalOpener = () => {
|
|
682
|
+
const modal = useModal();
|
|
683
|
+
return (_jsx("button", { onClick: () => modal.open(_jsxs("div", { style: { background: "#fff", padding: 24, borderRadius: 8, minWidth: 260 }, children: [_jsx("b", { children: "Modal via useModal" }), _jsx("div", { style: { margin: "10px 0", fontSize: 13 }, children: "Close with Escape, an outside click, or the button." }), _jsx("button", { onClick: () => modal.close(), children: "close" })] })), children: "open modal" }));
|
|
684
|
+
};
|
|
685
|
+
const ModalDemo = () => _jsx(ModalProvider, { children: _jsx(ModalOpener, {}) });
|
|
686
|
+
const resizeAssetRange = ["USDT", "BTC", "ETH", "BNB", "AMB"];
|
|
687
|
+
function makeResizeParams(asset) {
|
|
688
|
+
return {
|
|
689
|
+
asset: {
|
|
690
|
+
name: "asset",
|
|
691
|
+
value: asset,
|
|
692
|
+
range: resizeAssetRange,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const ResizeBugRepro = () => {
|
|
697
|
+
const [n, setN] = useState(0);
|
|
698
|
+
const [asset, setAsset] = useState("USDT");
|
|
699
|
+
const [fixedWidth, setFixedWidth] = useState(150);
|
|
700
|
+
const [autoWidth, setAutoWidth] = useState(0);
|
|
701
|
+
const [fixedSelectWidth, setFixedSelectWidth] = useState(0);
|
|
702
|
+
const onChange = (params) => {
|
|
703
|
+
setAsset(params.asset.value);
|
|
704
|
+
setN(v => v + 1);
|
|
705
|
+
};
|
|
706
|
+
// Resize detection now rides the library singleton: useResizeObserver on the auto box,
|
|
707
|
+
// useElementSize on the user-resizable fixed box (its width/height come from the hook).
|
|
708
|
+
const readWidths = () => {
|
|
709
|
+
const autoSelect = autoBox.element()?.querySelector("select");
|
|
710
|
+
const fixedSelect = fixedBox.element()?.querySelector("select");
|
|
711
|
+
setAutoWidth(Math.round(autoSelect?.getBoundingClientRect().width ?? 0));
|
|
712
|
+
setFixedSelectWidth(Math.round(fixedSelect?.getBoundingClientRect().width ?? 0));
|
|
713
|
+
};
|
|
714
|
+
const autoBox = useResizeObserver(readWidths);
|
|
715
|
+
const fixedBox = useElementSize();
|
|
716
|
+
React.useLayoutEffect(() => {
|
|
717
|
+
readWidths();
|
|
718
|
+
// setResizeableElement's probing settles after paint - re-read one frame later
|
|
719
|
+
const id = requestAnimationFrame(readWidths);
|
|
720
|
+
return () => cancelAnimationFrame(id);
|
|
721
|
+
}, [n, asset, fixedWidth, fixedBox.width]);
|
|
722
|
+
const selectInfo = { fontSize: 12, color: "#57606a", marginTop: 6 };
|
|
723
|
+
return _jsxs("div", { style: { display: "grid", gap: 12 }, children: [_jsxs("div", { style: { display: "flex", gap: 18, alignItems: "flex-start", flexWrap: "wrap" }, children: [_jsxs("div", { children: [_jsxs("div", { ref: autoBox.ref, style: { display: "inline-flex", flexDirection: "column", alignItems: "stretch", border: "1px solid #cf222e", padding: 6 }, children: [_jsx(ParamsEditor, { params: makeResizeParams(asset), onChange: onChange }), _jsxs("button", { onClick: () => setN(v => v + 1), children: ["rerender ", n] })] }), _jsxs("div", { style: selectInfo, children: ["auto-width select: ", _jsxs("b", { children: [autoWidth, "px"] })] })] }), _jsxs("div", { children: [_jsx("div", { ref: fixedBox.ref, style: { width: fixedWidth, minWidth: 90, maxWidth: 300, resize: "horizontal", overflow: "auto", border: "1px solid #0969da", padding: 6 }, children: _jsx(ParamsEditor, { params: makeResizeParams(asset), onChange: onChange }) }), _jsxs("div", { style: { display: "flex", gap: 8, marginTop: 6, flexWrap: "wrap" }, children: [_jsxs("button", { onClick: () => setFixedWidth(w => w == 150 ? 240 : 150), children: ["parent ", fixedWidth, "px"] }), _jsx("button", { onClick: () => setN(v => v + 1), children: "rerender" })] }), _jsxs("div", { style: selectInfo, children: ["fixed-parent select: ", _jsxs("b", { children: [fixedSelectWidth, "px"] }), " \u00B7 container (useElementSize): ", _jsxs("b", { children: [fixedBox.width, "\u00D7", fixedBox.height, "px"] })] })] })] }), _jsxs("div", { style: { fontSize: 13 }, children: ["selected asset: ", _jsx("b", { children: asset })] })] });
|
|
724
|
+
};
|
|
725
|
+
/* ---------- 20. SettingsDialog + section registry ---------- */
|
|
726
|
+
const dlgBody = { fontSize: 13, lineHeight: 1.6 };
|
|
727
|
+
const dlgStaticSections = [
|
|
728
|
+
{
|
|
729
|
+
key: "general",
|
|
730
|
+
name: "General",
|
|
731
|
+
keywords: ["workspace", "startup"],
|
|
732
|
+
searchText: "autosave project defaults",
|
|
733
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "General" }), _jsx("div", { children: "Static root section passed via the sections prop." })] }),
|
|
734
|
+
children: [
|
|
735
|
+
{
|
|
736
|
+
key: "general-project",
|
|
737
|
+
name: "Project",
|
|
738
|
+
searchText: "workspace autosave startup folder",
|
|
739
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Project" }), _jsx("div", { children: "Workspace folder, startup page, and autosave defaults." })] }),
|
|
740
|
+
children: [
|
|
741
|
+
{
|
|
742
|
+
key: "general-project-indexing",
|
|
743
|
+
name: "Indexing",
|
|
744
|
+
searchText: "file watcher cache suffix tree",
|
|
745
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Indexing" }), _jsx("div", { children: "File watcher cache and recursive search index settings." })] }),
|
|
746
|
+
children: [
|
|
747
|
+
{
|
|
748
|
+
key: "general-project-indexing-suffix",
|
|
749
|
+
name: "Suffix tree",
|
|
750
|
+
searchText: "suffix tree trie token depth recursive auto expand",
|
|
751
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Suffix tree" }), _jsx("div", { children: "Deep branch used to verify auto-expansion for any tree depth." })] }),
|
|
752
|
+
children: [
|
|
753
|
+
{
|
|
754
|
+
key: "general-project-indexing-suffix-leaves",
|
|
755
|
+
name: "Leaf buckets",
|
|
756
|
+
searchText: "suffix tree leaves buckets compact path",
|
|
757
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Leaf buckets" }), _jsx("div", { children: "Nested leaf configuration under the suffix tree branch." })] }),
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
},
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
],
|
|
764
|
+
},
|
|
765
|
+
],
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
key: "display",
|
|
769
|
+
name: "Display",
|
|
770
|
+
keywords: ["appearance", "theme"],
|
|
771
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Display" }), _jsx("div", { children: "Display root. Long content to test scrolling:" }), Array.from({ length: 30 }, (_, i) => _jsxs("div", { children: ["line ", i + 1] }, i))] }),
|
|
772
|
+
children: [
|
|
773
|
+
{
|
|
774
|
+
key: "display-theme",
|
|
775
|
+
name: "Theme",
|
|
776
|
+
searchText: "dark light contrast palette",
|
|
777
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Theme" }), _jsx("div", { children: "Dark, light, contrast, and palette options." })] }),
|
|
778
|
+
children: [
|
|
779
|
+
{
|
|
780
|
+
key: "display-theme-palette",
|
|
781
|
+
name: "Palette",
|
|
782
|
+
searchText: "semantic colors accent warning success",
|
|
783
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Palette" }), _jsx("div", { children: "Semantic colors shared by settings, windows, and toolbars." })] }),
|
|
784
|
+
children: [
|
|
785
|
+
{
|
|
786
|
+
key: "display-theme-palette-accent",
|
|
787
|
+
name: "Accent color",
|
|
788
|
+
searchText: "accent primary focus selected highlight",
|
|
789
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Accent color" }), _jsx("div", { children: "Primary focus and selected-state color tokens." })] }),
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
},
|
|
795
|
+
{ key: "display-font", name: "Font", searchText: () => "font size editor line height", render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "Font" }), _jsx("div", { children: "Editor font size and line height settings." })] }) },
|
|
796
|
+
],
|
|
797
|
+
},
|
|
798
|
+
];
|
|
799
|
+
// Any module: register on mount, the returned unregister runs on unmount.
|
|
800
|
+
const ExternalSectionModule = () => {
|
|
801
|
+
useEffect(() => registerSettingsSection({
|
|
802
|
+
key: "external",
|
|
803
|
+
parentKey: "display",
|
|
804
|
+
name: "External module",
|
|
805
|
+
searchText: "registered plugin mount unmount external",
|
|
806
|
+
render: () => _jsxs("div", { style: dlgBody, children: [_jsx("b", { children: "External" }), _jsx("div", { children: "Registered by a module on mount and removed on unmount." })] }),
|
|
807
|
+
}), []);
|
|
808
|
+
return _jsx("span", { style: { padding: "2px 8px", background: "#dafbe1", borderRadius: 6, fontSize: 12 }, children: "external module mounted" });
|
|
809
|
+
};
|
|
810
|
+
const SettingsDialogDemo = () => {
|
|
811
|
+
const [mounted, setMounted] = useState(true);
|
|
812
|
+
useCacheMapPersistence(memoryCache);
|
|
813
|
+
return _jsxs("div", { style: { display: "flex", gap: 12, alignItems: "center", flexWrap: "wrap" }, children: [_jsx(SettingsDialog, { sections: dlgStaticSections, defaultSection: "general" }), _jsx("button", { onClick: () => setMounted(v => !v), children: mounted ? "unmount external module" : "mount external module" }), mounted && _jsx(ExternalSectionModule, {})] });
|
|
814
|
+
};
|
|
815
|
+
/* ---------- 21. createUiSlot - configurable placement ---------- */
|
|
816
|
+
const qaSlot = createUiSlot({
|
|
817
|
+
key: "qa-ui-slot",
|
|
818
|
+
places: { top: "Top bar", side: "Sidebar" },
|
|
819
|
+
def: "top",
|
|
820
|
+
});
|
|
821
|
+
const slotContent = _jsx("span", { style: { padding: "4px 10px", background: "#0969da", color: "#fff", borderRadius: 6, fontSize: 12 }, children: "the block" });
|
|
822
|
+
const UiSlotDemo = () => {
|
|
823
|
+
// App-side persistence contract in one hook: memoryCache.load() on start, then the dirty
|
|
824
|
+
// channel -> saveDebounced(300) - the persisted maps are observable and mark their
|
|
825
|
+
// memoryCache dirty themselves, the app owns the write policy.
|
|
826
|
+
useCacheMapPersistence(memoryCache);
|
|
827
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsx("style", { children: `.qaChip{border:1px solid #6e7781;border-radius:6px;padding:3px 10px;font-size:12px;cursor:pointer;display:inline-block}.qaChipActive{background:#0969da;border-color:#0969da;color:#fff}` }), _jsxs("div", { style: { display: "flex", gap: 10, alignItems: "center" }, children: [_jsx("span", { style: { fontSize: 13 }, children: "place:" }), _jsx(qaSlot.PlacementSetting, { className: "qaChip", activeClassName: "qaChipActive" })] }), _jsxs("div", { style: { border: "1px dashed #6e7781", borderRadius: 6, padding: 8, minHeight: 38, fontSize: 13 }, children: ["Top bar: ", _jsx(qaSlot.Slot, { place: "top", children: slotContent })] }), _jsxs("div", { style: { border: "1px dashed #6e7781", borderRadius: 6, padding: 8, minHeight: 38, fontSize: 13 }, children: ["Sidebar: ", _jsx(qaSlot.Slot, { place: "side", children: slotContent })] })] });
|
|
828
|
+
};
|
|
829
|
+
/* ---------- 22. createCallbackHub - single callback slot multiplexer ---------- */
|
|
830
|
+
// Fake legacy API with ONE callback slot: a second direct subscriber would silently
|
|
831
|
+
// overwrite the first. The hub takes the slot once and fans events out.
|
|
832
|
+
const hubSlot = { cb: null };
|
|
833
|
+
let hubTick = 0;
|
|
834
|
+
const qaHub = createCallbackHub(emit => { hubSlot.cb = emit; });
|
|
835
|
+
const HubDemo = () => {
|
|
836
|
+
const [a, setA] = useState([]);
|
|
837
|
+
const [b, setB] = useState([]);
|
|
838
|
+
const [aOn, setAOn] = useState(false);
|
|
839
|
+
const [bOn, setBOn] = useState(false);
|
|
840
|
+
useEffect(() => { if (aOn)
|
|
841
|
+
return qaHub.on(n => setA(v => [...v, n])); }, [aOn]);
|
|
842
|
+
useEffect(() => { if (bOn)
|
|
843
|
+
return qaHub.on(n => setB(v => [...v, n])); }, [bOn]);
|
|
844
|
+
return _jsxs("div", { style: { display: "grid", gap: 8, fontSize: 13 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" }, children: [_jsx("button", { onClick: () => hubSlot.cb?.(++hubTick), children: "fire source event" }), _jsx("button", { onClick: () => setAOn(v => !v), children: aOn ? "off A" : "on A" }), _jsx("button", { onClick: () => setBOn(v => !v), children: bOn ? "off B" : "on B" }), _jsxs("span", { children: ["slot bound (lazy): ", _jsx("b", { children: String(hubSlot.cb != null) })] }), _jsxs("span", { children: ["hub.count(): ", _jsx("b", { children: qaHub.count() })] })] }), _jsxs("div", { children: ["A received: ", _jsx("b", { children: a.join(", ") || "-" })] }), _jsxs("div", { children: ["B received: ", _jsx("b", { children: b.join(", ") || "-" })] })] });
|
|
845
|
+
};
|
|
846
|
+
/* ---------- 35. DragBox - imperative delta drag (adapter over useDraggableApi) ---------- */
|
|
847
|
+
const DragBoxDemo = () => {
|
|
848
|
+
const base = useRef({ x: 20, y: 20 });
|
|
849
|
+
const delta = useRef({ x: 0, y: 0 });
|
|
850
|
+
const chipRef = useRef(null);
|
|
851
|
+
const renders = useRef(0);
|
|
852
|
+
const [starts, setStarts] = useState(0);
|
|
853
|
+
const [stops, setStops] = useState(0);
|
|
854
|
+
renders.current++;
|
|
855
|
+
const apply = () => {
|
|
856
|
+
const el = chipRef.current;
|
|
857
|
+
if (el)
|
|
858
|
+
el.style.transform = `translate(${base.current.x + delta.current.x}px, ${base.current.y + delta.current.y}px)`;
|
|
859
|
+
};
|
|
860
|
+
useEffect(apply, []);
|
|
861
|
+
return _jsxs("div", { style: { position: "relative", height: 220, border: "1px dashed #d0d7de", borderRadius: 8, overflow: "hidden" }, children: [_jsx(DragBox, { onStart: () => { delta.current = { x: 0, y: 0 }; setStarts(v => v + 1); }, onX: x => { delta.current.x = x; apply(); }, onY: y => { delta.current.y = y; apply(); }, onStop: () => {
|
|
862
|
+
base.current = { x: base.current.x + delta.current.x, y: base.current.y + delta.current.y };
|
|
863
|
+
delta.current = { x: 0, y: 0 };
|
|
864
|
+
apply();
|
|
865
|
+
setStops(v => v + 1);
|
|
866
|
+
}, children: _jsx("div", { ref: chipRef, style: { width: 90, padding: "10px 0", textAlign: "center", background: "#0969da", color: "#fff", borderRadius: 8, cursor: "grab", userSelect: "none", touchAction: "none" }, children: "drag me" }) }), _jsxs("div", { style: { position: "absolute", right: 10, bottom: 8, fontSize: 12, color: "#57606a" }, children: ["starts: ", _jsx("b", { children: starts }), " \u00B7 stops: ", _jsx("b", { children: stops }), " \u00B7 renders: ", _jsx("b", { children: renders.current })] })] });
|
|
867
|
+
};
|
|
868
|
+
/* ---------- 25. createToolbar - customizable toolbar ---------- */
|
|
869
|
+
// Item actions land in a module store (the demo component reads it via updateBy).
|
|
870
|
+
const tbActions = { last: "-", count: 0 };
|
|
871
|
+
function tbAct(name) {
|
|
872
|
+
return () => {
|
|
873
|
+
tbActions.last = name;
|
|
874
|
+
tbActions.count++;
|
|
875
|
+
renderBy(tbActions);
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
const tbBaseItems = [
|
|
879
|
+
{ key: "home", title: "Home (fixed)", short: "Home", icon: _jsx("span", { children: "\uD83C\uDFE0" }), fixed: true, onClick: tbAct("home") },
|
|
880
|
+
{ key: "star", title: "Add to favorites", short: "Star", icon: _jsx("span", { children: "\u2B50" }), onClick: tbAct("star") },
|
|
881
|
+
{ key: "bell", title: "Notifications", short: "Alerts", icon: _jsx("span", { children: "\uD83D\uDD14" }), onClick: tbAct("bell") },
|
|
882
|
+
{ key: "chart", title: "Open chart window", short: "Chart", icon: _jsx("span", { children: "\uD83D\uDCC8" }), onClick: tbAct("chart") },
|
|
883
|
+
{ key: "trash", title: "Clear workspace", short: "Clear", icon: _jsx("span", { children: "\uD83D\uDDD1\uFE0F" }), defaultVisible: false, onClick: tbAct("trash") },
|
|
884
|
+
];
|
|
885
|
+
const qaToolbar = createToolbar({ key: "qa-toolbar", items: tbBaseItems });
|
|
886
|
+
// The merge case (acceptance #3): the SAME persist key, one EXTRA item - as if the app
|
|
887
|
+
// shipped an update. Created lazily so the base bar is what the page starts with.
|
|
888
|
+
let qaToolbarExtra = null;
|
|
889
|
+
const getQaToolbarExtra = () => qaToolbarExtra ??= createToolbar({
|
|
890
|
+
key: "qa-toolbar",
|
|
891
|
+
items: [...tbBaseItems, { key: "help", title: "Help (added in the update)", short: "Help", icon: _jsx("span", { children: "\u2753" }), onClick: tbAct("help") }],
|
|
892
|
+
});
|
|
893
|
+
const ToolbarDemo = () => {
|
|
894
|
+
updateBy(tbActions);
|
|
895
|
+
const [extra, setExtra] = useState(false);
|
|
896
|
+
const [thirdDensity, setThirdDensity] = useState(false);
|
|
897
|
+
const [changes, setChanges] = useState(0);
|
|
898
|
+
const [lastCfg, setLastCfg] = useState(null);
|
|
899
|
+
const tb = extra ? getQaToolbarExtra() : qaToolbar;
|
|
900
|
+
// App-side persistence contract - same wiring as the UiSlot card.
|
|
901
|
+
useCacheMapPersistence(memoryCache);
|
|
902
|
+
// Same pure Settings element registered as a global settings section (see card 20's dialog).
|
|
903
|
+
useEffect(() => registerSettingsSection({
|
|
904
|
+
key: "qa-toolbar",
|
|
905
|
+
name: "Toolbar",
|
|
906
|
+
render: () => _jsx(tb.Settings, {}),
|
|
907
|
+
}), [tb]);
|
|
908
|
+
// Both bars share one persist key -> both apis emit; subscribe to the visible one.
|
|
909
|
+
useListenEffect(tb.api.onChange, cfg => {
|
|
910
|
+
setChanges(v => v + 1);
|
|
911
|
+
setLastCfg(cfg);
|
|
912
|
+
});
|
|
913
|
+
// Density registry extensibility: a third level is just one more registration.
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
if (!thirdDensity)
|
|
916
|
+
return;
|
|
917
|
+
return registerToolbarDensity({
|
|
918
|
+
key: "full",
|
|
919
|
+
name: "Full text",
|
|
920
|
+
renderItem: item => _jsxs("span", { children: [item.icon, " ", item.title] }),
|
|
921
|
+
});
|
|
922
|
+
}, [thirdDensity]);
|
|
923
|
+
return _jsxs("div", { style: { display: "grid", gap: 10 }, children: [_jsx("div", { style: { background: "#17202e", borderRadius: 8, padding: 6, width: "fit-content", maxWidth: "100%" }, children: _jsx(tb.Bar, { settings: true, popAlign: "left" }) }), _jsxs("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center", fontSize: 13 }, children: [_jsx(SettingsDialog, { trigger: _jsx("span", { style: { display: "inline-block", padding: "4px 10px", border: "1px solid #0969da", borderRadius: 6, color: "#0969da" }, children: "global settings" }) }), _jsx("button", { onClick: () => setExtra(v => !v), children: extra ? "app update: extra item ON" : "simulate app update (+1 item)" }), _jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: thirdDensity, onChange: e => setThirdDensity(e.target.checked) }), " register 3rd density (Full text)"] })] }), _jsxs("div", { style: { display: "flex", gap: 16, flexWrap: "wrap", fontSize: 13 }, children: [_jsxs("span", { children: ["last action: ", _jsx("b", { children: tbActions.last }), " (", tbActions.count, ")"] }), _jsxs("span", { children: ["onChange fired: ", _jsx("b", { children: changes })] })] }), lastCfg && _jsx("div", { style: { fontSize: 11, fontFamily: "monospace", color: "#57606a", overflowX: "auto" }, children: JSON.stringify(lastCfg) })] });
|
|
924
|
+
};
|
|
925
|
+
/* ---------- 26. useReorder - a field of blocks ---------- */
|
|
926
|
+
// Module store: the hook owns NO state beyond the live drag - order lives with the app.
|
|
927
|
+
const reorderState = { order: ["A", "B", "C", "D", "E", "F", "G", "H"], varied: false, commits: 0 };
|
|
928
|
+
const ReorderDemo = () => {
|
|
929
|
+
updateBy(reorderState);
|
|
930
|
+
const r = useReorder({
|
|
931
|
+
order: reorderState.order,
|
|
932
|
+
commit: next => {
|
|
933
|
+
reorderState.order = next;
|
|
934
|
+
reorderState.commits++;
|
|
935
|
+
renderBy(reorderState);
|
|
936
|
+
},
|
|
937
|
+
// equal blocks: 'slots' is exact; varied sizes: FLIP-measure the real layout
|
|
938
|
+
preview: reorderState.varied ? "measure" : "slots",
|
|
939
|
+
});
|
|
940
|
+
const width = (k) => reorderState.varied ? 56 + (k.charCodeAt(0) % 4) * 26 : 64;
|
|
941
|
+
return _jsxs("div", { style: { display: "grid", gap: 8, fontSize: 13 }, children: [_jsxs("label", { children: [_jsx("input", { type: "checkbox", checked: reorderState.varied, onChange: () => { reorderState.varied = !reorderState.varied; renderBy(reorderState); } }), " ", "varied block widths (preview: measure / FLIP)"] }), _jsx("div", { ref: r.listRef, style: { display: "flex", flexWrap: "wrap", gap: 8, width: 300, background: "#17202e", padding: 8, borderRadius: 8 }, children: reorderState.order.map(k => {
|
|
942
|
+
const it = r.item(k);
|
|
943
|
+
return _jsx("div", { ...it.props, style: {
|
|
944
|
+
width: width(k), height: 44, lineHeight: "44px", textAlign: "center",
|
|
945
|
+
background: it.dragging ? "#2f5a8f" : "#2b3648", color: "#dfe6ef", borderRadius: 6,
|
|
946
|
+
cursor: "grab", userSelect: "none", touchAction: "none",
|
|
947
|
+
transition: it.active && !it.dragging ? "transform 0.12s ease" : undefined,
|
|
948
|
+
position: it.dragging ? "relative" : undefined, zIndex: it.dragging ? 1 : undefined,
|
|
949
|
+
boxShadow: it.dragging ? "0 4px 14px rgba(0,0,0,0.4)" : undefined,
|
|
950
|
+
...it.style,
|
|
951
|
+
}, children: k }, k);
|
|
952
|
+
}) }), _jsxs("div", { style: { color: "#57606a", fontFamily: "monospace", fontSize: 12 }, children: ["order: ", reorderState.order.join(" "), " | commits: ", reorderState.commits] })] });
|
|
953
|
+
};
|
|
954
|
+
/* ---------- 27. useReorderBoard - columns with per-column gravity ---------- */
|
|
955
|
+
const boardState = {
|
|
956
|
+
cols: [
|
|
957
|
+
{ key: "c1", items: ["A1", "A2", "A3"] },
|
|
958
|
+
{ key: "c2", items: ["B1", "B2"] },
|
|
959
|
+
{ key: "c3", items: ["C1", "C2", "C3", "C4"] },
|
|
960
|
+
{ key: "c4", items: ["D1"] },
|
|
961
|
+
{ key: "c5", items: [] },
|
|
962
|
+
],
|
|
963
|
+
gravity: { c1: "top", c2: "bottom", c3: "top", c4: "bottom", c5: "top" },
|
|
964
|
+
// side tray: freshly created blocks land here, then drag into the board
|
|
965
|
+
tray: [],
|
|
966
|
+
commits: 0, events: 0, last: "-", nextCol: 6, nextItem: 1,
|
|
967
|
+
};
|
|
968
|
+
const qaBoardTray = "tray";
|
|
969
|
+
// left-rail trash: a registered board column whose content the commit discards
|
|
970
|
+
const qaBoardTrash = "trash";
|
|
971
|
+
const qaBoardStyles = {
|
|
972
|
+
root: { display: "grid", gap: 8, fontSize: 13 },
|
|
973
|
+
columns: { display: "grid", alignItems: "start", columnGap: 6 },
|
|
974
|
+
columnControls: { display: "grid", alignItems: "center", columnGap: 6, marginTop: -6 },
|
|
975
|
+
insertStrip: {
|
|
976
|
+
width: 20, height: 20, borderRadius: 10, cursor: "pointer", userSelect: "none",
|
|
977
|
+
display: "flex", alignItems: "center", justifyContent: "center",
|
|
978
|
+
color: "#9fb3c8", background: "#202c3c", border: "1px solid #38506d", fontSize: 11, fontWeight: 700,
|
|
979
|
+
},
|
|
980
|
+
column: { display: "flex", flexDirection: "column", gap: 6, width: 78, height: 240, padding: 6, borderRadius: 8 },
|
|
981
|
+
columnWrap: { display: "grid", width: 90 },
|
|
982
|
+
tray: { display: "grid", gap: 6, width: 90, alignContent: "start" },
|
|
983
|
+
newItem: {
|
|
984
|
+
height: 24, borderRadius: 6, cursor: "pointer", userSelect: "none",
|
|
985
|
+
border: "1px dashed #38506d", background: "#202c3c", color: "#9fb3c8", fontSize: 12,
|
|
986
|
+
},
|
|
987
|
+
leftRail: { display: "grid", gap: 8, alignContent: "start", width: 44 },
|
|
988
|
+
addColumn: {
|
|
989
|
+
width: 44, height: 44, borderRadius: 8, cursor: "pointer", userSelect: "none",
|
|
990
|
+
border: "1px solid #38506d", background: "#202c3c", color: "#9fb3c8", fontSize: 18, lineHeight: "42px",
|
|
991
|
+
},
|
|
992
|
+
trashZone: {
|
|
993
|
+
width: 44, height: 64, borderRadius: 8, boxSizing: "border-box", userSelect: "none",
|
|
994
|
+
display: "flex", alignItems: "center", justifyContent: "center", fontSize: 18,
|
|
995
|
+
},
|
|
996
|
+
removeColumn: { width: 24, height: 20, border: "1px solid #f4b1aa", borderRadius: 6, cursor: "pointer", color: "#cf222e", background: "#fff", fontSize: 15, lineHeight: "16px" },
|
|
997
|
+
item: {
|
|
998
|
+
height: 32, width: "100%", boxSizing: "border-box", lineHeight: "32px", textAlign: "center", borderRadius: 6,
|
|
999
|
+
color: "#dfe6ef", cursor: "grab", userSelect: "none", touchAction: "none",
|
|
1000
|
+
},
|
|
1001
|
+
status: { color: "#57606a", fontFamily: "monospace", fontSize: 12 },
|
|
1002
|
+
};
|
|
1003
|
+
function qaBoardColumnStyle(over, bottom) {
|
|
1004
|
+
return {
|
|
1005
|
+
...qaBoardStyles.column,
|
|
1006
|
+
justifyContent: bottom ? "flex-end" : "flex-start",
|
|
1007
|
+
background: over ? "#1d2b40" : "#17202e",
|
|
1008
|
+
outline: over ? "1px solid #2f5a8f" : undefined,
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function qaBoardItemStyle(it) {
|
|
1012
|
+
return {
|
|
1013
|
+
...qaBoardStyles.item,
|
|
1014
|
+
background: it.dragging ? "#2f5a8f" : "#2b3648",
|
|
1015
|
+
transition: it.active && !it.dragging ? "transform 0.12s ease" : undefined,
|
|
1016
|
+
position: it.dragging ? "relative" : undefined,
|
|
1017
|
+
zIndex: it.dragging ? 1 : undefined,
|
|
1018
|
+
boxShadow: it.dragging ? "0 4px 14px rgba(0,0,0,0.4)" : undefined,
|
|
1019
|
+
...it.style,
|
|
1020
|
+
// The hook may supply a measured drag width. It is valid only while
|
|
1021
|
+
// dragging; idle board items must fill their own column.
|
|
1022
|
+
width: it.dragging ? it.style?.width : "100%",
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const BoardDemo = () => {
|
|
1026
|
+
updateBy(boardState);
|
|
1027
|
+
const log = (s) => { boardState.last = s; boardState.events++; renderBy(boardState); };
|
|
1028
|
+
// The tray and the trash are just MORE columns of the same hook (registered
|
|
1029
|
+
// via columnRef like the rest) - rendered aside, excluded from the board grid.
|
|
1030
|
+
// Whatever lands in the trash column is simply not stored back: it disappears.
|
|
1031
|
+
const r = useReorderBoard({
|
|
1032
|
+
columns: [...boardState.cols, { key: qaBoardTray, items: boardState.tray }, { key: qaBoardTrash, items: [] }],
|
|
1033
|
+
commit: next => {
|
|
1034
|
+
const trashed = next.find(c => c.key == qaBoardTrash)?.items ?? [];
|
|
1035
|
+
boardState.tray = next.find(c => c.key == qaBoardTray)?.items ?? [];
|
|
1036
|
+
boardState.cols = next.filter(c => c.key != qaBoardTray && c.key != qaBoardTrash);
|
|
1037
|
+
boardState.commits++;
|
|
1038
|
+
if (trashed.length) {
|
|
1039
|
+
boardState.last = `deleted ${trashed.join(",")}`;
|
|
1040
|
+
boardState.events++;
|
|
1041
|
+
}
|
|
1042
|
+
renderBy(boardState);
|
|
1043
|
+
},
|
|
1044
|
+
onOverChange: e => log(`over ${e.over.col}#${e.over.index}` + (e.prev && e.prev.col != e.over.col ? " (column crossed)" : "")),
|
|
1045
|
+
onDragEnd: e => log(`drop ${e.key} -> ${e.over.col}#${e.over.index} committed=${e.committed}`),
|
|
1046
|
+
});
|
|
1047
|
+
// A column is consumer state: splice at ANY position, the hook picks it up
|
|
1048
|
+
// via the live columnRef registry - nothing to tell the hook.
|
|
1049
|
+
const addColumnAt = (i) => {
|
|
1050
|
+
const k = "c" + boardState.nextCol++;
|
|
1051
|
+
const cols = boardState.cols.slice();
|
|
1052
|
+
cols.splice(i, 0, { key: k, items: [] });
|
|
1053
|
+
boardState.cols = cols;
|
|
1054
|
+
boardState.gravity[k] = boardState.nextCol % 2 ? "bottom" : "top";
|
|
1055
|
+
renderBy(boardState);
|
|
1056
|
+
};
|
|
1057
|
+
const removeColumn = (key) => {
|
|
1058
|
+
const removed = boardState.cols.find(column => column.key == key);
|
|
1059
|
+
boardState.cols = boardState.cols.filter(column => column.key != key);
|
|
1060
|
+
delete boardState.gravity[key];
|
|
1061
|
+
log(`removed ${key}${removed?.items.length ? ` (${removed.items.length} items)` : ""}`);
|
|
1062
|
+
};
|
|
1063
|
+
const InsertStrip = ({ at }) => (_jsx("button", { title: "insert column between", onClick: () => addColumnAt(at), style: qaBoardStyles.insertStrip, children: "\u2194" }));
|
|
1064
|
+
const addItem = () => {
|
|
1065
|
+
const k = "E" + boardState.nextItem++;
|
|
1066
|
+
boardState.tray = [...boardState.tray, k];
|
|
1067
|
+
log(`created ${k}`);
|
|
1068
|
+
};
|
|
1069
|
+
const tracks = boardState.cols.map((_, i) => i < boardState.cols.length - 1 ? "90px 20px" : "90px").join(" ");
|
|
1070
|
+
const trashOver = r.over?.col == qaBoardTrash;
|
|
1071
|
+
return _jsxs("div", { style: qaBoardStyles.root, children: [_jsxs("div", { style: { display: "flex", alignItems: "flex-start", gap: 14 }, children: [_jsxs("div", { style: qaBoardStyles.leftRail, children: [_jsx("button", { title: "add a column at the start", onClick: () => addColumnAt(0), style: qaBoardStyles.addColumn, children: "+" }), _jsx("div", { ref: r.columnRef(qaBoardTrash), title: "drop a block here to delete it", style: {
|
|
1072
|
+
...qaBoardStyles.trashZone,
|
|
1073
|
+
border: trashOver ? "1px solid #cf5b6a" : "1px dashed #6b4a55",
|
|
1074
|
+
background: trashOver ? "#3a2230" : "#1c1622",
|
|
1075
|
+
color: trashOver ? "#ff8896" : "#cf5b6a",
|
|
1076
|
+
}, children: "\u2212" })] }), _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsx("div", { style: { ...qaBoardStyles.columns, gridTemplateColumns: tracks }, children: boardState.cols.map((c, ci) => (_jsx("div", { style: { ...qaBoardStyles.columnWrap, gridColumn: ci * 2 + 1 }, children: _jsx("div", { ref: r.columnRef(c.key), style: qaBoardColumnStyle(r.over?.col == c.key, boardState.gravity[c.key] == "bottom"), children: c.items.map(k => {
|
|
1077
|
+
const it = r.item(k);
|
|
1078
|
+
return _jsx("div", { ...it.props, style: qaBoardItemStyle(it), children: k }, k);
|
|
1079
|
+
}) }) }, c.key))) }), _jsx("div", { style: { ...qaBoardStyles.columnControls, gridTemplateColumns: tracks }, children: boardState.cols.map((c, ci) => _jsxs(React.Fragment, { children: [_jsx("button", { title: `remove ${c.key}`, onClick: () => removeColumn(c.key), style: { ...qaBoardStyles.removeColumn, gridColumn: ci * 2 + 1, justifySelf: "center" }, children: "\u2212" }), ci < boardState.cols.length - 1 && _jsx("span", { style: { gridColumn: ci * 2 + 2, justifySelf: "center" }, children: _jsx(InsertStrip, { at: ci + 1 }) })] }, c.key)) })] }), _jsxs("div", { style: qaBoardStyles.tray, children: [_jsx("button", { title: "create a new block in the tray", onClick: addItem, style: qaBoardStyles.newItem, children: "+ item" }), _jsx("div", { ref: r.columnRef(qaBoardTray), style: { ...qaBoardColumnStyle(r.over?.col == qaBoardTray, false), border: "1px dashed #2c3c55" }, children: boardState.tray.map(k => {
|
|
1080
|
+
const it = r.item(k);
|
|
1081
|
+
return _jsx("div", { ...it.props, style: qaBoardItemStyle(it), children: k }, k);
|
|
1082
|
+
}) })] })] }), _jsxs("div", { style: qaBoardStyles.status, children: [boardState.cols.map(c => c.key + (boardState.gravity[c.key] == "bottom" ? "↓" : "↑") + ":[" + c.items.join(",") + "]").join(" "), " tray:[", boardState.tray.join(","), "] | commits: ", boardState.commits, " | events: ", boardState.events, " | last: ", boardState.last] })] });
|
|
1083
|
+
};
|
|
1084
|
+
/* ---------- borad ---------- */
|
|
1085
|
+
function ActiveChecks() {
|
|
1086
|
+
return (_jsxs(_Fragment, { children: [_jsx(Check, { n: 18, title: "Observe hooks - local store and listen", do: "Click node.at(count) +1, replace status, plain state mutation + flush, emit listen, and replace whole store.", expect: "Leaf node, selection, direct state mutation after flush, add/delete object keys, and listen hooks all rerender. The count key is read through node.at(count), so it does not conflict with node.count().", note: "This isolates the React adapter from transport: no fetch/SSE/RPC involved.", tall: true, children: _jsx(ObserveStoreLocalDemo, {}) }), _jsx(Check, { n: 17, title: "Observe hooks - store mirror over HTTP/SSE", do: "Click server +1, label, add/delete key, deep leaf/deep add/deep delete, local mirror +1000, stop sync, then manual sync.", expect: "Server buttons POST to the Vite QA server, including add/delete object keys and deep mutations. SSE pushes changedPaths; mirror pulls only the intersecting mask when possible. Local mirror edits render immediately and are overwritten by the next server sync.", note: "This checks the React adapter only in wenay-react2: common2 remains React-free; transport policy stays outside the hook.", tall: true, children: _jsx(ObserveStoreMirrorDemo, {}) }), _jsx(Check, { n: 23, title: "Replay hooks - video line, conflation, time travel, freshness", do: "Watch A and B play the same synthetic video. Toggle slow network for B, switch resolution, unmount/remount A. In C drag the slider (playback pauses), then press live. In D: note the renders counter while the line is fresh, check stall producer, wait 2s, uncheck; while stalled press new client (keyframe). In E: switch pull pace (250ms/1s/3s), press pull now.", expect: "A plays smoothly at 10 fps. B on slow network stays CURRENT (bounded latency): frames drop (dropped/coalesced counters grow), the wire buffer never grows past highWater, and each recovery is one coalesced last-frame envelope. Resolution switches on all clients within a frame. Remounting A continues from the kept seq (seq does not reset, frames counter continues from the tail). C seeks to any archived seq via keyframe+tail fold and hands over to live seamlessly. D: renders stays FLAT while frames grow (no per-event re-renders); on stall the STALE badge appears after ~2s and disappears on the first frame after resume; a client mounted during a stall goes STALE within staleMs (this in-proc keyframe is stamped at request time; a tail/keyframe carrying an old producer ts goes stale from the first paint); StrictMode double-effect leaves one watchdog and no badge flicker. E advances ONLY at the pull cadence: frames jumps by ~pace\u00D710fps per pull while pulls grows by one; seq keeps up with head; switching pace keeps the position (no keyframe restart, frames does not re-fold); pull now folds immediately.", note: "All in-proc: the socket transport is already proven in wenay-common2 (replay/video-socket.demo, canvas-socket.test). This card tests the React side: useReplaySubscribe lifecycle (off on unmount, reconnect by since), useReplayHistory scrubber, frames drawn to canvas via ref - bypassing VDOM, stale/staleMs mirroring common2's edge-triggered watchdog into React state, useReplayFrame pull path (timer around remote.frame(), rev2 frame model; policy:'frame'/hint ride ReplaySubscribeOpts but need a server frameLine - the wire test lives in common2 replay/rpc-auto.test.ts). The producer starts on first render and runs until page reload.", tall: true, children: _jsx(ReplayVideoDemo, {}) }), _jsx(Check, { n: 24, title: "Replay hooks - store sync (useStoreReplayMirror)", do: "Watch ticks/price advance. Click server note / add key / delete key. Uncheck sync enabled, mutate the server a few times, recheck. Click restart. Check stall producer, wait 2.5s, then click server note.", expect: "Mirror follows the server store with seq ascending. While sync is disabled the mirror freezes; on re-enable it catches up through the journal tail (seq jumps to head, no full reset flicker). Object key add/delete replicate. restart resubscribes from the kept seq. On stall the stale flag flips true after 2.5s and lastTs freezes; any server mutation (e.g. server note) flips it back to fresh.", note: "exposeStoreReplay/syncStoreReplay in-proc: the remote is the exposed {line, since, keyframe} facade, exactly what createRpcServerAuto would expose over a socket. staleMs rides the same ReplaySubscribeOpts as the video card.", children: _jsx(ReplayStoreDemo, {}) }), _jsx(Check, { n: 33, title: "Replay hooks - per-key feed (useStoreReplayEach)", do: "Watch the table for a few producer ticks. Click server add row, server delete row, server replace ALL, then remount client (fresh keyframe).", expect: "On mount every row appears with cb calls=1 (keyframe expanded per key). Between clicks only the mutated row's cb calls counter grows - the whole dict is never re-delivered per tick. Delete removes the row via (key, undefined). replace ALL swaps the table: removed rows leave, new rows enter with cb calls=1. Remount folds a fresh keyframe (all counters reset to 1); StrictMode double-effect does not double-count.", note: "React counterpart of Observe.syncStoreReplayEach (wenay-common2 1.0.62): internal mirror store + syncStoreReplay + store.each(). The mirror lives in a ref, so in-mount resubscribes reconnect by journal tail on top of kept state; the fold target is a plain Map (grid-api style), not React state. drain:100 coalesces multiple writes to one key into one call per window.", tall: true, children: _jsx(ReplayStoreEachDemo, {}) }), _jsx(Check, { n: 34, title: "Replay hooks - route hand-off (useReplayRouteSubscribe)", do: "Watch one canvas draw the synthetic video. Click switch direct, then switch relay, then fail route. Repeat while the producer is moving.", expect: "The canvas keeps advancing as one logical fold: route switches catch up by seq before the old route closes, so frames do not reset or duplicate. The label changes to direct/relay only after ready. fail route reports an error but keeps the previous active route alive and the canvas continues.", note: "React wrapper over wenay-common2 1.0.65 Replay.replayRouteSubscribe. Route hand-off is explicit through switchRoute(); changing the remote prop remains a fresh subscription boundary. This route helper does not expose stale/lastTs, so freshness stays on the non-route hooks until common2 grows that surface.", tall: true, children: _jsx(ReplayRouteDemo, {}) }), _jsx(Check, { n: 13, title: "ModalProvider / useModal - Escape and outside click", do: "Click open modal. Close it with Escape. Open it again and close with an outside click. Open it again and close with the close button.", expect: "All three methods close it. The dimmed backdrop is above everything (z-index from token --wenay-z-modal).", note: "M1: Escape and closeOnEscape/closeOnOutsideClick options were added; useModal remains the app-level path and createModalElementStore remains low-level.", children: _jsx(ModalDemo, {}) }), _jsx(Check, { n: 20, title: "SettingsDialog - searchable settings tree + registry", do: "Click the three-dot toolbar-style settings button: drag the window by its header, drag the divider between tree and content, double-click it to reset width, use the tree icons and the dotted tree-cycle button, search for suffix/leaf/palette/accent/font/external and wrong-layout examples like \u044B\u0433\u0430\u0430\u0448\u0447 for suffix. Press Enter to save a query into search history, reopen history from the clock button, pick a saved query, then clear history. Clear search via the x and via Escape, then close via window x/outside click/Escape with empty search. Unmount external module and open again.", expect: "The default trigger is the same compact toolbar-button style as createToolbar, using a three-dot icon. Dialog opens as the standard draggable FloatingWindow with a header, larger size, shared close x, and outside-click close. The tree/content divider changes the tree width, persists it through memoryCache, supports keyboard arrows, and double-click/Enter resets to default. Search uses original input plus RU/EN keyboard-layout variants, selects the first real match, auto-expands parents, and highlights only the matched word once. Enter stores non-empty queries in a small persisted search history; choosing a history item restores the query; clearing history removes the dropdown; leaving the search box closes the dropdown. The dotted tree-cycle button switches expanded/current branch/collapsed and stays in the search row. The clear x and Escape both cancel search text; Escape with empty search closes the dialog. The external child under Display appears only while mounted.", note: "Registry is a module singleton (registerSettingsSection -> unregister), no React context. Tree shape comes from children or parentKey. Search history uses createSearchHistory -> memoryGetOrCreate/memoryCache dirty channel; this demo loads memoryCache and saves dirty changes with saveDebounced(300). Look via --dlg-* tokens; apps pass their own section classes via sectionClassName/sectionActiveClassName.", children: _jsx(SettingsDialogDemo, {}) }), _jsx(Check, { n: 38, title: "Media video - capture lifecycle + canvas viewer", do: "Press start camera and grant permission. Move the tab to background for a short time, then return. Press stop, then start again.", expect: "The canvas renders without React re-rendering per frame. State changes idle \u2192 requesting \u2192 live; the stats line shows drawn frames and frame age. Hidden-tab capture is owned by common2 worker/ImageCapture defaults.", note: "useMediaSource owns only permission/device lifecycle. Media.attachVideoCanvas owns JPEG decode and drawing; frame data never enters React state.", children: _jsx(MediaVideoDemo, {}) }), _jsx(Check, { n: 39, title: "Media audio - mic lifecycle + sequential player", do: "Press enable + start mic, grant permission, speak briefly, then stop. If browser autoplay blocks audio, press the same button again.", expect: "Audio activation happens in a user gesture; common2 player keeps a short live backlog and reports played/dropped frames. React renders only the half-second stats snapshot.", note: "The PCM player is common2 imperative code. This card proves the React wrapper's start/stop cleanup and gesture boundary, not an audio implementation in React.", children: _jsx(MediaAudioDemo, {}) }), _jsx(Check, { n: 40, title: "Peer SDK - mirrored store + explicit resync", do: "Click peer A +1 several times. Watch mirrored value on peer B. Press resync B; the mirror must stay coherent.", expect: "Peer SDK owns relay journal and repair. React reads the peer store normally and exposes route/ready/seq as low-frequency control state; no transport or patch protocol is reimplemented here.", note: "In-process host replaces a fake UI mock: this card proves the actual Peer.createPeerClient contract. Direct WebRTC needs the app's real signaling/rtc factory and remains a separate browser recipe.", children: _jsx(PeerSdkDemo, {}) }), _jsx(Check, { n: 41, title: "Peer calls - ring, accept, hangup", do: "Click call B. On B press accept; both sides become active. Then hang up from A. Repeat and decline on B.", expect: "The incoming ring, active state and terminal reason propagate through the existing Peer signal hub. React only renders manager state; common2 owns call IDs, busy/glare resolution and timeout/offline verdicts.", note: "In-process host, no fake call protocol: usePeerCalls binds Peer.createCallManager. A real app supplies its server-side authorize policy before allowing media viewers.", children: _jsx(PeerCallDemo, {}) }), _jsx(Check, { n: 42, title: "Peer presence - snapshot plus online/offline edges", do: "Observe both accounts online. Toggle B connection off and on.", expect: "The list updates on connection edges without polling. The hook subscribes before it reads the snapshot, so it follows the host protocol rather than creating a second presence store.", note: "Presence is common2 host state. React receives only list/edge data and does not decide authentication or account validity.", children: _jsx(PeerPresenceDemo, {}) }), " ", _jsx(Check, { n: 43, title: "Media relay - actual camera stream with live ACL revoke", do: "Start camera and grant permission: the right canvas must show the relayed stream. Revoke ACL: the viewer frame counter stops while capture keeps running. Grant it again: frames resume. Stop camera.", expect: "The path is camera \u2192 Media source \u2192 common2 relay \u2192 policy-filtered viewer canvas. ACL revocation gates an already-open viewer without trusting React to detach it; the source stats continue because publishing and viewing are separate responsibilities.", note: "This is an in-process relay, so it proves the React/lifecycle seam and live policy filter. A deployed app exposes publishOf/watchOf through its RPC server and keeps canWatch plus call authorization on the server.", children: _jsx(MediaRelayAclDemo, {}) }), " ", _jsx(Check, { n: 44, title: "Audio relay - actual microphone stream with live ACL revoke", do: "Press enable + start relay mic and grant permission, then speak. Revoke ACL: playback and viewer counters stop while capture remains live. Grant it again and confirm playback resumes.", expect: "The path is microphone \u2192 Media source \u2192 common2 audio relay \u2192 policy-filtered AudioContext player. Audio activation is a user gesture; ACL gates the existing viewer without React deciding access.", note: "This uses the relay's short lossless audio queue. In production publishOf/watchOf/canWatch are exposed by the server next to the call authorization policy.", children: _jsx(MediaRelayAudioDemo, {}) }), " ", _jsx(Check, { n: 45, title: "Peer call with live video and audio relay", do: "Enable camera + mic, call B, then accept on B. The canvas starts receiving video only after accept; speak to hear the relayed audio. Hang up: the viewer detaches.", expect: "One real scenario: call state gates server-style relay access and viewer lifecycle. Before/after the active call, capture may run but B receives no media.", note: "This is the complete in-process consumer demo exported from wenay-react2/demo/peer-media; production keeps the same ACL decision on its server.", children: _jsx(PeerCallVideoAudioDemo, {}) }), " ", _jsx(Check, { n: 21, title: "createUiSlot - configurable block placement", do: "Switch Top bar / Sidebar. Then reload the page (F5).", expect: "The block moves between the two containers WITHOUT a reload; only one mount point shows it at a time. After F5 the chosen place is restored (memoryGetOrCreate -> memoryCache).", note: "Mount points render <Slot place=...> themselves and stay ignorant of each other. The demo calls memoryCache.load() on mount and subscribes memoryCache.onDirty -> saveDebounced(300): the persisted maps are observable and mark memoryCache dirty themselves, the app owns the write policy.", children: _jsx(UiSlotDemo, {}) }), _jsx(Check, { n: 22, title: "createCallbackHub - one slot, many subscribers", do: "Note slot bound = false. Click on A (bound becomes true), fire source event, then on B and fire again. Then off A and fire once more.", expect: "Before the first on() the slot is untouched (lazy bind). With A+B subscribed both receive the same events. After off A, B keeps receiving; hub.count() tracks subscribers.", note: "Fixes the real bug where two onX(cb) subscribers silently overwrote each other. Built on `listen` from wenay-common2; bind(emit) runs once.", children: _jsx(HubDemo, {}) }), _jsx(Check, { n: 25, title: "createToolbar - customizable toolbar (config / Bar / Settings)", do: "Click toolbar items (last action updates). Open the gear popover: toggle Clear workspace on, drag rows to reorder - grab ANYWHERE on the row, mouse or touch, try dragging above the fixed Home too (or focus the handle and press arrow keys), switch density Icons / Icons + labels. Open global settings -> Toolbar section and repeat an edit there. Register the 3rd density and switch to Full text. Uncheck the separated Toolbar settings row at the bottom - the gear (and this popover) disappears from the bar; re-enable it via global settings -> Toolbar. Click simulate app update. Reload the page (F5). In Settings, click the Reset toolbar action button inside its row. Then check the Reset toolbar row, confirm the reset icon appears in the bar, click it, and uncheck the row again.", expect: "The bar renders visible items in config order; density switches icon-only <-> icon+label (tooltips show titles in icon mode). Home is fixed: checkbox disabled, no drag handle, pinned first - it never moves during a drag preview and a row dragged above it lands right below it, exactly as previewed (no snap-back on drop). The gear popover and the global settings section are THE SAME editor - an edit in one is instantly visible in the other and on the bar. The 3rd density appears in the editor as one more segment and renders icon + full title. The app update appends Help as visible WITHOUT wiping your order/visibility. After F5 everything is restored (memoryGetOrCreate -> memoryCache). onChange fires on every edit with the new config (JSON below); Settings is visible by default, the reset icon is hidden by default, the Reset toolbar row action restores defaults, and that row can hide/show the bar icon.", note: "Three decoupled layers: serializable config (single source of truth, persisted like createUiSlot), Bar, and a PURE Settings editor over config. Density levels live in an extensible module registry (registerToolbarDensity); reorder is a built-in nearest-slot pointer sort (no dnd deps, layout-agnostic: list / bar / grid) + keyboard arrows; the preview simulates the commit incl. fixed pinning, so what you see is what you drop. v1 has no overflow menu - visibility is the space tool.", tall: true, children: _jsx(ToolbarDemo, {}) }), _jsx(Check, { n: 26, title: "useReorder - drag blocks in a field (mini dnd)", do: "Drag blocks around the wrapped field (mouse or touch) - within a row, across rows, to the first/last slot. Click a block without moving. Then enable varied block widths and repeat: rows re-wrap differently, blocks still glide exactly to where they will land.", expect: "During a drag the grabbed block follows the pointer, the rest GLIDE to their preview slots; on drop everything is already where the preview showed - no snap-back, no jumps. A plain click commits nothing (commits counter unchanged). With varied widths the preview is measured from the real CSS layout (FLIP via the order property), so wrapping changes are previewed exactly too.", note: "The library's own mini reorder-by-drag (useReorder, extracted from the Toolbar editor): keyed blocks in ANY CSS layout - list, bar, wrapped grid; DOM order never changes mid-drag, targeting runs against START slots (no boundary oscillation), ONE commit on drop. Deliberately not a dnd framework: no nesting, no cross-container moves, no collision packing.", children: _jsx(ReorderDemo, {}) }), _jsx(Check, { n: 27, title: "useReorderBoard - columns, per-column gravity, cross-column drag", do: "Drag blocks between columns: from a top-packed (up arrow) into a bottom-packed (down arrow) column, into the EMPTY column, back. Watch the landing gap: in a bottom-packed column the blocks ABOVE the slot slide UP to make room. Drag within one column too. Use \u2194 strictly between two columns to insert a new one. The \u2212 directly below a column removes that column; all actions form one aligned lower rail. Click + item in the side tray a few times, drag the created blocks into any column and drag a block back into the tray. On the left rail: + adds a column at the start; drag any block onto the \u2212 trash to delete it. Watch the events line: over changes, column crossings, drop.", expect: "The dragged block follows the pointer; the hovered column highlights (r.over); survivors glide to exactly where they land on drop - including the source column compacting per ITS gravity and the target column opening a real gap per ITS gravity. One commit per drop (counter); a plain click commits nothing. onOverChange fires only when the slot changes, onDragEnd reports the final slot and committed flag. + item spawns E1, E2... into the dashed tray; the tray highlights on hover-over and accepts blocks like any column, and the status line tracks tray:[...]. The \u2212 trash highlights while hovered and swallows the dropped block (last: deleted ...) - it is one more registered column whose content the commit discards.", note: "useReorderBoard - the columns extension of useReorder: column gravity is pure consumer CSS (justify-content), the hook never knows it - it measures the real layout (offset-based FLIP with display:none for the dragged and a real margin gap at the landing slot, so CSS decides who moves aside). Columns register via live callback refs - adding one is just consumer state; the side tray IS one more such column rendered aside, so creating blocks needed no new hook API. Same non-goals: no nesting, no collision packing, no autoscroll.", tall: true, children: _jsx(BoardDemo, {}) }), _jsx(Check, { n: 35, title: "DragBox - imperative delta drag (adapter over useDraggableApi)", do: "Drag the blue chip around (mouse or touch), several gestures in a row. Watch the counters while moving.", expect: "The chip follows the pointer 1:1; releasing commits the position - the next drag continues from where it stopped (no jump-back, no accumulation drift). starts/stops grow by 1 per gesture; renders grows only with starts/stops, NOT per move pixel (the per-tick path is imperative onX/onY over refs).", note: "DragBox is now a thin adapter over useDraggableApi (holdMs 0, trackState:false, onMove) - the old bespoke document-listener loop is gone; contract pinned by __test/dragBox.test.tsx. Production consumer: LeftModal sidebar. DragArea deliberately stays as-is (@deprecated: unique semantics - body listeners, stopPropagation per tick, absolute coords).", children: _jsx(DragBoxDemo, {}) }), _jsx(Check, { n: 31, title: "Toolbar over columnState - one config drives toolbar + menu + grid", do: "Drag the qty column in the GRID before price and HOLD it over the target - toolbar buttons and compact menu must preview the same order BEFORE drop. Open the toolbar gear: drag rows in Settings, toggle checkboxes - the grid and the menu follow. Toggle a button in the compact menu - the toolbar Bar drops/regains the item. Switch density in Settings (Icons / Icons + labels).", expect: "All four surfaces (grid, toolbar Bar, Settings editor, compact menu) mirror ONE config: any reorder or visibility change made on any of them lands on all others. In icon density, items without an icon show their first letters (NAM, QTY, NOT) as a text pseudo-icon; price keeps its emoji. Density and the gear checkbox are toolbar-local (they do not touch the column config); Name is fixed everywhere - not draggable, not hideable.", note: "createToolbar({source}) - the toolbar's order/visibility now can live OUTSIDE it: UiListSource is the extracted control contract, and columnState.api.listSource implements it over the same config the grid adapter syncs. No bridge, no double storage - Toolbar became a VIEW. Backward compatible: without source the toolbar keeps its own store exactly as in card 25.", tall: true, children: _jsx(ToolbarColumnsDemo, {}) }), _jsx(Check, { n: 32, title: "createColumnGrid - default grid menu + mobile dots for table/cards", do: "Switch table/cards. Use the dots overlay: hide/show/replace fields. DRAG a dot slowly along the track and watch the table/cards: every empty mark it crosses swaps the shown column IMMEDIATELY, and a small label above the finger names the column - this is how you search for a column on a phone. Release anywhere.", expect: "createColumnGrid inferred column metadata from columnDefs, applied small overrides, took default data/getId at the controller level, and rendered dots as the built-in overlay with no manual max. Dots are not card-only: the same selector drives the table through columnState.grid.attach - LIVE while dragging (show/hide follows the finger, nothing waits for the drop; the drop only settles selection). Width restore stays protected because Table defaults autoSizeColumns=false; this card explicitly enables fit on visible column-count changes.", note: "This is the reusable wrapper for the card-29/30/31 pattern: one keyed controller, auto ColumnMeta from ag-grid defs, optional overrides/default data, built-in dots overlay, and ready-made representations. Use View for quick table/card switching, or use the returned pieces manually.", tall: true, children: _jsx(ColumnGridKitDemo, {}) }), _jsx(Check, { n: 30, title: "columnState toolbar menu - grouped sub-columns", do: "TOP is our menu drawn with a square-edged client skin over the card-25 toolbar config; the old lower ColumnsMenu is intentionally gone. Drag menu order in Settings and HOLD before drop: the grid and horizontal tiles must preview the same order live, then click column tiles to toggle grid visibility; switch density in Settings and check that labels expand by content. Click the BLO tile with vertical square dots several times. The grid has a Mode block group with 3 sub-columns: Values has text, Zeros has only 0, Empty has blanks. Enable Reset toolbar in Settings and click its tile.", expect: "The grouped block changes by dot count without rebuilding grid columnDefs: 1 dot shows Values+Zeros+Empty; 2 dots shows only Values; 3 dots shows Values+Zeros; 4 dots hides the whole group. The top menu has large square-edged content tiles: 1px separators between tiles, comfortable internal padding, dark by default, white border on hover, white fill when pressed/open. When order/density changes elsewhere, existing tiles animate to their new places; the menu itself remains click-only, without drag handles or drag reorder. Label/full densities grow by text content. BLO uses square vertical dots and remains clickable when the whole group is off. Hidden-by-mode sub-columns keep dashed/inert tiles and revive when the mode brings them back. Reset appears as a small-icon tile only when enabled.", note: "This demonstrates the multi-state layer and replaceable face: the menu uses the standard Toolbar.Bar for structure/order/membership/density while the client fully draws the square-edged item face. The mode tile is not a column; it changes a runtime columnState presentGate over a stable grouped schema. columnState presence marks gated leaf columns disabled; createToolbar({source, sourceMode:'order'}) lets the source own only real-column order, while blockMode position/membership stay local and are never pushed into the grid config.", tall: true, children: _jsx(ColumnsMenuDemo, {}) }), _jsx(Check, { n: 29, title: "columnState mobile - ColumnDots + CardList (dots create the blocks)", do: "Tap an EMPTY mark (qty / ver / note) - a dot appears and the field is created in every card below. Drag a dot slowly along the track - every empty mark it crosses replaces the field LIVE in the cards (a small label above the finger names the current field); release anywhere. Swipe a dot UP (quick vertical flick) - the dot tears off, the field disappears. Tap a dot without moving - the field gets selected (blue); press the sort button several times (asc -> desc -> off). Select ANOTHER dot - note the sort did not change. Enable sort by price, then swipe the price dot away - cards stay ordered by price.", expect: "Dots ARE the visible fields: every dot change instantly rebuilds the cards (no table involved), INCLUDING mid-drag - the swap happens as the dot crosses an empty mark, the drop commits nothing extra. Symbol is fixed (ring): its dot cannot be dragged away or torn off, it is the card title. ver shows as a badge (accent role). The sort is STICKY: it survives selecting other dots AND hiding its own field; the arrow marker above the track shows the sorted column. Max 4 dots: taps on empty marks beyond that are ignored.", note: "ColumnDots + CardList run on the columnState config alone; this stand uses optional layout=compact (two-column key/value fields, one column below 320px) - no ag-grid, no storage. The same config could drive a desktop grid via grid.attach (card 28). Touch works: gestures are pointer events with a dominant-axis test, so a horizontal drag never removes and a vertical flick never reorders.", tall: true, children: _jsx(MobileColumnsDemo, {}) }), _jsx(Check, { n: 28, title: "columnState - persisted column layout (external layer)", do: "Drag the qty column before price, resize it, hide price via the button, click the qty header to sort. Try dragging a column BEFORE the fixed Name too. Then: unmount -> mount the grid.", expect: "The JSON below mirrors every change (order/visible/width/sort) - the reverse reactivity: whatever the GRID does lands in the config. A column dropped before Name snaps back: Name is fixed and stays first, in the grid AND in the config. After remount the grid restores the exact layout from the in-memory config. sort price cycles asc -> desc -> off and the header arrow follows; sort survives hiding the column (sticky sort).", note: "createColumnState: standalone config store + two-way ag-grid adapter attached via onGridReady - agGrid4 itself is untouched. Name is fixed (fixed: true): it cannot be hidden and always stays first. Storage wiring is deliberately NOT here: what to persist will become a library-level setting later, components stay storage-free.", tall: true, children: _jsx(ColumnStateDemo, {}) }), _jsx(Check, { n: 8, title: "Outside-click closing (OutsideClickArea)", do: "Click open. Then click ANY place outside the panel, including on the same horizontal line and slightly to the right of the open button, within the panel width where there used to be a dead zone.", expect: "A click anywhere outside the panel/button closes it, including the area to the right of the button above the panel. Clicking the panel or button does NOT close it.", note: "Library BUG, this card used to fail on it: Button+outClick wraps in OutsideClickArea, which is a full-width block div, so the entire horizontal strip counts as inside. Here OutsideClickArea uses display:inline-block, and the popup uses position:absolute; otherwise it expands the wrapper rectangle and creates a dead zone to the right of the button. Real library fix: wrap content by default, or let Button narrow the wrapper.", children: _jsx(OutsideDemo, {}) })] }));
|
|
1087
|
+
}
|
|
1088
|
+
function ArchiveChecks() {
|
|
1089
|
+
return (_jsxs(_Fragment, { children: [_jsx(Check, { n: 1, title: "Reactivity updateBy / renderBy", do: "Click +1 and renderBy, then +1 WITHOUT renderBy, then renderBy only.", expect: "+1 and renderBy increases the number. +1 WITHOUT renderBy does NOT change the number on screen. renderBy only shows the accumulated value.", note: "This is the split between change and notification. After migration to the store, app.set(...) performs both steps at once.", children: _jsx(ReactivityDemo, {}) }), _jsx(Check, { n: 14, title: "Keyboard API - useKeyboard / keyboard", do: "Press any key, then click clear.", expect: "Last key updates through keyboard.on; reset clears the value and also notifies subscribers.", note: "The new pub/sub is built with `listen`: listen.on(cb) -> off(). The old keyboardState remains compatible.", children: _jsx(KeyDownDemo, {}) }), _jsx(Check, { n: 2, title: "Drag + Resize (FloatingWindow / FloatingWindow)", do: "Click window, drag the window by its header, resize it from the edges, and close it with the x button. Open the console (F12).", expect: "The window moves and resizes smoothly; the x button closes it; position and size are restored on reopen (keyForSave).", note: "Plan bug: the console must NOT contain xxx spam (FloatingWindow:532); listener resubscription on every tick is a candidate for usePointerDrag.", tall: true, children: _jsx(Button, { button: (e) => _jsx("div", { style: { display: "inline-block", padding: "6px 12px", border: "1px solid #0969da", borderRadius: 6, cursor: "pointer", background: e ? "#0969da" : "#fff", color: e ? "#fff" : "#0969da" }, children: "window" }), children: (api) => (_jsx(FloatingWindow, { keyForSave: "qa-rnd", size: { height: 220, width: 280 }, className: "fon border fonLight", moveOnlyHeader: true, onCLickClose: api.onClose, limit: { y: { min: 0 } }, onUpdate: () => { }, children: _jsx("div", { style: { width: "100%", height: "100%", display: "flex", alignItems: "center", justifyContent: "center", background: "#eef2f6" }, children: "drag header / resize / close" }) }, "qa-rnd")) }) }), _jsx(Check, { n: 3, title: "Nested menu (Menu) + hover", do: "Hover menu, then move the cursor to the item with \u25B6 and into its submenu.", expect: "The menu opens ONLY when hovering the menu trigger itself, not the full row width.", note: "Library BUG confirmed: HoverButton wraps in a <div> with no width, so it becomes a full-row block, unlike ButtonBase (width:min-content). This board wraps it with width:min-content as a workaround; add width:min-content to HoverButton in the library.", children: _jsx("div", { style: { width: "min-content" }, children: _jsx(HoverButton, { button: () => _jsx("div", { style: { display: "inline-block", padding: "6px 12px", border: "1px solid #888", borderRadius: 6, cursor: "pointer", whiteSpace: "nowrap" }, children: "menu" }), children: _jsx(Menu, { zIndex: 50, coordinate: { x: 0, y: 0 }, data: [
|
|
1090
|
+
{ name: "item 1", onClick: () => alert("item 1") },
|
|
1091
|
+
{ name: "submenu ▶", next: () => [{ name: "leaf A", onClick: () => alert("A") }, { name: "leaf B", onClick: () => alert("B") }] },
|
|
1092
|
+
] }) }) }) }), _jsx(Check, { n: 5, title: "Grid + transactions (applyGridRows)", do: "update Tesla uses a random price and updates by ID. Then remove Tesla should make the row disappear.", expect: "Update by ID without duplicates. remove Tesla removes the row. \u2705 Remove-only updates used to be lost, leaving the row in place.", note: "Fix: applyGridRows applies remove once, INDEPENDENTLY of add/update. Previously remove was lost when add/update were empty, and duplicated when both were present.", tall: true, children: _jsxs("div", { children: [_jsx("button", { style: { marginBottom: 8 }, onClick: () => renderBy(tt), children: "update Tesla (random price)" }), _jsx("div", { style: { height: 280 }, children: _jsx(GridExample, {}) })] }) }), _jsx(Check, { n: 6, title: "Chart (MyChartEngine) - LOD min+max", do: "Let the chart collect data, then zoom out with the mouse wheel. Watch the line amplitude.", expect: "When zooming out, the line keeps its amplitude and peaks do not collapse into a straight line. \u2705 Fix: LOD takes min+max per pixel. Both panels, line and bars, stay fully inside the card; nothing slides down.", note: "Fixed: drawLineChartLOD now takes min+max points per pixel instead of the first point, so peaks/dips are not clipped. Height fix: MyChartEngine had a hardcoded height 600px, so the lower panel left the card; an optional style prop was added, with the 600px default unchanged. Perf, dirty flag/filter each frame, was not touched.", tall: true, children: _jsx("div", { style: { height: 300 }, children: _jsx(MyChartEngine, { style: { height: "100%" } }) }) }), _jsx(Check, { n: 7, title: "Parameters (ParamsEditor / ParamsEditor)", do: "Move sliders and fields. The test3 field has a name and a hover comment.", expect: "The value changes in the UI while editing; range/number stay in sync; commentary appears on hover.", note: "Plan bugs: ParamsArrayEdit loses edits (Other.tsx); broken debounce and missing ResizeObserver cleanup in ParamsEditor.", tall: true, children: _jsx("div", { style: { minHeight: 260 }, children: _jsx(TestParams, {}) }) }), _jsx(Check, { n: 19, title: "Parameters - resize observer shrink repro", do: "Click rerender many times and change the asset select. Then click parent 150/240px, or narrow the blue wrapper in DevTools.", expect: "The red auto-width select does not ratchet down. The blue fixed-parent select gets narrower when the parent is 150px and grows back when the parent is 240px.", note: "This is the setResizeableElement repro: repeated ref/ResizeObserver runs must keep the natural width and avoid feedback in shrink-to-content containers.", tall: true, children: _jsx(ResizeBugRepro, {}) }), _jsx(Check, { n: 12, title: "agGrid4 - controller, removal, external buffer", do: "add/update Tesla and Apple should make rows appear/update. remove Tesla should remove it. Then: unmount grid -> write MSFT directly to the buffer -> mount grid.", expect: "Updates by ID have no duplicates; removal works. After remount, the grid catches up with the buffer itself: Tesla/Apple are still present, MSFT appears (attach->sync). Theme is dark, like production grids (GridStyleDefault).", note: "Controller path over createGridBuffer; the old transaction helper names were removed. The createGridBuffer core also works outside React.", tall: true, children: _jsx(AgGrid4Demo, {}) }), _jsx(Check, { n: 15, title: "agGrid4 - overlay rowData", do: "Click stream C before rowData. C must not appear. Then add C to rowData: C appears and receives the buffered stream price. Stream A updates A only.", expect: "Overlay sync updates only rowData-owned rows, never adds stream-only rows and never removes rowData rows.", note: "This covers selectHistory/portfolio/symbol tables where React state owns the row set.", tall: true, children: _jsx(AgGrid4OverlayDemo, {}) }), _jsx(Check, { n: 16, title: "agGrid4 - dynamic column buffer", do: "Switch alpha / alpha+beta / gamma+alpha / clear. Reloading the stand should keep the component API path clean; detach itself does not clear names.", expect: "The explicit dynamic group shows exactly the selected columns, deduped and in order. Base columns remain intact.", note: "This covers dynamic add/remove columns without any business-specific default in the shared utility.", tall: true, children: _jsx(AgGrid4ColumnBufferDemo, {}) }), _jsx(Check, { n: 4, title: "Right-click context menu (contextMenu)", do: "Right-click the gray area to open the menu. Then right-click somewhere ELSE.", expect: "Right-clicking elsewhere closes the previous menu and opens a new one with items. \u2705 Fixed.", note: "Fix: menuR stores an item snapshot on open + menuMouse onConsume. Menu items now use explicit actionKey for local action stats.", tall: true, children: _jsx(contextMenu.Layer, { zIndex: 40, children: _jsx("div", { style: { width: "100%", height: 300, background: "#e7ebef", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", color: "#57606a" }, onContextMenu: (e) => contextMenu.openAt(e, [
|
|
1093
|
+
{ name: "action 1", actionKey: "qa4.action1", onClick: () => alert("action 1") },
|
|
1094
|
+
{ name: "submenu ▶", actionKey: "qa4.submenu", next: () => [{ name: "nested", actionKey: "qa4.nested", onClick: () => alert("nested") }] },
|
|
1095
|
+
]), children: "right-click here" }) }) }), _jsx(Check, { n: 9, title: "Logs - time format + MiniLogs layers", do: "Click add log several times, watch the corner notification layer, then click a row in the compact MiniLogs table.", expect: "PageLogs and MiniLogs show time in hh:mm:ss format; a temporary notification appears in the card corner; MiniLogs row click updates the small click label.", note: "This card mounts logsApi.React.Message; that compatibility wrapper now uses useMessageEventLogsController -> MessageEventLogsView. The newer context LogsNotifications remains a separate LogsProvider-based surface. MiniLogs is hook/controller-first: useMiniLogsTable -> MiniLogsView/MiniLogsTable -> compatibility MiniLogs.", tall: true, children: _jsx(LogsDemo, {}) }), _jsx(Check, { n: 10, title: "ParamsArrayEdit vs ParamsEdit - what is sent to onSave", do: "Change the value in EACH column (test/test2), then click save. Compare what was sent to onSave and the console (F12).", expect: "BOTH save the CHANGED value (ParamsArrayEdit matches ParamsEdit). \u2705 Fixed (regression check).", note: "Fix in Other.tsx: params[i]=z -> params[i]=e. z was a placeholder and discarded the edited clone e.", tall: true, children: _jsx(ParamsSaveDemo, {}) }), _jsx(Check, { n: 11, title: "Parameters - debounce onChange", do: "Quickly move the slider/number several times in a row, then stop.", expect: "The counter does NOT grow on every tiny movement, but roughly once after ~200ms of stopping. \u2705 Fixed.", note: "Fix: timeoutId -> useRef + cleanup on unmount.", tall: true, children: _jsx(DebounceDemo, {}) })] }));
|
|
1096
|
+
}
|
|
1097
|
+
export function QABoard() {
|
|
1098
|
+
const [hash, setHash] = useState(typeof location !== "undefined" ? location.hash : "");
|
|
1099
|
+
useEffect(() => {
|
|
1100
|
+
const f = () => setHash(location.hash);
|
|
1101
|
+
window.addEventListener("hashchange", f);
|
|
1102
|
+
return () => window.removeEventListener("hashchange", f);
|
|
1103
|
+
}, []);
|
|
1104
|
+
const archive = hash === "#archive";
|
|
1105
|
+
const link = (on) => ({ padding: "4px 10px", borderRadius: 6, textDecoration: "none", color: on ? "#fff" : "#0969da", background: on ? "#0969da" : "#fff", border: "1px solid #0969da", fontSize: 13 });
|
|
1106
|
+
return (_jsxs("div", { style: { maxWidth: 920, margin: "0 auto", padding: 20, fontFamily: "system-ui, sans-serif" }, children: [_jsx("h2", { style: { margin: "0 0 4px" }, children: "QA board wenay-react2" }), _jsxs("div", { style: { display: "flex", gap: 8, margin: "8px 0" }, children: [_jsx("a", { href: "#", style: link(!archive), children: "Active checks" }), _jsx("a", { href: "#archive", style: link(archive), children: "Verified archive" })] }), _jsx("div", { style: { color: "#57606a", fontSize: 13, marginBottom: 8 }, children: archive ? "Verified and fixed nodes are kept for repeated checks." : "Click elements, compare with Expected, and mark ✓/✗. Token regression (S1): the appearance of ALL cards/menus/grids should not have changed; variables were moved to tokens.css without changing values." }), archive ? _jsx(ArchiveChecks, {}) : _jsx(ActiveChecks, {})] }));
|
|
1107
|
+
}
|
|
1108
|
+
/* ---------- 38/39. Media sources: common2 capture + viewer helpers ---------- */
|
|
1109
|
+
const MediaVideoDemo = () => {
|
|
1110
|
+
const media = useMediaSource("video", { fps: 12, replay: { history: 32, current: "last" } });
|
|
1111
|
+
const canvasRef = useRef(null);
|
|
1112
|
+
const [stats, setStats] = useState({ frames: 0, drawn: 0, perSec: 0, ageMs: 0 });
|
|
1113
|
+
const [error, setError] = useState(null);
|
|
1114
|
+
useEffect(() => {
|
|
1115
|
+
const canvas = canvasRef.current;
|
|
1116
|
+
if (!canvas)
|
|
1117
|
+
return;
|
|
1118
|
+
const view = Media.attachVideoCanvas(media.listen, canvas, { onError: e => setError(String(e)) });
|
|
1119
|
+
const timer = window.setInterval(() => setStats(view.stats()), 500);
|
|
1120
|
+
return () => { window.clearInterval(timer); view.off(); };
|
|
1121
|
+
}, [media.listen]);
|
|
1122
|
+
return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => void media.start(), children: "start camera" }), _jsx("button", { onClick: media.stop, children: "stop" }), _jsxs("b", { children: ["state: ", media.state] }), _jsxs("span", { style: { fontSize: 12 }, children: ["drawn ", stats.drawn, ", ", stats.perSec, "/s, age ", Math.round(stats.ageMs), "ms"] })] }), error && _jsxs("div", { style: { color: "#cf222e" }, children: ["viewer error: ", error] }), _jsx("canvas", { ref: canvasRef, width: 320, height: 180, style: { width: 320, height: 180, background: "#111", borderRadius: 8 } })] });
|
|
1123
|
+
};
|
|
1124
|
+
const MediaAudioDemo = () => {
|
|
1125
|
+
const media = useMediaSource("audio", { mode: "pcm", bufferSize: 4096, replay: { history: 64, current: "last" } });
|
|
1126
|
+
const playerRef = useRef(null);
|
|
1127
|
+
const [stats, setStats] = useState({ frames: 0, played: 0, dropped: 0, perSec: 0, ageMs: 0 });
|
|
1128
|
+
useEffect(() => {
|
|
1129
|
+
const player = Media.attachAudioPlayer(media.listen, { maxBacklogSec: .35 });
|
|
1130
|
+
playerRef.current = player;
|
|
1131
|
+
const timer = window.setInterval(() => setStats(player.stats()), 500);
|
|
1132
|
+
return () => { window.clearInterval(timer); player.off(); playerRef.current = null; };
|
|
1133
|
+
}, [media.listen]);
|
|
1134
|
+
return _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => { playerRef.current?.enable(); void media.start(); }, children: "enable + start mic" }), _jsx("button", { onClick: media.stop, children: "stop" }), _jsxs("b", { children: ["state: ", media.state] }), _jsxs("span", { style: { fontSize: 12 }, children: ["played ", stats.played, ", dropped ", stats.dropped, ", ", stats.perSec, "/s"] })] });
|
|
1135
|
+
};
|
|
1136
|
+
/* ---------- 40. Peer SDK: in-process mirrored store ---------- */
|
|
1137
|
+
const PeerSdkDemo = () => {
|
|
1138
|
+
const pair = useMemo(() => {
|
|
1139
|
+
const host = Peer.createPeerHost();
|
|
1140
|
+
const a = Peer.createPeerClient({ remote: host.connection("qa-peer-a").fragment, account: "qa-peer-a", initial: { value: 0 } });
|
|
1141
|
+
const b = Peer.createPeerClient({ remote: host.connection("qa-peer-b").fragment, account: "qa-peer-b", initial: { value: 0 } });
|
|
1142
|
+
return { host, a, b };
|
|
1143
|
+
}, []);
|
|
1144
|
+
useEffect(() => () => { pair.a.close(); pair.b.close(); }, [pair]);
|
|
1145
|
+
const remote = usePeer(pair.b, "qa-peer-a");
|
|
1146
|
+
const value = useStoreNode(remote.store.node.value);
|
|
1147
|
+
return _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => { pair.a.store.state.value += 1; }, children: "peer A +1" }), _jsx("button", { onClick: () => void remote.resync(), children: "resync B" }), _jsxs("b", { children: ["mirrored value: ", value.value] }), _jsxs("span", { style: { fontSize: 12 }, children: ["ready=", String(remote.ready), " route=", remote.route, " state=", remote.state, " seq=", remote.seq()] })] });
|
|
1148
|
+
};
|