wenay-react2 1.0.45 → 1.0.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +7 -1
  2. package/doc/changes/v1.0.46.md +16 -0
  3. package/doc/changes/v1.0.47.md +9 -0
  4. package/doc/examples/README.md +6 -0
  5. package/doc/examples/peer-call-media.tsx +8 -0
  6. package/doc/examples/stand.tsx +6 -0
  7. package/doc/native.md +37 -0
  8. package/doc/progress/common2-adoption-1.0.74.md +24 -0
  9. package/doc/progress/stand-as-examples-audit.md +22 -0
  10. package/doc/target/my.md +12 -0
  11. package/doc/wenay-react2.md +1 -1
  12. package/lib/common/demo/peerMedia.d.ts +6 -0
  13. package/lib/common/demo/peerMedia.js +128 -0
  14. package/lib/common/src/components/Toolbar/Toolbar.d.ts +4 -0
  15. package/lib/common/src/components/Toolbar/Toolbar.js +16 -7
  16. package/lib/common/src/components/index.d.ts +12 -0
  17. package/lib/common/src/components/index.js +12 -0
  18. package/lib/common/src/grid/columnState/CardList.d.ts +2 -0
  19. package/lib/common/src/grid/columnState/CardList.js +1 -1
  20. package/lib/common/src/grid/columnState/ColumnsMenu.d.ts +2 -0
  21. package/lib/common/src/grid/columnState/ColumnsMenu.js +3 -2
  22. package/lib/common/src/grid/columnState/columnState.d.ts +15 -0
  23. package/lib/common/src/grid/columnState/columnState.js +53 -5
  24. package/lib/common/src/hooks/index.d.ts +1 -0
  25. package/lib/common/src/hooks/index.js +1 -0
  26. package/lib/common/src/hooks/usePeerCall.d.ts +68 -0
  27. package/lib/common/src/hooks/usePeerCall.js +79 -0
  28. package/lib/common/src/hooks/useReorder.d.ts +2 -0
  29. package/lib/common/src/hooks/useReorder.js +3 -1
  30. package/lib/common/src/utils/memoryStore.d.ts +2 -2
  31. package/lib/common/testUseReact/qa.d.ts +1 -0
  32. package/lib/common/testUseReact/qa.js +1148 -0
  33. package/lib/common/testUseReact/replayVideo.d.ts +14 -0
  34. package/lib/common/testUseReact/replayVideo.js +311 -0
  35. package/lib/common/testUseReact/testParams.d.ts +1 -0
  36. package/lib/common/testUseReact/testParams.js +15 -0
  37. package/lib/common/testUseReact/useGrid.d.ts +2 -0
  38. package/lib/common/testUseReact/useGrid.js +62 -0
  39. package/lib/native/columnDots.d.ts +41 -0
  40. package/lib/native/columnDots.js +104 -0
  41. package/lib/native/columnState.d.ts +69 -0
  42. package/lib/native/columnState.js +209 -0
  43. package/lib/native/index.d.ts +3 -0
  44. package/lib/native/index.js +3 -0
  45. package/lib/style/style.css +24 -0
  46. package/package.json +6 -3
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  Documentation index only.
4
4
 
5
- - Brief API guide: [doc/wenay-react2.md](doc/wenay-react2.md)
5
+ - Brief API guide: [doc/wenay-react2.md](doc/wenay-react2.md)
6
+ - React Native/headless entrypoint: [doc/native.md](doc/native.md)
6
7
  - Detailed / rare API guide: [doc/wenay-react2-rare.md](doc/wenay-react2-rare.md)
7
8
  - Project functionality map: [doc/PROJECT_FUNCTIONALITY.md](doc/PROJECT_FUNCTIONALITY.md)
8
9
  - Example usage standards: [doc/EXAMPLE_USAGE.md](doc/EXAMPLE_USAGE.md)
@@ -13,3 +14,8 @@ Documentation index only.
13
14
  `wenay-common2` is an external dependency. Read its current docs from the installed module/package when needed; keep this README focused on `wenay-react2` documentation.
14
15
 
15
16
  Feature-specific docs may also live next to their code, for example agGrid4 docs under `src/common/src/grid/agGrid4/`.
17
+
18
+ ## Shipped examples
19
+
20
+ - [doc/examples/stand.tsx](doc/examples/stand.tsx) — весь интерактивный стенд (`wenay-react2/demo/stand`); Active = канон, Archive = regression/compat.
21
+ - [doc/examples/peer-call-media.tsx](doc/examples/peer-call-media.tsx) — Peer calls, presence, camera and microphone relay with server-owned ACL.
@@ -0,0 +1,16 @@
1
+ # v1.0.46 — common2 Calls, presence and media relay adoption
2
+
3
+ Status: ready for publication.
4
+
5
+ - Updated the declared `wenay-common2` dependency to `^1.0.74`.
6
+ - Added `usePeerCalls` over the common2 call manager; React renders call lifecycle but does not own signaling, busy/glare, timeout or closure policy.
7
+ - Added `usePeerPresence` for the host presence snapshot and edge stream.
8
+ - Added QA cards 41–44 and in-process tests for call lifecycle, presence edges and ACL revocation; card 43 carries a real camera frame through Media → relay → policy-filtered canvas.
9
+ - Verified with qa-check TypeScript, Jest, package build and the QA stand.
10
+ - Ships doc/examples/peer-call-media.tsx: a consumer template for calls, presence, camera/microphone relay and server-owned ACL.
11
+
12
+ - Ships the entire QA stand as wenay-react2/demo/stand; Active cards are canonical examples, Archive cards are regression/compat context.
13
+
14
+ - Added runtime-only live drag-preview across `useReorder`, Toolbar, ColumnsMenu, columnState and attached grid; persistence still happens only on drop.
15
+ - Added optional `CardList layout="compact"` for dense mobile key/value cards; the stacked layout remains default.
16
+ - Rebuilt QA card 27 controls on one CSS-grid track system so remove/insert actions cannot drift from their columns.
@@ -0,0 +1,9 @@
1
+ # 1.0.47
2
+
3
+ - Added the platform-neutral `wenay-react2/native` entrypoint.
4
+ - Added `createNativeColumnState`: an AsyncStorage-compatible controller for order, visibility, width, sorting, filters and groups without DOM, CSS, React DOM or ag-grid dependencies.
5
+ - Added `createNativeColumnDots`: a renderer-independent gesture model with live replacement, reorder, visibility, sorting and tear-off behavior.
6
+ - Added focused hydration, persistence, gesture and dependency-boundary tests.
7
+ - Fixed the shipped examples list formatting in README.
8
+
9
+ Verification: Jest, TypeScript build, package archive inspection and runtime import of `wenay-react2/native` from the packed artifact.
@@ -0,0 +1,6 @@
1
+ # Runnable consumer examples
2
+
3
+ - [`peer-call-media.tsx`](peer-call-media.tsx) — re-exports the interactive Peer/Media demos used by QA cards 41–44. Install the package and import from `wenay-react2/demo/peer-media`.
4
+
5
+ The component uses an in-process `Peer.createPeerHost`, so calls, presence, camera and microphone relay work without server credentials. For production, expose `publishOf(account)` and `watchOf(account)` from the RPC server and keep `canWatch` plus call authorization on that server.
6
+ - [stand.tsx](stand.tsx) — exports the entire interactive stand from wenay-react2/demo/stand. Active cards teach canonical APIs; Archive cards remain visible regression/compat examples.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * This is the same interactive component used by the QA stand cards 41–44.
3
+ * It is shipped as a supported demo entrypoint, not a private test helper.
4
+ *
5
+ * import {PeerCallDemo, PeerPresenceDemo, MediaRelayAclDemo, MediaRelayAudioDemo}
6
+ * from "wenay-react2/demo/peer-media"
7
+ */
8
+ export {PeerCallDemo, PeerPresenceDemo, MediaRelayAclDemo, MediaRelayAudioDemo} from "wenay-react2/demo/peer-media";
@@ -0,0 +1,6 @@
1
+ /** The complete interactive wenay-react2 stand, shipped in the package. */
2
+ export {QABoard} from "wenay-react2/demo/stand";
3
+
4
+ // import {createRoot} from "react-dom/client";
5
+ // import {QABoard} from "wenay-react2/demo/stand";
6
+ // createRoot(document.getElementById("root")!).render(<QABoard />);
package/doc/native.md ADDED
@@ -0,0 +1,37 @@
1
+ # React Native entrypoint
2
+
3
+ Import the renderer-neutral API from `wenay-react2/native`. It has no React DOM,
4
+ CSS, browser-global or ag-grid dependency.
5
+
6
+ ```ts
7
+ import AsyncStorage from '@react-native-async-storage/async-storage'
8
+ import {createNativeColumnDots, createNativeColumnState} from 'wenay-react2/native'
9
+
10
+ const columns = createNativeColumnState({
11
+ key: 'super-admin.columns',
12
+ columns: [
13
+ {key: 'server', title: 'Server', fixed: true},
14
+ {key: 'status', title: 'Status'},
15
+ {key: 'panic', title: 'Panic'},
16
+ ],
17
+ storage: AsyncStorage,
18
+ })
19
+ await columns.ready
20
+
21
+ const dots = createNativeColumnDots({state: columns, max: 8})
22
+ dots.begin('status', 40, 0)
23
+ dots.move(120, 0, {start: 0, length: 240})
24
+ dots.end()
25
+ ```
26
+
27
+ The persisted `v/order/visible/width/sort/filter/groups` shape matches the web
28
+ `ColumnsConfig`. Writes are serialized and local edits made while hydration is
29
+ pending win over stale storage.
30
+
31
+ `createNativeColumnDots` is a view-independent interaction model. Connect its
32
+ `begin/move/end` coordinates to PanResponder or Gesture Handler. Set `removeDirection` to `up`, `down` or `either` (the renderer-neutral default). It supports
33
+ live field replacement along the slider, explicit reorder and toggle, sticky
34
+ `asc -> desc -> off` sorting, and upward tear-off.
35
+
36
+ Call `dots.dispose()` and `columns.dispose()` with the owning screen. Use
37
+ `columns.api.flush()` when the latest AsyncStorage write must be awaited.
@@ -0,0 +1,24 @@
1
+ # wenay-common2 1.0.74 — adoption assessment
2
+
3
+ ## Upgrade
4
+
5
+ - Updated the workspace lockfile from `wenay-common2` 1.0.73 to 1.0.74.
6
+ - Compatibility: qa-check TypeScript, full Jest (20 suites / 67 tests), package build and diff-check pass.
7
+
8
+ ## Architecture decision
9
+
10
+ - `Peer.createCallManager` is a new application interaction lifecycle, not an extension of the mirrored-store `usePeer` adapter.
11
+ - React may bind call state and controls; common2 retains signaling envelopes, call-id ordering, busy/glare handling, timeouts and offline verdicts.
12
+ - Presence belongs to the host protocol (`list()` plus edge-triggered `changes`), not polling or a second React-owned store.
13
+ - `createMediaRelay` remains relay-first and server-authorized through `watchOf` / `canWatch`; a call UI attaches viewers only while active. React must not make ACL decisions.
14
+
15
+ ## Planned validation batch
16
+
17
+ Presence edges; ring/accept/active; media keyframe; hangup/decline/offline/busy; ACL revoke of an already-open viewer.
18
+ ## Implementation batch — 1.0.46
19
+
20
+ - Added `usePeerCalls(manager)`: a thin binding for ring/active state and explicit `call`; the app owns `manager.close()`.
21
+ - Added `usePeerPresence(presence)`: subscribe-first snapshot plus online/offline edge projection.
22
+ - QA cards 41–44 verify an in-process call lifecycle, presence disconnect/reconnect and media relay ACL revocation on an open viewer.
23
+ - Automated verification: qa-check TypeScript, real in-process hook tests, full Jest and package build.
24
+ - Manual Verify pending: QA card 43 — grant camera access, confirm canvas receives frames; revoke ACL and confirm viewer frame count freezes while source remains live; grant again and confirm it resumes. QA card 44 — grant microphone access, confirm audio player receives frames/plays; revoke and grant ACL with the same expectation.
@@ -0,0 +1,22 @@
1
+ # Stand as examples — audit
2
+
3
+ ## Current shape
4
+
5
+ - `src/common/testUseReact/qa.tsx` owns 44 hand-written cards and the component implementations are mostly in the same file.
6
+ - The board has only Active/Archive separation. It does not declare whether a card is canonical consumer guidance or internal regression/compat coverage.
7
+ - `doc/examples/peer-call-media.tsx` is shipped, but card 41–44 currently use a parallel implementation.
8
+
9
+ ## Decision
10
+
11
+ Turn the stand into a renderer/acceptance layer over a small set of canonical demo modules. Ship those modules in `demo/`; keep isolated repros and compat examples in QA/regression only. Use a manifest per card to link source demo, automated test and manual verification.
12
+
13
+ ## First migration
14
+
15
+ Extract Media/Peer calls, presence, camera relay and microphone relay. The QA cards must import the same component that is shipped in the package. No new transport/ACL code belongs in React.
16
+ ## Batch 1 complete — Media/Peer
17
+
18
+ - Extracted cards 41–44 into `src/common/demo/peerMedia.tsx`.
19
+ - QA imports those exact components; package exports `wenay-react2/demo/peer-media`.
20
+ - `doc/examples/peer-call-media.tsx` re-exports the public demo entrypoint.
21
+ - Remaining batches: Replay, then toolbar/columnState/grid; archive stays internal.
22
+ - Verification: qa-check TypeScript, full Jest (21 suites / 71 tests), package build and Vite stand cards 41–44 all pass. The package is browser-oriented; direct Node require is not a supported validation surface for its extensionless ESM build.
package/doc/target/my.md CHANGED
@@ -27,6 +27,18 @@ _Пусто._
27
27
  - Оценка: 1 — simple/low-risk (аддитивно); 2 — medium (in-proc fake стенд, по образцу peer-sdk oracle); 3 — docs-first. Приоритет: после architecture-fix очереди A1-A10.
28
28
  - **Аудит 1.0.71–1.0.73 (2026-07-10, dependency обновлена до 1.0.73)**: `Peer` repair/resync (1.0.71) остаётся core protocol — React не должен повторять gap/journal state machine; `usePeer` может лишь отразить `peer(account)` store/route и вызвать `resync()` по app-owned transport reconnect. Media hidden-tab workers/ImageCapture (1.0.72) принимаем автоматически через core defaults, без React-кода. Media viewer helpers (1.0.73: canvas/audio/publish pipe) — core imperative hot path; React-кандидаты только тонкие lifecycle bindings над ref (`useMediaVideoCanvas`, `useMediaAudioPlayer`), без frame-level React state. Сначала docs/recipe + QA card с реальным consumer; хук — лишь если recipe повторяется.
29
29
 
30
+ - **common2 1.0.74 adoption: Calls, presence and protected Media relay — implementation complete in 1.0.46; manual camera verify pending.** Отдельный продуктовый проход; не смешан с `usePeer`.
31
+ - **Call controller/view — DONE.** — поверх `Peer.createCallManager({port: callPortOf(remote), self})`: lifecycle `ringing → active → ended`, входящий звонок, accept/decline/hangup, offline/busy/timeout reason. React хранит только UI-состояние/подписки; signal envelopes, glare и busy gate остаются в common2.
32
+ - **Presence — DONE.** — тонкая подписка на `peerHost.presence` с initial `list()` после subscription; показывать только edges online/offline, не polling и не собственный presence-store.
33
+ - **Media relay + ACL — DONE (binding/QA recipe).** — call view подключает viewers только в active состоянии. Сервер задаёт `watchOf(watcher)` + `canWatch`; React не получает права решать доступ. Relay-first остаётся default, direct — явный opt-in.
34
+ - **Стенд и тесты одним batch:** два in-process peer-клиента: presence edge, ring/accept/active, audio/video relay keyframe, hangup/decline/offline/busy; отдельный ACL-revoke кейс, доказывающий закрытие уже открытой подписки.
35
+ - Оценка: medium/high; сначала определить server-owned authorize policy и UX входящего звонка, затем делать адаптер. `usePeer` не расширять до call manager — это другой lifecycle.
36
+ - **Stand → shipped examples: разделить интерактивные consumer demos и internal QA-regressions** (архитектурная переработка стенда).
37
+ - Проблема: 44 карточки живут в одном `testUseReact/qa.tsx`; сценарий может одновременно быть хорошим consumer-примером и проверкой, но npm сейчас получает отдельную копию в `doc/examples`. Это создаёт риск расхождения.
38
+ - Целевая модель: весь `demo/stand` поставляется как интерактивный пример; Active-карточки — canonical consumer guidance, Archive-карточки — явный regression/compat context. QA-доска добавляет manual acceptance metadata. Карточка получает manifest: category, canonical/regression, source example, automated test, manual check.
39
+ - Миграция batch-ами: **1) Media/Peer calls/presence/relay — DONE in 1.0.46**: единый export `wenay-react2/demo/peer-media`, который рендерят QA cards 41–44; **2) Replay cards 23/24/33/34**; **3) toolbar/columnState/grid**; legacy archive оставить internal до отдельного решения. Никакого big-bang rewrite и никаких изменений публичного API.
40
+ - Acceptance: npm tarball содержит запускаемые исходники `demo/` + README; QA card импортирует тот же canonical component, а не его копию; каждое demo имеет test/ручной чек; archive regression-карточки явно не называются рекомендуемым путём.
41
+ - Оценка: high usability / medium effort / medium migration risk. Сначала extraction Media/Peer, потому что его уже можно проверить картами 41–44.
30
42
  - **Context menu / Menu: controller-first split**.
31
43
  - Прогресс/оценка: [../progress/hook-controller-opportunities.md](../progress/hook-controller-opportunities.md)
32
44
  - Смысл: выделить `createContextMenuController` / `useMenuLayer` + `MenuView` и action-layer для async/progress/submenu flow, не ломая `contextMenu.openAt`, `contextMenu.Layer` и `contextMenu.stats`.
@@ -472,7 +472,7 @@ const tt = useReplayHistory(history, (frame) => draw(frame), {head: () => replay
472
472
  tt.live; tt.seq; tt.head; tt.pause(); tt.play(); tt.seek({seq})
473
473
  ```
474
474
 
475
- Route hand-off here is the MANUAL surface (`switchRoute`). common2 1.0.67 `Replay.createRouteCoordinator` moves route decisions (policy, promote/fallback/shadow) out of the consumer; a coordinator `link.subscribe(cb)` handle has the same shape (`ready, seq(), label(), active()`) and survives every route change, so components consuming a coordinator link do not need `switchRoute` at all. common2 1.0.68 supplies the direct transport for it: `createWebRtcConnector` with an app-injected `rtc: () => new RTCPeerConnection(cfg)` factory (the browser — i.e. the React app — owns that injection) and signaling over the existing RPC socket (`createSignalHub`). common2 1.0.69 wraps the whole stack into the `Peer` SDK (`wenay-common2/peer`): `createPeerClient(...).peer(account)` returns a live mirrored store (works with `useStoreNode`/`useStoreKeys` as-is) + route control, surviving relay<->direct hand-offs in one seq space. `usePeer(client, account)` is the thin React adapter: it returns that mirror plus low-frequency route/status and explicit route/resync controls; it does not own journal, repair or transport state.
475
+ Route hand-off here is the MANUAL surface (`switchRoute`). common2 1.0.67 `Replay.createRouteCoordinator` moves route decisions (policy, promote/fallback/shadow) out of the consumer; a coordinator `link.subscribe(cb)` handle has the same shape (`ready, seq(), label(), active()`) and survives every route change, so components consuming a coordinator link do not need `switchRoute` at all. common2 1.0.68 supplies the direct transport for it: `createWebRtcConnector` with an app-injected `rtc: () => new RTCPeerConnection(cfg)` factory (the browser — i.e. the React app — owns that injection) and signaling over the existing RPC socket (`createSignalHub`). common2 1.0.69 wraps the whole stack into the `Peer` SDK (`wenay-common2/peer`): `createPeerClient(...).peer(account)` returns a live mirrored store (works with `useStoreNode`/`useStoreKeys` as-is) + route control, surviving relay<->direct hand-offs in one seq space. `usePeer(client, account)` is the thin React adapter: it returns that mirror plus low-frequency route/status and explicit route/resync controls; it does not own journal, repair or transport state. `usePeerCalls(manager)` binds a `Peer.createCallManager` to rings/active/call UI state without owning `manager.close()` or signal policy. The exact interactive components used by QA cards 41–44 ship from `wenay-react2/demo/peer-media`; [`doc/examples/peer-call-media.tsx`](examples/peer-call-media.tsx) shows the import. `usePeerPresence(fragment.presence)` subscribes before reading the host snapshot and exposes online/offline edges. For calls with media, wire `Peer.createMediaRelay` on the server (`publishOf(owner)`, `watchOf(watcher)`, `canWatch`) and attach viewers only when the server-owned call policy grants access; React must never make the ACL decision.
476
476
 
477
477
  Media lines (common2 1.0.66 `Media.createAudioSource` / `Media.createVideoSource`) are ordinary binary Listen sources; with `replay:true` their `listen` is a replay line, so `useReplaySubscribe` / `useReplayFrame` consume mic/camera frames with no media-specific hook. `useMediaSource(kind, options)` is only the capture lifecycle adapter (`start`, `stop`, device selection, state and stats); it stops a started source on unmount and returns `listen` unchanged. Each frame is one `Uint8Array` (`Media.decodeMediaFrame`); draw/play it via ref (canvas, AudioContext), never useState — the same rule as any high-frequency line. Without `replay`, the plain `listen` works with the listen hooks above.
478
478
 
@@ -0,0 +1,6 @@
1
+ export declare const PeerCallDemo: () => import("react/jsx-runtime").JSX.Element;
2
+ export declare const PeerPresenceDemo: () => import("react/jsx-runtime").JSX.Element;
3
+ export declare const MediaRelayAclDemo: () => import("react/jsx-runtime").JSX.Element;
4
+ export declare const MediaRelayAudioDemo: () => import("react/jsx-runtime").JSX.Element;
5
+ /** Complete messenger-style call: viewer attachment follows the active call. */
6
+ export declare const PeerCallVideoAudioDemo: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,128 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Shipped interactive Peer/Media demonstrations. The QA board imports these exact
4
+ * components; consumers can import them from `wenay-react2/demo/peer-media`.
5
+ * They use an in-process host intentionally, so no server credentials are needed.
6
+ */
7
+ import { useEffect, useMemo, useRef, useState } from "react";
8
+ import { Media, Peer } from "wenay-common2";
9
+ import { useMediaSource } from "../src/hooks/useMedia";
10
+ import { usePeerCalls, usePeerPresence } from "../src/hooks/usePeerCall";
11
+ /* ---------- 41/42. Peer calls and host presence ---------- */
12
+ export const PeerCallDemo = () => {
13
+ const pair = useMemo(() => {
14
+ const host = Peer.createPeerHost();
15
+ const a = host.connection("qa-call-a");
16
+ const b = host.connection("qa-call-b");
17
+ return {
18
+ host,
19
+ caller: Peer.createCallManager({ port: Peer.callPortOf(a.fragment), self: "qa-call-a" }),
20
+ callee: Peer.createCallManager({ port: Peer.callPortOf(b.fragment), self: "qa-call-b" }),
21
+ };
22
+ }, []);
23
+ useEffect(() => () => { pair.caller.close(); pair.callee.close(); pair.host.close(); }, [pair]);
24
+ const a = usePeerCalls(pair.caller);
25
+ const b = usePeerCalls(pair.callee);
26
+ const incoming = b.rings.find(call => call.direction === "in");
27
+ return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { disabled: !a.ready || Boolean(a.active), onClick: () => a.call("qa-call-b", { from: "QA" }), children: "call B" }), incoming && _jsxs(_Fragment, { children: [_jsx("button", { onClick: () => incoming.accept(), children: "B accept" }), _jsx("button", { onClick: () => incoming.decline(), children: "B decline" })] }), a.active && _jsx("button", { onClick: () => a.active?.hangup(), children: "A hang up" }), _jsxs("b", { children: ["A: ", a.active?.state() ?? "idle", "; B: ", b.active?.state() ?? (incoming ? "ringing" : "idle")] })] }), _jsxs("span", { style: { fontSize: 12 }, children: ["manager ready: A=", String(a.ready), " B=", String(b.ready), "; rings B=", b.rings.length] })] });
28
+ };
29
+ export const PeerPresenceDemo = () => {
30
+ const pair = useMemo(() => {
31
+ const host = Peer.createPeerHost();
32
+ const a = host.connection("qa-presence-a");
33
+ const b = host.connection("qa-presence-b");
34
+ return { host, a, b };
35
+ }, []);
36
+ const [connected, setConnected] = useState(true);
37
+ useEffect(() => () => { pair.a.close(); pair.b.close(); pair.host.close(); }, [pair]);
38
+ const presence = usePeerPresence(pair.a.fragment.presence);
39
+ return _jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { disabled: !connected, onClick: () => { pair.b.close(); setConnected(false); }, children: "disconnect B" }), _jsx("button", { disabled: connected, onClick: () => { pair.b = pair.host.connection("qa-presence-b"); setConnected(true); }, children: "reconnect B" }), _jsxs("b", { children: ["online: ", presence.accounts.join(", ") || "none"] })] });
40
+ };
41
+ export const MediaRelayAclDemo = () => {
42
+ const media = useMediaSource("video", { fps: 12, replay: { history: 8, current: "last" } });
43
+ const allowed = useRef(true);
44
+ const relay = useMemo(() => Peer.createMediaRelay({ lines: { camera: "video" }, canWatch: () => allowed.current }), [allowed]);
45
+ const canvasRef = useRef(null);
46
+ const [granted, setGranted] = useState(true);
47
+ const [stats, setStats] = useState({ frames: 0, drawn: 0, ageMs: 0 });
48
+ const [error, setError] = useState(null);
49
+ const publish = useMemo(() => {
50
+ const publishLine = relay.publishOf("qa-media-owner");
51
+ return (frame, sentAt) => publishLine("camera", frame, sentAt ?? Date.now());
52
+ }, [relay]);
53
+ useEffect(() => Media.pipeMediaPublish(media.listen, publish, { onError: error => setError(String(error)) }), [media.listen, publish]);
54
+ useEffect(() => {
55
+ const canvas = canvasRef.current;
56
+ if (!canvas)
57
+ return;
58
+ const watcher = relay.watchOf("qa-media-watcher");
59
+ const view = Media.attachVideoCanvas(watcher["qa-media-owner"].camera, canvas, { onError: error => setError(String(error)) });
60
+ const timer = window.setInterval(() => setStats(view.stats()), 500);
61
+ return () => { window.clearInterval(timer); view.off(); };
62
+ }, [relay]);
63
+ useEffect(() => () => relay.close(), [relay]);
64
+ const setAccess = (next) => { allowed.current = next; setGranted(next); };
65
+ 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 relay camera" }), _jsx("button", { onClick: media.stop, children: "stop" }), _jsx("button", { onClick: () => setAccess(!granted), children: granted ? "revoke ACL" : "grant ACL" }), _jsxs("b", { children: ["source: ", media.state, "; ACL: ", granted ? "granted" : "revoked"] }), _jsxs("span", { style: { fontSize: 12 }, children: ["viewer drawn ", stats.drawn, ", frames ", stats.frames, ", age ", Math.round(stats.ageMs), "ms"] })] }), error && _jsxs("div", { style: { color: "#cf222e" }, children: ["relay viewer error: ", error] }), _jsx("canvas", { ref: canvasRef, width: 320, height: 180, style: { width: 320, height: 180, background: "#111", borderRadius: 8 } })] });
66
+ };
67
+ export const MediaRelayAudioDemo = () => {
68
+ const media = useMediaSource("audio", { mode: "pcm", bufferSize: 4096, replay: { history: 64, current: "last" } });
69
+ const allowed = useRef(true);
70
+ const relay = useMemo(() => Peer.createMediaRelay({ lines: { mic: "audio" }, canWatch: () => allowed.current }), [allowed]);
71
+ const playerRef = useRef(null);
72
+ const [granted, setGranted] = useState(true);
73
+ const [stats, setStats] = useState({ frames: 0, played: 0, dropped: 0, ageMs: 0 });
74
+ const [error, setError] = useState(null);
75
+ const publish = useMemo(() => {
76
+ const publishLine = relay.publishOf("qa-audio-owner");
77
+ return (frame, sentAt) => publishLine("mic", frame, sentAt ?? Date.now());
78
+ }, [relay]);
79
+ useEffect(() => Media.pipeMediaPublish(media.listen, publish, { onError: error => setError(String(error)) }), [media.listen, publish]);
80
+ useEffect(() => {
81
+ const watcher = relay.watchOf("qa-audio-watcher");
82
+ const player = Media.attachAudioPlayer(watcher["qa-audio-owner"].mic, { maxBacklogSec: .35, onError: error => setError(String(error)) });
83
+ playerRef.current = player;
84
+ const timer = window.setInterval(() => setStats(player.stats()), 500);
85
+ return () => { window.clearInterval(timer); player.off(); playerRef.current = null; };
86
+ }, [relay]);
87
+ useEffect(() => () => relay.close(), [relay]);
88
+ const setAccess = (next) => { allowed.current = next; setGranted(next); };
89
+ 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 relay mic" }), _jsx("button", { onClick: media.stop, children: "stop" }), _jsx("button", { onClick: () => setAccess(!granted), children: granted ? "revoke audio ACL" : "grant audio ACL" }), _jsxs("b", { children: ["source: ", media.state, "; ACL: ", granted ? "granted" : "revoked"] }), _jsxs("span", { style: { fontSize: 12 }, children: ["played ", stats.played, ", frames ", stats.frames, ", dropped ", stats.dropped, ", age ", Math.round(stats.ageMs), "ms"] }), error && _jsxs("span", { style: { color: "#cf222e" }, children: ["player error: ", error] })] });
90
+ };
91
+ /** Complete messenger-style call: viewer attachment follows the active call. */
92
+ export const PeerCallVideoAudioDemo = () => {
93
+ const pair = useMemo(() => {
94
+ const host = Peer.createPeerHost();
95
+ const a = host.connection("qa-av-a");
96
+ const b = host.connection("qa-av-b");
97
+ return { host, caller: Peer.createCallManager({ port: Peer.callPortOf(a.fragment), self: "qa-av-a" }), callee: Peer.createCallManager({ port: Peer.callPortOf(b.fragment), self: "qa-av-b" }) };
98
+ }, []);
99
+ useEffect(() => () => { pair.caller.close(); pair.callee.close(); pair.host.close(); }, [pair]);
100
+ const caller = usePeerCalls(pair.caller);
101
+ const callee = usePeerCalls(pair.callee);
102
+ const active = Boolean(caller.active || callee.active);
103
+ const allowed = useRef(false);
104
+ allowed.current = active;
105
+ const relay = useMemo(() => Peer.createMediaRelay({ lines: { camera: "video", microphone: "audio" }, canWatch: () => allowed.current }), [allowed]);
106
+ useEffect(() => () => relay.close(), [relay]);
107
+ const video = useMediaSource("video", { fps: 12 });
108
+ const audio = useMediaSource("audio", { mode: "pcm", bufferSize: 4096 });
109
+ const canvasRef = useRef(null);
110
+ const playerRef = useRef(null);
111
+ const [drawn, setDrawn] = useState(0);
112
+ const publishVideo = useMemo(() => { const send = relay.publishOf("qa-av-a"); return (frame, sentAt) => send("camera", frame, sentAt ?? Date.now()); }, [relay]);
113
+ const publishAudio = useMemo(() => { const send = relay.publishOf("qa-av-a"); return (frame, sentAt) => send("microphone", frame, sentAt ?? Date.now()); }, [relay]);
114
+ useEffect(() => Media.pipeMediaPublish(video.listen, publishVideo), [video.listen, publishVideo]);
115
+ useEffect(() => Media.pipeMediaPublish(audio.listen, publishAudio), [audio.listen, publishAudio]);
116
+ useEffect(() => {
117
+ if (!active || !canvasRef.current)
118
+ return;
119
+ const watcher = relay.watchOf("qa-av-b");
120
+ const view = Media.attachVideoCanvas(watcher["qa-av-a"].camera, canvasRef.current);
121
+ const player = Media.attachAudioPlayer(watcher["qa-av-a"].microphone, { maxBacklogSec: .35 });
122
+ playerRef.current = player;
123
+ const timer = window.setInterval(() => setDrawn(view.stats().drawn), 500);
124
+ return () => { window.clearInterval(timer); view.off(); player.off(); playerRef.current = null; };
125
+ }, [active, relay]);
126
+ const incoming = callee.rings.find(call => call.direction === "in");
127
+ return _jsxs("div", { style: { display: "grid", gap: 8 }, children: [_jsxs("div", { style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" }, children: [_jsx("button", { onClick: () => { playerRef.current?.enable(); void video.start(); void audio.start(); }, children: "enable camera + mic" }), !active && !incoming && _jsx("button", { onClick: () => caller.call("qa-av-b"), children: "call B" }), incoming && _jsx("button", { onClick: () => incoming.accept(), children: "B accept call" }), caller.active && _jsx("button", { onClick: () => caller.active?.hangup(), children: "hang up" }), _jsxs("b", { children: ["call: ", active ? "active — media attached" : incoming ? "ringing" : "idle", "; video frames: ", drawn] })] }), _jsx("canvas", { ref: canvasRef, width: 320, height: 180, style: { width: 320, height: 180, background: "#111", borderRadius: 8 } })] });
128
+ };
@@ -54,6 +54,10 @@ export type UiListSource = {
54
54
  setConfig: (next: UiListConfig) => void;
55
55
  /** re-emitted as the toolbar's own onChange (grid-driven changes included) */
56
56
  onChange?: (cb: (cfg: UiListConfig) => void) => () => void;
57
+ /** optional transient display order; never persisted */
58
+ useBaseConfig?: () => UiListConfig;
59
+ getBaseConfig?: () => UiListConfig;
60
+ setPreview?: (order: string[] | null) => void;
57
61
  };
58
62
  export type ToolbarDensity = {
59
63
  key: string;
@@ -135,9 +135,9 @@ export function createToolbar(opts) {
135
135
  * items, an unregistered density) - never crash, never drop user data that
136
136
  * still applies: unknown keys are filtered out, missing items are appended
137
137
  * (default-visible), fixed items are pinned back to their descriptor index. */
138
- function normalize() {
138
+ function normalize(base = false) {
139
139
  const known = new Set(opts.items.map(i => i.key));
140
- const extRaw = ext?.getConfig();
140
+ const extRaw = ext ? (base ? (ext.getBaseConfig?.() ?? ext.getConfig()) : ext.getConfig()) : undefined;
141
141
  const localRaw = { order: st.order, visible: st.visible };
142
142
  const raw = ext && sourceMode == 'orderVisible' ? extRaw : localRaw;
143
143
  const sourceKeys = ext && sourceMode == 'order' ? sourceKeySet(extRaw, known) : new Set();
@@ -189,7 +189,7 @@ export function createToolbar(opts) {
189
189
  emitChange(normalize());
190
190
  }
191
191
  else if (ext && sourceMode == 'order') {
192
- const extRaw = ext.getConfig();
192
+ const extRaw = ext.getBaseConfig?.() ?? ext.getConfig();
193
193
  const known = new Set(opts.items.map(i => i.key));
194
194
  const sourceKeys = sourceKeySet(extRaw, known);
195
195
  const curSourceOrder = (Array.isArray(extRaw.order) ? extRaw.order : []).filter(k => sourceKeys.has(k));
@@ -219,9 +219,12 @@ export function createToolbar(opts) {
219
219
  }
220
220
  const getConfig = () => normalize();
221
221
  /** subscription to everything the toolbar renders from (hook) */
222
- function useSubscribe() {
222
+ function useSubscribe(base = false) {
223
223
  stApi.use();
224
- ext?.useConfig(); // ext is per-instance constant - hook order is stable
224
+ if (base && ext?.useBaseConfig)
225
+ ext.useBaseConfig();
226
+ else
227
+ ext?.useConfig(); // ext is per-instance constant - hook order is stable
225
228
  }
226
229
  function useConfig() {
227
230
  useSubscribe();
@@ -303,9 +306,14 @@ export function createToolbar(opts) {
303
306
  * never move in the preview and a drop never lands elsewhere than shown.
304
307
  * Plus arrow keys on the focused handle; fixed rows have neither. */
305
308
  function Settings(p = {}) {
306
- useSubscribe();
309
+ // BASE config on purpose: Settings is the preview AUTHOR (onPreviewChange ->
310
+ // ext.setPreview). Rendering from the display config would feed its own
311
+ // preview back mid-drag: the rows' DOM order changes under the pointer
312
+ // (useReorder requires it stable), styles jump, and the drop re-applies
313
+ // the move over the already-previewed order - one extra row.
314
+ useSubscribe(true);
307
315
  densitiesApi.use();
308
- const cfg = normalize();
316
+ const cfg = normalize(true);
309
317
  const base = p.className ?? 'wenaySegBtn';
310
318
  const activeCls = p.activeClassName ?? 'wenaySegBtnActive';
311
319
  const byKey = new Map(opts.items.map(i => [i.key, i]));
@@ -318,6 +326,7 @@ export function createToolbar(opts) {
318
326
  commit: order => setConfig({ ...cfg, order }),
319
327
  move: movedOrder,
320
328
  canDrag: k => !byKey.get(k)?.fixed,
329
+ onPreviewChange: order => ext?.setPreview?.(order),
321
330
  });
322
331
  function onHandleKey(key, e) {
323
332
  const step = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 }[e.key];
@@ -0,0 +1,12 @@
1
+ export * from './Buttons';
2
+ export * from './Dnd';
3
+ export * from './Menu';
4
+ export * from './Modal';
5
+ export * from './Input';
6
+ export * from './Parameters';
7
+ export * from './ParamsEditor';
8
+ export * from './MyResizeObserver';
9
+ export * from './Other';
10
+ export * from './Settings';
11
+ export * from './UiSlot';
12
+ export * from './Toolbar';
@@ -0,0 +1,12 @@
1
+ export * from './Buttons';
2
+ export * from './Dnd';
3
+ export * from './Menu';
4
+ export * from './Modal';
5
+ export * from './Input';
6
+ export * from './Parameters';
7
+ export * from './ParamsEditor';
8
+ export * from './MyResizeObserver';
9
+ export * from './Other';
10
+ export * from './Settings';
11
+ export * from './UiSlot';
12
+ export * from './Toolbar';
@@ -6,6 +6,8 @@ export declare function CardList<T extends object>(p: {
6
6
  getId?: (row: T, index: number) => string;
7
7
  /** custom field renderer; default = String(row[key]) */
8
8
  renderValue?: (key: string, row: T) => React.ReactNode;
9
+ /** default stacked key/value rows; compact packs fields into a responsive two-column grid */
10
+ layout?: 'stack' | 'compact';
9
11
  className?: string;
10
12
  style?: React.CSSProperties;
11
13
  }): import("react/jsx-runtime").JSX.Element;
@@ -27,5 +27,5 @@ export function CardList(p) {
27
27
  const { key, dir } = cfg.sort;
28
28
  rows.sort((a, b) => cmpValues(a[key], b[key]) * (dir == 'asc' ? 1 : -1));
29
29
  }
30
- return _jsx("div", { className: cx(['wenayCardList', p.className]), style: p.style, children: rows.map((row, i) => (_jsxs("div", { className: 'wenayCardListItem', children: [_jsxs("div", { className: cx(['wenayCardListHeader', fieldKeys.length == 0 && 'wenayCardListHeader_compact']), children: [_jsx("b", { className: 'wenayCardListTitle', children: titleKey ? value(titleKey, row) : '' }), accentKey && _jsx("span", { className: 'wenayCardListAccent', children: value(accentKey, row) })] }), fieldKeys.map(k => (_jsxs("div", { className: 'wenayCardListField', children: [_jsx("span", { className: 'wenayCardListLabel', children: byKey.get(k)?.title ?? k }), _jsx("span", { className: 'wenayCardListValue', children: value(k, row) })] }, k)))] }, p.getId?.(row, i) ?? i))) });
30
+ return _jsx("div", { className: cx(['wenayCardList', p.className]), style: p.style, children: rows.map((row, i) => (_jsxs("div", { className: cx(['wenayCardListItem', p.layout == 'compact' && 'wenayCardListItem_compact']), children: [_jsxs("div", { className: cx(['wenayCardListHeader', fieldKeys.length == 0 && 'wenayCardListHeader_compact']), children: [_jsx("b", { className: 'wenayCardListTitle', children: titleKey ? value(titleKey, row) : '' }), accentKey && _jsx("span", { className: 'wenayCardListAccent', children: value(accentKey, row) })] }), _jsx("div", { className: 'wenayCardListFields', children: fieldKeys.map(k => (_jsxs("div", { className: 'wenayCardListField', children: [_jsx("span", { className: 'wenayCardListLabel', children: byKey.get(k)?.short ?? byKey.get(k)?.title ?? k }), _jsx("span", { className: 'wenayCardListValue', children: value(k, row) })] }, k))) })] }, p.getId?.(row, i) ?? i))) });
31
31
  }
@@ -25,6 +25,8 @@ export declare function MenuStrip(p: {
25
25
  onItem?: (key: string, e: React.MouseEvent) => void;
26
26
  /** present = items are drag-reorderable (one commit per drop) */
27
27
  onMove?: (order: string[]) => void;
28
+ /** transient order while pointer is held; null on drop/cancel */
29
+ onPreview?: (order: string[] | null) => void;
28
30
  /** preview rule for the drop (fixed pinning etc.) - same contract as
29
31
  * useReorder.move, so the preview shows exactly what onMove receives */
30
32
  move?: (order: string[], key: string, to: number) => string[];
@@ -45,6 +45,7 @@ export function MenuStrip(p) {
45
45
  move: p.move,
46
46
  canDrag: k => !!p.onMove && !p.items.find(i => i.key == k)?.fixed,
47
47
  holdMs: p.holdMs ?? 150,
48
+ onPreviewChange: p.onPreview,
48
49
  });
49
50
  // A drag that ends over the button it started on still produces a browser
50
51
  // click - without this guard every snapped-back drag would ALSO toggle the
@@ -76,7 +77,7 @@ export function MenuStrip(p) {
76
77
  * on / off. Default click toggles visibility; pass onItem for richer,
77
78
  * multi-state semantics - the strip below stays untouched. */
78
79
  export function ColumnsMenu(p) {
79
- const cfg = p.state.api.useConfig();
80
+ const cfg = p.state.api.useDisplayConfig();
80
81
  const present = p.state.api.usePresent();
81
82
  const byKey = new Map(p.state.columns.map(c => [c.key, c]));
82
83
  const items = cfg.order.filter(k => byKey.has(k)).map(k => {
@@ -102,5 +103,5 @@ export function ColumnsMenu(p) {
102
103
  function movedOrder(order, key, to) {
103
104
  return movedOrderWithFixed(order, key, to, p.state.columns);
104
105
  }
105
- return _jsx(MenuStrip, { items: items, tail: p.tail, onItem: onItem, onMove: p.reorder == false ? undefined : order => p.state.api.move(order), move: movedOrder, holdMs: p.holdMs, compact: p.compact, className: p.className, style: p.style });
106
+ return _jsx(MenuStrip, { items: items, tail: p.tail, onItem: onItem, onMove: p.reorder == false ? undefined : order => p.state.api.move(order), onPreview: p.reorder == false ? undefined : order => p.state.api.setPreviewOrder(order), move: movedOrder, holdMs: p.holdMs, compact: p.compact, className: p.className, style: p.style });
106
107
  }
@@ -82,6 +82,8 @@ export declare function createColumnState(opts: {
82
82
  [key: string]: true;
83
83
  } | null;
84
84
  setPresentGate: (keys: string[] | null) => void;
85
+ useDisplayConfig: () => ColumnsConfig;
86
+ setPreviewOrder: (order: string[] | null) => void;
85
87
  listSource: {
86
88
  useConfig(): {
87
89
  order: string[];
@@ -95,6 +97,19 @@ export declare function createColumnState(opts: {
95
97
  [key: string]: boolean;
96
98
  };
97
99
  };
100
+ useBaseConfig(): {
101
+ order: string[];
102
+ visible: {
103
+ [key: string]: boolean;
104
+ };
105
+ };
106
+ getBaseConfig(): {
107
+ order: string[];
108
+ visible: {
109
+ [key: string]: boolean;
110
+ };
111
+ };
112
+ setPreview: (order: string[] | null) => void;
98
113
  setConfig(next: {
99
114
  order: string[];
100
115
  visible: {