wenay-react2 1.0.43 → 1.0.45

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.
@@ -0,0 +1,14 @@
1
+ # v1.0.44
2
+
3
+ Date: 2026-07-10
4
+
5
+ Status: released.
6
+
7
+ - Fixed: `createColumnState` no longer persists `visible=false` for a column
8
+ hidden solely by runtime `presentGate` when another grid interaction is read
9
+ back. Opening the gate restores the user's saved visibility as intended.
10
+ - Added: regression coverage for gated-column readback and normal user
11
+ visibility changes while the gate is open.
12
+ - Verification: repro first failed on v1.0.43 (`visible.b` became `false` after
13
+ an unrelated resize); targeted Jest 3/3; full Jest 64/64; `tsc -p
14
+ tsconfig.qa-check.json --noEmit`; `pnpm run build`.
@@ -0,0 +1,9 @@
1
+ # v1.0.45 — Media and Peer React adapters
2
+
3
+ Status: ready for publication.
4
+
5
+ - Updated `wenay-common2` to `^1.0.73`.
6
+ - Added `useMediaSource` for capture lifecycle only; video/audio frames stay outside React state.
7
+ - Added `usePeer` as a thin Peer SDK adapter with mirrored store, route/status and explicit resync controls.
8
+ - Added QA stand cards 38–40 and focused hook tests.
9
+ - Verified with qa-check TypeScript, full Jest, package build, diff-check and the live stand; manual stand verification was confirmed by the user.
@@ -0,0 +1,28 @@
1
+ # ColumnState present-gate — progress
2
+
3
+ ## Scope
4
+
5
+ Fix the v1.0.43 regression reported by a consumer: a grid event must not persist
6
+ `visible=false` for a column that is hidden only by the runtime `presentGate`.
7
+
8
+ ## Contract
9
+
10
+ `presentGate` is application-owned runtime presence. `visible` is user-owned,
11
+ persisted configuration. A grid readback may update visibility only when the
12
+ column passes the gate; order, width, sort and filter continue to read back for
13
+ all known columns.
14
+
15
+ ## Plan
16
+
17
+ 1. Reproduce against current source with a fake grid. **Confirmed:** an unrelated
18
+ resize changed gated `b` from persisted `visible=true` to `false`.
19
+ 2. Guard the visibility fold in `readFromGrid` with `passesPresentGate`. **Done.**
20
+ 3. Add regression coverage for closed and open gates. **Done:** fake GridApi
21
+ covers both the protected gated column and a normal open-gate visibility edit.
22
+ 4. Run TypeScript, Jest and package build; record release notes. **Done:**
23
+ targeted Jest 3/3, full Jest 64/64, qa-check TypeScript and build passed.
24
+
25
+ ## Result
26
+
27
+ Published as `wenay-react2@1.0.44`; durable details are in
28
+ `doc/changes/v1.0.44.md`.
@@ -0,0 +1,30 @@
1
+ # wenay-common2 1.0.73 adoption — progress
2
+
3
+ ## Update
4
+
5
+ - Previous dependency: 1.0.70.
6
+ - npm latest verified: 1.0.73.
7
+ - Updated dependency and lockfile to 1.0.73.
8
+ - Compatibility: `tsc -p tsconfig.qa-check.json --noEmit` and full Jest 21 suites / 68 tests pass.
9
+
10
+ ## Architecture decision
11
+
12
+ | common2 capability | wenay-react2 decision | Reason |
13
+ | --- | --- | --- |
14
+ | Peer gap repair / `resync()` (1.0.71) | Core-only protocol; future `usePeer` may expose status and explicit `resync` | React must not own journal or repair state. |
15
+ | Hidden-tab Media capture (1.0.72) | Adopt core defaults, no adapter | Browser worker/ImageCapture implementation belongs below React. |
16
+ | `Media.attachVideoCanvas` / audio player / publish pipe (1.0.73) | Recipe and QA consumer first; thin ref-lifecycle hooks only if repeated | Frame decode/playback must bypass React renders. |
17
+
18
+ ## Follow-up
19
+
20
+ - Keep the existing `useMediaSource`, `usePeer` and WebRTC recipe items in target.
21
+ - Add a real Media/Peer QA consumer before introducing any new hook.
22
+ - No raw route-coordinator hook: Peer mirrors already survive route changes.
23
+ ## Implementation batch
24
+
25
+ - Added `useMediaSource`: React owns source start/stop/device lifecycle only; frames remain on the common2 Listen path.
26
+ - Added `usePeer`: React exposes an SDK peer's mirrored store, low-frequency route state and explicit controls; it owns no repair or transport state.
27
+ - QA cards 38/39/40 cover camera canvas viewer, microphone player and an in-process Peer host/mirror/resync scenario.
28
+ - Verification: qa-check TypeScript, Jest 22 suites / 71 tests, package build, diff-check and live stand HTTP 200.
29
+ - Manual Verify: run cards 38, 39, 40 together with existing replay cards 23 and 34 before release.
30
+ - Manual verification completed by the user on the stand; release candidate is 1.0.45.
package/doc/target/my.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  Правила ведения: [README.md](README.md). Ниже — статус по задачам; **исходные надиктовки сохранены дословно в конце файла** (ничего не удаляю).
4
4
 
5
- ## In Progress
5
+ ## In Progress
6
+
7
+ _Пусто._
8
+
6
9
 
7
- _Пусто._ «Hook extraction — do-now кандидаты (3)» выполнен 2026-07-09: `useCacheMapPersistence` (cache.ts), `useResizeObserver`/`useElementSize` (MyResizeObserver.tsx), `useLogsPageTable` (logs.tsx); старые имена — compatibility wrappers, card 25 не тронута. Проверено: tsc qa-check, jest 45/45, build, стенд 3010 (карточки 21 F5-persistence / 19 resize / 9 logs). Durable-запись: `doc/changes/v1.0.41.md`; parked/отклонённые кандидаты остаются в `doc/progress/hook-extraction-audit.md`.
10
+ «Hook extraction — do-now кандидаты (3)» выполнен 2026-07-09: `useCacheMapPersistence` (cache.ts), `useResizeObserver`/`useElementSize` (MyResizeObserver.tsx), `useLogsPageTable` (logs.tsx); старые имена — compatibility wrappers, card 25 не тронута. Проверено: tsc qa-check, jest 45/45, build, стенд 3010 (карточки 21 F5-persistence / 19 resize / 9 logs). Durable-запись: `doc/changes/v1.0.41.md`; parked/отклонённые кандидаты остаются в `doc/progress/hook-extraction-audit.md`.
8
11
 
9
12
  (Предыдущий проход «Нормализация внутренних слоёв библиотеки» проверен multi-agent-верификацией и отгружен в v1.0.40/v1.0.41; durable-запись: `doc/progress/public-surface-normalization.md`, `doc/progress/hook-controller-opportunities.md`, `doc/changes/v1.0.38–v1.0.41.md`.)
10
13
 
@@ -16,12 +19,13 @@ _Пусто._ «Hook extraction — do-now кандидаты (3)» выполн
16
19
  - Оценка: эффективность high (самый дешёвый рост юзабельности), простота easy/medium (докосновно + один реэкспорт-модуль), риск low (аддитивно, публичный API не меняется).
17
20
  - Связка: breaking-версия (A3 + снос deprecated `onCLickClose`/`keySave`/`DragArea`/`menuR`, сужение `ListenApi`) идёт ПОСЛЕ витрины — витрина и есть список того, что сносим.
18
21
 
19
- - **common2 1.0.66–1.0.68 adoption: Media + route coordinator/WebRTC React surface** (из надиктовки №4 «посмотри, что нового, создай цели использовать»).
22
+ - **common2 1.0.66–1.0.73 adoption: Media + Peer/route React surface** (из надиктовки №4 «посмотри, что нового, создай цели использовать»).
20
23
  - Контекст: 1.0.66 = `Media` (захват мик/камера как бинарные Listen/replay-источники, `decodeMediaFrame`), 1.0.67 = `Replay.createRouteCoordinator` (policy-gated relay↔direct, link.subscribe без switchRoute), 1.0.68 = WebRTC direct-путь (`createSignalHub` по существующему RPC-сокету, `createWebRtcConnector` с инжектируемой `rtc`-фабрикой — инжекция `() => new RTCPeerConnection(cfg)` живёт на браузерной/React-стороне, `acceptWebRtcDirect`, `serveReplayChannel`). Вместе: media → replay line → policy-маршрутизация relay/direct.
21
- - **1. `useMediaSource` (capture-control hook)** пересмотр прежнего решения «Media-хуков намеренно нет»: consumption по-прежнему через существующие `useReplay*`-хуки, но capture `control` (`start()` → `'idle'|'requesting'|'live'|'denied'|'no-device'|'error'`, `stop()`, `setDevice`, `listDevices`, stats) это permission/device/lifecycle state machine, hook-shaped. Хук: state как React state, `listen` наружу как есть, stop() на unmount. QA card: камера `replay:true` линия → canvas через `useReplaySubscribe`+`decodeMediaFrame`; deny/no-device видимы на стенде.
22
- - **2. Peer SDK adapter (supersedes the raw coordinator-link idea, common2 1.0.69)** — React surface over `Peer.createPeerClient`: `usePeer(client, account)` -> peer's mirrored store (plugs into existing `useStoreNode`/`useStoreKeys`) + route state/controls (`promoteDirect` result-object, `reinterposeRelay`, `route()`, `ready`, `seq()`). Do NOT bend existing route hooks; mirrors survive route changes by design. QA card modeled on oracle `replay/peer-sdk.test.ts` (fake in-proc RTC runtime); common2 1.0.70 ships the oracles + `demo/` stand in the package as living examples to crib from.
24
+ - **1. `useMediaSource` — DONE in 1.0.45.** Capture lifecycle only: state, start/stop, device selection and stats; frame consumption stays on Listen/replay. QA cards 38/39 use core canvas/audio viewers.
25
+ - **2. Peer SDK adapter DONE in 1.0.45.** `usePeer(client, account)` exposes the mirrored store, route/status and explicit controls including resync, without taking over protocol repair or transport. QA card 40 covers an in-process host/mirror/resync scenario.
23
26
  - **3. WebRTC recipe (docs/example, not a hook)** — how the app injects the `rtc` factory (`() => new RTCPeerConnection(cfg)`) and wires the signal port from `createRpcServerAuto`; combine with a Media source (call/stream demo) over Peer. Real-browser ICE serialization is fixed in 1.0.69 (`toJSON()` on the wire), so a live two-tab demo is feasible — see common2 `demo/` shared-cursors stand.
24
- - Оценка: 1 — simple/low-risk (аддитивно); 2 — medium (in-proc fake стенд, по образцу peer-sdk oracle); 3 — docs-first. Приоритет: после architecture-fix очереди A1-A10.
27
+ - Оценка: 1 — simple/low-risk (аддитивно); 2 — medium (in-proc fake стенд, по образцу peer-sdk oracle); 3 — docs-first. Приоритет: после architecture-fix очереди A1-A10.
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 повторяется.
25
29
 
26
30
  - **Context menu / Menu: controller-first split**.
27
31
  - Прогресс/оценка: [../progress/hook-controller-opportunities.md](../progress/hook-controller-opportunities.md)
@@ -42,10 +46,36 @@ _Пусто._ «Hook extraction — do-now кандидаты (3)» выполн
42
46
  - **A10 remaining (breaking only)**: raw `ListenApi` (emit/close) exposed from columnState/Toolbar onChange (narrowing is breaking); removal of deprecated `onCLickClose`/`keySave` — breaking pass only.
43
47
  - Rule: each fix is its own focused pass with verify (tsc+jest+build+stand); breaking removals only in a deliberate breaking version.
44
48
 
49
+ - **Architecture follow-up: ownership and lifecycle boundaries (overlay / grid / columnState)** (code review 2026-07-10).
50
+ - **Overlay stack (high)**: `Overlay` instances independently subscribe to `document`. A `SettingsDialog` above a `ModalProvider` lets the lower modal handle the same Escape/outside interaction; this breaks SettingsDialog's two-stage Escape contract. Introduce an internal stack/arbiter so only the topmost dismissible layer receives Escape and outside-click; verify nested ModalProvider + SettingsDialog (search clear on first Escape, dialog close on second, parent remains open).
51
+ - **ag-grid row identity (high)**: `AgGridTable` currently lets a caller supply `getRowId` alongside `controller`, while the controller buffer owns another `getId`. Make the ownership unambiguous (reject/ignore override or explicitly derive the core identity); verify update/remove/reconnect with a controller and a conflicting callback.
52
+ - **ColumnGrid attachment ownership (high)**: one controller keeps one mutable `gridApi`, but its `Table` surface permits multiple mounts; an old table's destroy can detach the newer one. Either enforce one table per controller with a clear error or scope attachment state to a Table instance; verify two mounts plus either unmount order.
53
+ - **ColumnState contract consistency (medium)**: `groups` affects `visibleKeys()`/cards but is ignored when applying state to desktop ag-grid; `ColumnGrid.View` also forwards index `0` to a supplied `getId`. Align the shared config on both views and pass the actual row index. Add integration coverage.
54
+ - **Dispose lifecycle (medium)**: `createColumnGrid.dispose()` releases local listeners but not its attached grid listeners. Define dispose as a full release and make it detach the active grid; verify no grid events mutate state after dispose.
55
+ - Приоритет: отдельные focused passes; не смешивать с feature-задачами ColumnDots. Проверка каждого: tsc qa-check, jest, build и соответствующий lifecycle/integration test.
56
+
57
+ - **QA-стенд: расширить интеграционное покрытие и сделать его картой публичных примитивов**.
58
+ - Проблема: стенд уже содержит полезные локальные карточки (modal 13, Settings 20, columnState 28–32, replay 23–26), но не покрывает опасные межслойные сценарии: вложенные overlay, ownership `GridApi`, `dispose`, повторный mount и смену entrypoint/платформенного слоя. Зелёный Jest не заменяет эти проверки.
59
+ - Задача: 1) составить компактную матрицу «публичный primitive → unit → QA card → ручной сценарий» и пометить пробелы; 2) добавить active QA-карточки только для interaction/lifecycle сценариев, которые нельзя надёжно доказать unit-тестом: overlay stack (Modal + Settings), ColumnGrid mount/unmount/ownership, persistence reload/reset, replay reconnect/route switch; 3) дать каждой карточке действия, ожидаемый результат, reset/изоляцию ключей и номер без дублей; 4) завести правило: новая визуальная либо lifecycle-поверхность не считается готовой без unit/integration проверки и обновлённой или намеренно неприменимой QA-карточки.
60
+ - Не превращать стенд в копию всех компонентов: чистые функции и детерминированные controller-ветки остаются в Jest; стенд — для пользовательского взаимодействия, DOM, аг-grid, browser lifecycle и визуальной приёмки.
61
+ - Acceptance: карточки запускаются на чистой странице без console errors; ключевые сценарии воспроизводимы вручную; карта покрытия хранится в `doc/`; команды tsc qa-check, jest и build проходят.
62
+ - Приоритет: после исправления overlay/grid ownership — новые карточки должны проверять именно исправленные контракты.
63
+
45
64
  - **Hook extraction — parked кандидаты** (do-now 1-3 выполнены и отгружены, см. In Progress выше / `doc/changes/v1.0.41.md`). Остались условные: `useChartCanvas` (ждёт ChartDemo в карточке/тесте), `useContextMenuGesture` (ждёт оживления menuR или live touch-consumer), `columnState.api.reorderPreview` (ждёт общего reorder-контракта), `useResizeableFit` (маленький shared bind callback-ref, отдельный микро-pass). Полные причины/условия возврата: [../progress/hook-extraction-audit.md](../progress/hook-extraction-audit.md).
46
65
 
47
66
  ## Inbox
48
67
 
68
+ - **ColumnDots: фильтр-ползунок по значениям + мультисорт-опция + именованные пресеты** (надиктовка 2026-07-10, дословно в конце файла; контекст — мобильный ползунок таблицы, карточки 29/32).
69
+ - **1. Сорт-кнопка**: сейчас одна (asc→desc→off) — оставить одиночной по умолчанию, но добавить ОПЦИЮ множественной сортировки (multi-sort) настройкой.
70
+ - **2. Второй ползунок — фильтр по значениям** (открывается по кнопке при необходимости, кнопок вызова две — уточнить у пользователя что вторая): такой же трек с ячейками/метками, на котором пользователь СТАВИТ ТОЧКИ:
71
+ - числовые значения: значения раскладываются по треку; две точки = диапазон (считаем всегда по КРАЙНИМ точкам — лишние точки внутри не меняют диапазон); крайняя правая + середина = правый диапазон;
72
+ - категориальные значения (например, 9 типов): те же точки по меткам типов; выбор = диапазон ЛИБО конкретные точки;
73
+ - **режим фильтра** переключается кнопкой (как сорт-кнопка): «левее точек / между точками / только по точкам / …» — т.е. lt/range/in-set и т.п.;
74
+ - главный сценарий — мобила, всё пальцем (продолжение live-драга точек из v1.0.43).
75
+ - **3. Именованные пресеты**: кнопки «Сохранить» (с именем) и «Сбросить»; загрузка = выбор из сохранённых по имени. ОТКРЫТЫЙ ВОПРОС (пользователь сам не решил): куда поместить кнопки на телефоне — места на треке не хватает; кандидаты — в шапку дотс-панели / в меню по long-press / отдельная строка-«шторка». Персист — обычный путь memoryGetOrCreate→memoryCache (пресеты = отдельный ключ со словарём имя→конфиг фильтра+сорта).
76
+ - Смежное: фильтр должен лечь в columnState-конфиг (`filter` там уже есть — формат согласовать с ag-grid setFilterModel), чтобы desktop-грид получал тот же фильтр через grid.attach.
77
+ - Статус: надиктовка записана, скоупинг и уточнения (вторая кнопка вызова; полный список режимов) — перед взятием в работу.
78
+
49
79
  - **React Native / мобильная версия функционала** (надиктовка 2026-07-10, дословно в конце файла).
50
80
  - Суть: библиотеку планируют использовать и в мобильном (React Native), где DOM недоступен (нет `document`/`window`/`createPortal`/CSS-классов).
51
81
  - Первичная оценка: слоистость уже движется в нужную сторону — headless/controller-слой (updateBy/renderBy, columnState config, createToolbar config+Settings-модель, callbackHub, observableMap/memoryCache, replay-хуки) в основном DOM-free и потенциально переносим; DOM живёт во view-слое (FloatingWindow/Rnd, порталы/скримы, CSS-токены, ag-grid). Направление: (1) аудит `document`/`window`/`createPortal`/CSS-зависимостей по модулям; (2) выделить/закрепить platform-neutral core (возможно отдельный entrypoint без DOM-импортов); (3) RN-view-адаптеры — отдельный пакет/подпапка, не в этом проходе. ag-grid в RN не живёт — мобильные представления уже есть (ColumnDots/CardList) как модель. Персист: storage-слой абстрагировать (localStorage → AsyncStorage).
@@ -53,7 +83,9 @@ _Пусто._ «Hook extraction — do-now кандидаты (3)» выполн
53
83
 
54
84
  ## Verify
55
85
 
56
- _Пусто._ «Система стилей и CSS variables» проверена: `doneInCode=fully`, `.wenayColsMenu*`/`--cols-menu-*` и `--cols-dots-*`/`--cols-card-*` в коде, changelog v1.0.38/v1.0.39/v1.0.41, отгружено в релизах. Удалено; durable-запись `doc/progress/style-system-normalization.md`.
86
+ _Пусто._ «ColumnState present-gate» выпущен как v1.0.44: repro сначала подтвердил утечку `visible.b=false` после чужого resize под закрытым gate; фикс читает visibility только для ключей, проходящих gate; targeted Jest 3/3, полный Jest 64/64, tsc и build прошли; пакет опубликован. Durable-запись: `doc/changes/v1.0.44.md`; рабочая история: `doc/progress/column-state-present-gate.md`.
87
+
88
+ «Система стилей и CSS variables» проверена: `doneInCode=fully`, `.wenayColsMenu*`/`--cols-menu-*` и `--cols-dots-*`/`--cols-card-*` в коде, changelog v1.0.38/v1.0.39/v1.0.41, отгружено в релизах. Удалено; durable-запись — `doc/progress/style-system-normalization.md`.
57
89
 
58
90
  ## Blocked — уровень приложения (не эта React-библиотека)
59
91
 
@@ -79,3 +111,5 @@ _Пусто._ «Система стилей и CSS variables» проверен
79
111
  > да мы еще планиурем использовать библиотеку и для мобильного использования , а там дум обьекты вроде не потдерживаются.... ну ты сам знаешь что надо для реакт нативу - может сделать мобильную версию этого функционала
80
112
 
81
113
  > вопрос насоклько этот код вобще лакнчиный и юзабельный?ты бы сам ег остал использовать [оценка 2026-07-10: вызовы лаконичны, изучение — нет; задача «API-витрина» в Ready] да оформи задачу в my.md но текущий релиз запуш
114
+
115
+ > Запиши такую задачу. Значит, надо М-м-м, где менюшка? Ст-- для таблицы есть, особенно вот этот ползунок. Надо его улучшить. А именно: там вот есть фильтр по возрастанию, по убыванию, кнопочка одна. Во-первых, он должен быть множественный. Либо не множественный. Пока что пусть он будет нормальный. С-- но опционально такая настройка должна быть. А-а-а, второй момент важен. Есть ли помимооо фильтра у нас открывается второй ползунок? Если нужно. Там получается две кнопки вызвать ползунок второй. Второй ползунок, оннн так же, как и первый, состоит из каких-то ячеек иии ползунков . Ну, вот этих вот. То есть, допустим, мы там можем задать Кремль, край? Естественно, если мы выбрали «Больше» и выбираем тут-- там на ползунке есть эти цифры, то они, цифры, расположены, ну, вся эта куча, чтобы человек мог выбрать Там в левую часть либо в правую часть. То есть технический человек ставит сразу две точки? А-а-а, ты поставил крайние, я посередине. Это означает, что, ну, был этот диапазон. Крайний правый посередине означает, что был правый диапазон. Если он добавляет еще точки, мы их всё равно считаем по крайним точкам. Нам неважно, если этооо Если этооо, к-к-какой-нибудь, например, тверд-- ну, либо какой-нибудь другой вариант еще рассматриваем. Вот. А если этооо, к примеру Там название чего-то, и этих названий определенное количество. Допустим Девять типов чего-то. То мыыы-- э-э-э, вот у нас есть девять типов. Мы точно так же точки расставляем. Человек может выбрать несколько точек: либо диапазон, автооо, типа, либо конкретно. И режим выбирается, к примеру, вот этой кнопкой: фильтровать вверх, вниз. То есть мы выбираем левее, точки, между точками, только по точкам, там меньше точек. Ну, чё-то типа такого. Вот, я думаю, это будет очень удобно. Особенно на мобиле. Иии нужна настройка ещёёё Сохранить этот настрой. Ну, типа, вот, н-- допустим, так сделали . Понятно, кнопка «Сбросить» их вот, и кнопка «Сохранить». Только пока не очень понимаю, куда это поставить. Особенно на мобиле, на телефоне там места не хватает, ну, точнее, не хватает. Типа ты, допустим, настроил настройку . Кнопку нажал, их там запримел. Иии, соответственно, точно так же, еслиии тыыы Как загружать правильно? Загружать на опять же, смотреть, чё ты сохраняешься. Сохраняешься с именем?
@@ -472,9 +472,9 @@ 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. A dedicated `usePeer` adapter is a target-backlog item, not yet a library surface.
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.
476
476
 
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. 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.
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
 
479
479
  Contract: `off()` on unmount, StrictMode-safe; seq survives resubscribes inside one mount (keepSeq, default on) — a resubscribe reconnects with `{since}` and gets the journal tail, not a keyframe. Across a FULL unmount/remount keep the position outside via `onSeq` and pass it back as `since`. `seq()` is a getter — high-frequency lines (video frames, ticks) do not re-render per event; draw to canvas via ref, bypassing VDOM. Freshness: detection lives in wenay-common2 (`staleMs` watchdog); the non-route hooks mirror its edge-triggered `onStale` into `stale`, so a fresh 100 ev/s line causes zero extra renders. Route hand-off is explicit through `switchRoute(nextRemote, {label?, since?, reset?, policy?, hint?})`: old route stays live while the replacement catches up by `seq`, then closes. `useReplayHistory` is archive playback — staleness does not apply. QA cards 23 (video line + conflation + time travel + freshness), 24 (store sync), 25 (per-key feed), and 26 (route hand-off) are the live examples.
480
480
 
@@ -229,7 +229,11 @@ export function createColumnState(opts) {
229
229
  if (!known.has(s.colId))
230
230
  continue;
231
231
  order.push(s.colId);
232
- visible[s.colId] = !s.hide;
232
+ // `hide` for a gated-out column was written by applyToGrid(), not by
233
+ // the user. Folding it back would turn runtime presence into persisted
234
+ // visibility and keep the column hidden when the gate opens again.
235
+ if (passesPresentGate(s.colId))
236
+ visible[s.colId] = !s.hide;
233
237
  if (typeof s.width == 'number' && s.width > 0)
234
238
  width[s.colId] = s.width;
235
239
  if (s.sort == 'asc' || s.sort == 'desc')
@@ -1,7 +1,9 @@
1
+ export * from './useReplay';
2
+ export * from './useObserveStore';
3
+ export * from './useOutside';
1
4
  export * from './useKeyboard';
2
5
  export * from './useDraggable';
3
6
  export * from './useReorder';
4
7
  export * from './useReorderBoard';
5
- export * from './useObserveStore';
6
- export * from './useReplay';
7
- export * from './useOutside';
8
+ export * from './useMedia';
9
+ export * from './usePeer';
@@ -1,7 +1,9 @@
1
+ export * from './useReplay';
2
+ export * from './useObserveStore';
3
+ export * from './useOutside';
1
4
  export * from './useKeyboard';
2
5
  export * from './useDraggable';
3
6
  export * from './useReorder';
4
7
  export * from './useReorderBoard';
5
- export * from './useObserveStore';
6
- export * from './useReplay';
7
- export * from './useOutside';
8
+ export * from './useMedia';
9
+ export * from './usePeer';
@@ -0,0 +1,14 @@
1
+ import { Media } from "wenay-common2";
2
+ export type UseMediaSourceController = {
3
+ source: Media.MediaSource;
4
+ listen: Media.MediaAnyListen;
5
+ state: Media.MediaSourceState;
6
+ start(): Promise<Media.MediaSourceState>;
7
+ stop(): void;
8
+ setDevice(id: string): Promise<Media.MediaSourceState>;
9
+ listDevices(): Promise<Media.MediaSourceDevice[]>;
10
+ stats(): Media.MediaStats;
11
+ };
12
+ /** React lifecycle adapter for a common2 Media source. Frames stay on `listen`:
13
+ * draw/play them through a canvas or AudioContext, never through React state. */
14
+ export declare function useMediaSource(kind: "audio" | "video", options?: Media.AudioSourceOpts | Media.VideoSourceOpts): UseMediaSourceController;
@@ -0,0 +1,39 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { Media } from "wenay-common2";
3
+ /** React lifecycle adapter for a common2 Media source. Frames stay on `listen`:
4
+ * draw/play them through a canvas or AudioContext, never through React state. */
5
+ export function useMediaSource(kind, options = {}) {
6
+ const sourceRef = useRef(null);
7
+ if (!sourceRef.current)
8
+ sourceRef.current = kind == "video"
9
+ ? Media.createVideoSource(options)
10
+ : Media.createAudioSource(options);
11
+ const source = sourceRef.current;
12
+ const [state, setState] = useState(source.state);
13
+ const sync = useCallback(() => setState(source.state), [source]);
14
+ const start = useCallback(async () => {
15
+ const next = await source.start();
16
+ setState(next);
17
+ return next;
18
+ }, [source]);
19
+ const stop = useCallback(() => {
20
+ source.stop();
21
+ sync();
22
+ }, [source, sync]);
23
+ const setDevice = useCallback(async (id) => {
24
+ const next = await source.setDevice(id);
25
+ setState(next);
26
+ return next;
27
+ }, [source]);
28
+ useEffect(() => () => source.stop(), [source]);
29
+ return useMemo(() => ({
30
+ source,
31
+ listen: source[1],
32
+ state,
33
+ start,
34
+ stop,
35
+ setDevice,
36
+ listDevices: () => source.listDevices(),
37
+ stats: () => source.getStats(),
38
+ }), [source, state, start, stop, setDevice]);
39
+ }
@@ -0,0 +1,16 @@
1
+ import { Peer } from "wenay-common2";
2
+ /** Thin React view over common2 Peer SDK. The SDK keeps route/repair ownership;
3
+ * consumers use `store` with useStoreNode/useStoreKeys as usual. */
4
+ export declare function usePeer<T extends object>(client: Peer.PeerClient<T>, account: string): {
5
+ store: import("wenay-common2/lib/Common/Observe").Store<T>;
6
+ ready: boolean;
7
+ route: string;
8
+ state: import("wenay-common2/lib/Common/events/route-coordinator").tRouteState;
9
+ seq: () => number;
10
+ promoteDirect: (opts?: import("wenay-common2/lib/Common/events/route-coordinator").PromoteDirectOpts) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
11
+ reinterposeRelay: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
12
+ fallback: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
13
+ block: (reason?: unknown) => Promise<import("wenay-common2/lib/Common/events/route-coordinator").RouteOpResult>;
14
+ resync: () => Promise<void>;
15
+ close: () => void;
16
+ };
@@ -0,0 +1,29 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+ /** Thin React view over common2 Peer SDK. The SDK keeps route/repair ownership;
3
+ * consumers use `store` with useStoreNode/useStoreKeys as usual. */
4
+ export function usePeer(client, account) {
5
+ const peer = useMemo(() => client.peer(account), [client, account]);
6
+ const [epoch, setEpoch] = useState(0);
7
+ const [ready, setReady] = useState(false);
8
+ useEffect(() => {
9
+ let alive = true;
10
+ setReady(false);
11
+ peer.ready.then(() => { if (alive)
12
+ setReady(true); });
13
+ const off = client.onRoute(() => setEpoch(v => v + 1));
14
+ return () => { alive = false; off(); };
15
+ }, [client, peer]);
16
+ return useMemo(() => ({
17
+ store: peer.store,
18
+ ready,
19
+ route: peer.route(),
20
+ state: peer.state(),
21
+ seq: peer.seq,
22
+ promoteDirect: peer.promoteDirect,
23
+ reinterposeRelay: peer.reinterposeRelay,
24
+ fallback: peer.fallback,
25
+ block: peer.block,
26
+ resync: client.resync,
27
+ close: peer.close,
28
+ }), [client, peer, ready, epoch]);
29
+ }
package/package.json CHANGED
@@ -1,50 +1,50 @@
1
- {
2
- "name": "wenay-react2",
3
- "version": "1.0.43",
4
- "description": "Common react",
5
- "strict": true,
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "files": [
9
- "lib/**/*",
10
- "doc/**/*",
11
- "!**/*.tsbuildinfo"
12
- ],
13
- "author": "wenay",
14
- "license": "ISC",
15
- "peerDependenciesMeta": {
16
- "react-dom": {
17
- "optional": true
18
- }
19
- },
20
- "dependencies": {
21
- "ag-grid-community": "^35.1.0",
22
- "ag-grid-react": "^35.1.0",
23
- "re-resizable": "^6.11.2",
24
- "react": "^19.2.0",
25
- "react-dom": "^19.2.0",
26
- "react-rnd": "^10.5.3",
27
- "wenay-common2": "^1.0.70"
28
- },
29
- "peerDependencies": {
30
- "react": "^19.2.0",
31
- "react-dom": "^19.2.0"
32
- },
33
- "engines": {
34
- "node": ">=18",
35
- "vscode": "^1.22.0"
36
- },
37
- "browserslist": {
38
- "production": [
39
- ">0.2%",
40
- "not dead"
41
- ],
42
- "development": [
43
- ">1%"
44
- ]
45
- },
46
- "exports": {
47
- ".": "./lib/index.js",
48
- "./package.json": "./package.json"
49
- }
50
- }
1
+ {
2
+ "name": "wenay-react2",
3
+ "version": "1.0.45",
4
+ "description": "Common react",
5
+ "strict": true,
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "lib/**/*",
10
+ "doc/**/*",
11
+ "!**/*.tsbuildinfo"
12
+ ],
13
+ "author": "wenay",
14
+ "license": "ISC",
15
+ "peerDependenciesMeta": {
16
+ "react-dom": {
17
+ "optional": true
18
+ }
19
+ },
20
+ "dependencies": {
21
+ "ag-grid-community": "^35.1.0",
22
+ "ag-grid-react": "^35.1.0",
23
+ "re-resizable": "^6.11.2",
24
+ "react": "^19.2.0",
25
+ "react-dom": "^19.2.0",
26
+ "react-rnd": "^10.5.3",
27
+ "wenay-common2": "^1.0.73"
28
+ },
29
+ "peerDependencies": {
30
+ "react": "^19.2.0",
31
+ "react-dom": "^19.2.0"
32
+ },
33
+ "engines": {
34
+ "node": ">=18",
35
+ "vscode": "^1.22.0"
36
+ },
37
+ "browserslist": {
38
+ "production": [
39
+ ">0.2%",
40
+ "not dead"
41
+ ],
42
+ "development": [
43
+ ">1%"
44
+ ]
45
+ },
46
+ "exports": {
47
+ ".": "./lib/index.js",
48
+ "./package.json": "./package.json"
49
+ }
50
+ }