@dimina-kit/electron-deck 0.1.0-dev.20260702175315 → 0.1.0-dev.20260703051110

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 (63) hide show
  1. package/README.md +45 -15
  2. package/dist/client/index.d.ts +8 -2
  3. package/dist/client/index.d.ts.map +1 -1
  4. package/dist/client/index.js +1 -0
  5. package/dist/client/index.js.map +1 -1
  6. package/dist/dock-react/dock-view.d.ts +43 -19
  7. package/dist/dock-react/dock-view.d.ts.map +1 -1
  8. package/dist/dock-react/drag-redock.d.ts +18 -0
  9. package/dist/dock-react/drag-redock.d.ts.map +1 -1
  10. package/dist/dock-react/group-view.d.ts +18 -0
  11. package/dist/dock-react/group-view.d.ts.map +1 -0
  12. package/dist/dock-react/index.d.ts +3 -2
  13. package/dist/dock-react/index.d.ts.map +1 -1
  14. package/dist/dock-react/index.js +384 -289
  15. package/dist/dock-react/index.js.map +1 -1
  16. package/dist/dock-react/panel-body.d.ts +24 -0
  17. package/dist/dock-react/panel-body.d.ts.map +1 -0
  18. package/dist/dock-react/split-sizing.d.ts +109 -0
  19. package/dist/dock-react/split-sizing.d.ts.map +1 -0
  20. package/dist/dock-react/split-view.d.ts +15 -0
  21. package/dist/dock-react/split-view.d.ts.map +1 -0
  22. package/dist/{electron-deck-B9fun93z.js → electron-deck-Bi-u-Aze.js} +175 -79
  23. package/dist/electron-deck-Bi-u-Aze.js.map +1 -0
  24. package/dist/electron-deck.d.ts +5 -0
  25. package/dist/electron-deck.d.ts.map +1 -1
  26. package/dist/events.d.ts +1 -0
  27. package/dist/events.d.ts.map +1 -1
  28. package/dist/host/index.d.ts +5 -0
  29. package/dist/host/index.d.ts.map +1 -1
  30. package/dist/host/index.js +1 -1
  31. package/dist/index.js +1 -1
  32. package/dist/internal/deck-app-view-move.d.ts +17 -0
  33. package/dist/internal/deck-app-view-move.d.ts.map +1 -0
  34. package/dist/internal/deck-app.d.ts +58 -1
  35. package/dist/internal/deck-app.d.ts.map +1 -1
  36. package/dist/layout/index.d.ts +1 -0
  37. package/dist/layout/index.d.ts.map +1 -1
  38. package/dist/layout/index.js +2 -2
  39. package/dist/layout/model.d.ts.map +1 -1
  40. package/dist/layout/mutations.d.ts.map +1 -1
  41. package/dist/layout/tree-query.d.ts +18 -0
  42. package/dist/layout/tree-query.d.ts.map +1 -0
  43. package/dist/layout/types.d.ts +11 -9
  44. package/dist/layout/types.d.ts.map +1 -1
  45. package/dist/{layout-ByOiOdnc.js → layout-D-LYJIpM.js} +57 -41
  46. package/dist/layout-D-LYJIpM.js.map +1 -0
  47. package/dist/main/compositor.d.ts.map +1 -1
  48. package/dist/main/index.js +1 -1
  49. package/dist/main/view-handle.d.ts.map +1 -1
  50. package/dist/preload/index.cjs +5 -0
  51. package/dist/preload/index.cjs.map +1 -1
  52. package/dist/preload/index.d.ts +5 -0
  53. package/dist/preload/index.d.ts.map +1 -1
  54. package/dist/preload/index.js +5 -0
  55. package/dist/preload/index.js.map +1 -1
  56. package/dist/types.d.ts +32 -10
  57. package/dist/types.d.ts.map +1 -1
  58. package/dist/{view-handle-DDhpBuW-.js → view-handle-zav25k3-.js} +141 -86
  59. package/dist/view-handle-zav25k3-.js.map +1 -0
  60. package/package.json +11 -5
  61. package/dist/electron-deck-B9fun93z.js.map +0 -1
  62. package/dist/layout-ByOiOdnc.js.map +0 -1
  63. package/dist/view-handle-DDhpBuW-.js.map +0 -1
package/README.md CHANGED
@@ -2,38 +2,68 @@
2
2
 
3
3
  领域中立的 Electron host-shell 框架。它把跨窗口编排、原生 `WebContentsView` 的 z 叠放与几何跟随、嵌套寿命管理抽成一组正交原语,让任意 Electron 多窗口应用用极少的 host 代码拼出「窗口 + 原生 view + 浮层 + popout」。同时提供 host-shell 的跨进程 transport(声明式 + typed 双向 IPC)、信任边界、确定性资源生命周期,以及 webview 侧的 preload / client。
4
4
 
5
- ## 入口
5
+ 本包有两个**生产消费面**:
6
6
 
7
- 框架入口 `electronDeck(config, options?)` `startElectronDeck(...)` 都从本包根导出。host 写一份 `DeckConfig`,framework 接管 Electron 装配、IPC、生命周期:
7
+ 1. **`backend` 装配**(下面「入口」一节)——领域方实现 `RuntimeBackend`,把 `electronDeck({ backend })` 当唯一入口,自己建窗口、自己接线 IPC。`@dimina-kit/devtools` 就是这样接入的。
8
+ 2. **`/layout` + `/dock-react`**(下面「浏览器消费面」一节)——纯窗口内 docking 布局引擎,Electron devtools 和纯浏览器 web 项目都在用。
9
+
10
+ 框架自身的声明式装配面(`startElectronDeck` 顶层配置 `hostServices` / `simulatorApis` / `events` / `toolbar`,`runtime.windows` / `runtime.view` 等 host-shell 高层 API)目前**没有生产消费者**,见文末「实验性面」一节。
11
+
12
+ ## 入口:`backend` 装配(生产路径)
13
+
14
+ 领域方实现一份 `RuntimeBackend`(`assemble(runtime)` 建窗口、接域内 IPC;`ownsWindows: true` 让框架把主窗口装配完全让给 backend),把它交给 `electronDeck`:
8
15
 
9
16
  ```ts
10
17
  // main.ts
11
- import { startElectronDeck, defineEvent } from '@dimina-kit/electron-deck'
18
+ import { electronDeck } from '@dimina-kit/electron-deck'
19
+ import { myBackend } from './my-backend.js'
12
20
 
13
- const authChanged = defineEvent<{ user: { id: string } | null }>('authChanged')
14
-
15
- startElectronDeck({
16
- app: { name: 'My DevTools' },
17
- hostServices: { getUser: async () => ({ user: null }) },
18
- events: [authChanged],
21
+ electronDeck({ backend: myBackend }).catch((err) => {
22
+ console.error(err)
23
+ process.exit(1)
19
24
  })
20
25
  ```
21
26
 
22
- `startElectronDeck()` 内部已对 `app.whenReady()` gating,可在 main 模块顶层直接调用。`electronDeck()` 是其 await 版本——若直接用它,不要在 main 模块顶层 `await`(electron main 模块求值完成前不触发 `whenReady`,顶层 await 会死锁),改用 `startElectronDeck()` 或 `electronDeck(...).catch(...)`。
27
+ 框架接管 process 生命周期 gate(`app.whenReady()`)、wire transport、信任边界;backend 只负责领域装配(真实 context、主窗口内容、projects/simulator/views、IPC 模块)。`@dimina-kit/devtools` 的 `launch(config)` 就是「预置 devtools backend + `electronDeck({ backend })`」的薄封装——集成 devtools 时直接用它,见 [`../devtools/docs/workbench-model.md`](../devtools/docs/workbench-model.md)
28
+
29
+ `electronDeck()` 不要在 main 模块顶层 `await`(electron 在 main 模块求值完成前不触发 `whenReady`,顶层 await 会死锁)——用 `.catch(...)` 收尾,或改用 `startElectronDeck()`(见「实验性面」一节,内部已对 `whenReady` 做 gating,可在顶层直接调用)。
30
+
31
+ ## 浏览器消费面:`/layout` + `/dock-react`
23
32
 
24
- `@dimina-kit/devtools` 的 `launch(config)` 是预注入 devtools backend 的薄封装——集成 devtools 时用它,见 [`../devtools/docs/workbench-model.md`](../devtools/docs/workbench-model.md)。
33
+ `@dimina-kit/electron-deck/layout`(纯 TS layout-as-data 引擎:`SplitNode`/`TabGroupNode` 树、`movePanel`/`splitPanel`/`closePanel`/`insertPanel`/`setActive`/`setSizes`/`setConstraint` mutation、`serializeLayout`/`parseLayout`/`validateTree`、`createLayoutModel` 单写者可观察模型、panel registry)和 `@dimina-kit/electron-deck/dock-react`(`<DockView>` React 渲染器)是**双端消费面**:Electron devtools(IDE-dockable 布局)和纯浏览器的 web 项目都直接消费。
34
+
35
+ 因此这两个子路径(及其传递依赖)**禁止引入 `electron` / `node` 依赖**——`/layout` 连 `react` 都不许 import(`src/layout/boundary.test.ts` 钉死这条边界),`/dock-react` 的运行时依赖仅限 `react` / `react-dom` / `react-resizable-panels`(`src/dock-react/boundary.test.ts` 钉死)。split 子节点可带 `SizeConstraint`:`fixedPx` 锁死到 N px;`minPx` 同样是 px-sized(有最小像素下限、不参与弹性权重池),但用户仍可把它拖宽。descriptor 可带 `PanelCapabilities`:`draggable`/`dropPolicy`/`closable`/`hideTab`,`<DockView>` 按它约束拖拽/关闭交互;`computeReorderIndex` 是配套的纯几何函数。
25
36
 
26
37
  ## 导出
27
38
 
28
39
  | 你要的 | 从哪导入 |
29
40
  |---|---|
30
- | `electronDeck` / `startElectronDeck` 入口、`defineEvent`、`DeckConfig` / `RuntimeBackend` 等类型 | `@dimina-kit/electron-deck` |
41
+ | `electronDeck` 入口、`DeckConfig` / `RuntimeBackend` 等类型 | `@dimina-kit/electron-deck` |
31
42
  | 主进程装配工具 | `@dimina-kit/electron-deck/main` |
32
43
  | host 侧 control-bus / capability / trust 原语 | `@dimina-kit/electron-deck/host` |
33
44
  | preload bridge `exposeDeckBridge()` | `@dimina-kit/electron-deck/preload` |
34
- | renderer client `createDeckClient<HS, EV>()` | `@dimina-kit/electron-deck/client`(浏览器构建:`/client/browser`) |
35
- | layout-as-data 引擎(`SplitNode`/`TabGroupNode` 树、`movePanel`/`splitPanel`/`closePanel`/`insertPanel`/`setActive`/`setSizes`/`setConstraint` mutation、`serializeLayout`/`parseLayout`/`validateTree`、`createLayoutModel` 单写者可观察模型、panel registry,descriptor 可带 `PanelCapabilities`:`draggable`/`dropPolicy`/`closable`/`hideTab`;split 子可带 `SizeConstraint`:`fixedPx` 锁死 / `minPx` 柔性下限) | `@dimina-kit/electron-deck/layout` |
36
- | `<DockView>` React 渲染器(把 layout 树渲染成可拖拽 re-dock / tab / 分屏的 docking UI;按 descriptor 的 `PanelCapabilities` 约束交互——`draggable:false` 锁定为不可拖且不可作落点锚,`dropPolicy:'reorder-only'` 只允许组内重排,`closable:false` 隐藏关闭控件)+ `computeReorderIndex` 纯几何 | `@dimina-kit/electron-deck/dock-react` |
45
+ | renderer client `createDeckClient<HS, EV>()` | `@dimina-kit/electron-deck/client`(`/client/browser` 是同一产物的别名) |
46
+ | layout-as-data 引擎 + panel registry(见上「浏览器消费面」) | `@dimina-kit/electron-deck/layout` |
47
+ | `<DockView>` React 渲染器 + `computeReorderIndex`(见上「浏览器消费面」) | `@dimina-kit/electron-deck/dock-react` |
48
+
49
+ ## 实验性面(无生产消费者)
50
+
51
+ 以下声明式装配教程和 host-shell 高层 API 目前只有本包自己的 `examples/` / `spike/` 在用——devtools 等下游都走上面的 `backend` 装配路径,从不触达这套面。在第二个真实消费者采纳之前,把它们当 `@experimental`:签名可能变化,未经非 demo 工作负载验证。
52
+
53
+ ```ts
54
+ // main.ts
55
+ import { startElectronDeck, defineEvent } from '@dimina-kit/electron-deck'
56
+
57
+ const authChanged = defineEvent<{ user: { id: string } | null }>('authChanged')
58
+
59
+ startElectronDeck({
60
+ app: { name: 'My DevTools' },
61
+ hostServices: { getUser: async () => ({ user: null }) },
62
+ events: [authChanged],
63
+ })
64
+ ```
65
+
66
+ `startElectronDeck()` 内部已对 `app.whenReady()` 做 gating,可在 main 模块顶层直接调用——这是它相对 `electronDeck()` 的唯一优势;装配面本身(`hostServices`/`simulatorApis`/`events`/`toolbar`、`runtime.windows`/`runtime.view`/`runtime.scopes`/`runtime.grants`/`runtime.layout` 等 host-shell 高层 API)仍是实验性的。判定「实现是否已上生产」以谁在调 `assemble`/`backend` 为准,不是这份教程。
37
67
 
38
68
  ## 文档
39
69
 
@@ -11,12 +11,18 @@ export interface CreateDeckClientOptions {
11
11
  /** 默认 `__electronDeckBridge`;必须与 host preload 对齐 */
12
12
  readonly globalName?: string;
13
13
  }
14
- export interface DeckClient<HS extends Record<keyof HS, (...args: any[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]> {
14
+ /** @experimental No production consumer yet pairs with `DeckConfig.hostServices`
15
+ * / `events`, which only `examples/` / `spike/` set; the devtools `backend`
16
+ * assembly never touches this client. `createDeckLayoutClient` and
17
+ * `createPlacementPublisher` (re-exported above) are unaffected — they have
18
+ * real consumers. */
19
+ export interface DeckClient<HS extends Record<keyof HS, (...args: never[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]> {
15
20
  ready(): Promise<void>;
16
21
  invoke<K extends keyof HS & string>(name: K, ...args: Parameters<HS[K]>): Promise<Awaited<ReturnType<HS[K]>>>;
17
22
  on<E extends EV[number]>(event: E, listener: (payload: E extends HostEvent<infer P> ? P : never) => void): Disposable;
18
23
  }
19
- export declare function createDeckClient<HS extends Record<keyof HS, (...args: any[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]>(options?: CreateDeckClientOptions): DeckClient<HS, EV>;
24
+ /** @experimental No production consumer yet see the note on {@link DeckClient}. */
25
+ export declare function createDeckClient<HS extends Record<keyof HS, (...args: never[]) => unknown>, EV extends readonly HostEvent<JsonValue>[]>(options?: CreateDeckClientOptions): DeckClient<HS, EV>;
20
26
  export declare function readBridgeFromGlobal(globalName?: string): DeckBridge | undefined;
21
27
  export { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL };
22
28
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,uBAAuB,EACvB,eAAe,EACf,MAAM,cAAc,CAAA;AACrB,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,CAAA;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACX,SAAS,EACT,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACnE,YAAY,EACX,kBAAkB,EAClB,sBAAsB,GACtB,MAAM,0BAA0B,CAAA;AAEjC,MAAM,WAAW,uBAAuB;IACvC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AASD,MAAM,WAAW,UAAU,CAE1B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,EACxD,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE;IAE1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,EACjC,IAAI,EAAE,CAAC,EACP,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EACtB,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,KAAK,IAAI,GACnE,UAAU,CAAA;CACb;AAED,wBAAgB,gBAAgB,CAE/B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,OAAO,CAAC,EACxD,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE,EACzC,OAAO,CAAC,EAAE,uBAAuB,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CA+DvD;AAED,wBAAgB,oBAAoB,CACnC,UAAU,GAAE,MAA8B,GACxC,UAAU,GAAG,SAAS,CAKxB;AASD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/client/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,uBAAuB,EACvB,eAAe,EACf,MAAM,cAAc,CAAA;AACrB,OAAO,EACN,uBAAuB,EACvB,qBAAqB,EACrB,MAAM,uBAAuB,CAAA;AAC9B,OAAO,KAAK,EAAiB,UAAU,EAAE,MAAM,uBAAuB,CAAA;AACtE,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEnE,OAAO,EAAE,uBAAuB,EAAE,eAAe,EAAE,CAAA;AAEnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAC3D,YAAY,EACX,SAAS,EACT,YAAY,EACZ,gBAAgB,GAChB,MAAM,oBAAoB,CAAA;AAE3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACnE,YAAY,EACX,kBAAkB,EAClB,sBAAsB,GACtB,MAAM,0BAA0B,CAAA;AAEjC,MAAM,WAAW,uBAAuB;IACvC,oDAAoD;IACpD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AAcD;;;;sBAIsB;AACtB,MAAM,WAAW,UAAU,CAC1B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,EAC1D,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE;IAE1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,MAAM,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,MAAM,EACjC,IAAI,EAAE,CAAC,EACP,GAAG,IAAI,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GACxB,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IACtC,EAAE,CAAC,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EACtB,KAAK,EAAE,CAAC,EACR,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,KAAK,IAAI,GACnE,UAAU,CAAA;CACb;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,CAC/B,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,EAC1D,EAAE,SAAS,SAAS,SAAS,CAAC,SAAS,CAAC,EAAE,EACzC,OAAO,CAAC,EAAE,uBAAuB,GAAG,UAAU,CAAC,EAAE,EAAE,EAAE,CAAC,CA+DvD;AAED,wBAAgB,oBAAoB,CACnC,UAAU,GAAE,MAA8B,GACxC,UAAU,GAAG,SAAS,CAKxB;AASD,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,CAAA"}
@@ -334,6 +334,7 @@ function createDeckLayoutClient(deps) {
334
334
  }
335
335
  //#endregion
336
336
  //#region src/client/index.ts
337
+ /** @experimental No production consumer yet — see the note on {@link DeckClient}. */
337
338
  function createDeckClient(options) {
338
339
  const globalName = options?.globalName ?? "__electronDeckBridge";
339
340
  function getBridge() {
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../view-anchor/src/view-anchor.ts","../../src/client/placement-publisher.ts","../../src/client/layout-client.ts","../../src/client/index.ts"],"sourcesContent":["import type {\n Bounds,\n Placement,\n ViewAnchorOptions,\n ViewAnchorHandle,\n} from './types.js'\n\nconst ZERO: Bounds = { x: 0, y: 0, width: 0, height: 0 }\n\n// Round to integers (setBounds rejects fractionals). Width/height are clamped\n// to ≥0 (negative area is meaningless and `0` is the canonical \"hidden\"\n// signal). x/y are NOT clamped: a position is a position — an anchored overlay\n// scrolled past the top/left edge has a legitimately NEGATIVE origin, and\n// flooring it to 0 would pin the native view at the edge instead of letting it\n// track its element off-screen. (Each consumer's IPC schema enforces its own\n// origin policy — the simulator's allows negative; the DevTools placeholder\n// never goes negative, so its NonNegInt schema is unaffected.)\nconst clampRect = (r: {\n x: number\n y: number\n width: number\n height: number\n}): Bounds => ({\n x: Math.round(r.x),\n y: Math.round(r.y),\n width: Math.max(0, Math.round(r.width)),\n height: Math.max(0, Math.round(r.height)),\n})\n\n/**\n * Create an anchor binding ONE native view's bounds to `target`'s geometry.\n *\n * Imperative core — no React, no Electron. Behaviour:\n * - `present === true`: publish `target.getBoundingClientRect()` (x/y rounded,\n * width/height `Math.max(0, Math.round(...))`) immediately, then re-publish\n * SYNCHRONOUSLY on every `ResizeObserver` tick and window `resize`.\n * - `present === false`: publish `{0,0,0,0}` immediately; do not observe.\n * - `update(opts)`: re-apply synchronously.\n * - `dispose()`: stop observing, never publish again.\n *\n * Synchronous, NOT RAF-deferred: the native overlay is a cross-process\n * `WebContentsView` whose `setBounds` already lands ~1 compositor frame behind\n * the renderer's DOM paint (the two processes composite on different frames).\n * Deferring the measure+publish to a RAF stacked a SECOND frame on top — during\n * a height/splitter drag that read as the overlay visibly trailing the region\n * edge (worst when GROWING, where the not-yet-followed edge exposes background).\n * Publishing in the observer tick itself removes that self-inflicted frame and\n * leaves only the unavoidable cross-process frame (masked by matching the\n * placeholder/desk background colour). The anti-flood role the RAF used to play\n * — collapsing a burst of RO+resize ticks in one frame into one publish — is now\n * served by `lastPublished` dedup: a tick whose measured rect is byte-identical\n * to the last published one is dropped, so a continuous drag still emits at most\n * one publish per distinct rect.\n *\n * Teardown safety: there is no queued frame to outrun a state change — every\n * emit reads `disposed`/`present` synchronously, so a tick after\n * `update`/`dispose` can never write a stale rect over the live one.\n */\nexport function createViewAnchor(\n target: HTMLElement,\n opts: ViewAnchorOptions,\n): ViewAnchorHandle {\n let present = opts.present\n let publish = opts.publish\n let observer: ResizeObserver | null = null\n // The last rect handed to `publish`, for dedup-coalescing (see header). Reset\n // to `null` on every `apply()` so a state change (e.g. zoom, which rides in\n // the `publish` closure, not in `Bounds`) always forces one fresh publish\n // even when the geometry is unchanged.\n let lastPublished: Bounds | null = null\n let disposed = false\n\n const measure = (): Bounds => {\n const r = target.getBoundingClientRect()\n return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height })\n }\n\n const sameRect = (a: Bounds, b: Bounds): boolean =>\n a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height\n\n // Measure + publish SYNCHRONOUSLY on the triggering tick — no RAF defer (see\n // header for why). Bail if torn down or detached, and dedup a rect\n // byte-identical to the last published one (collapses a same-frame RO+resize\n // burst, and a steady drag that re-fires the same final rect, into one).\n const emit = (): void => {\n if (disposed || !present) return\n const m = measure()\n if (lastPublished && sameRect(lastPublished, m)) return\n lastPublished = m\n publish(m)\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n window.removeEventListener('resize', emit)\n }\n\n // Apply the current (present, publish) synchronously. Reset `lastPublished`\n // first so the publish below is never dedup-skipped — a state change (zoom,\n // present flip, new publish target) must always re-emit even if the geometry\n // is byte-identical to the previous emit.\n const apply = (): void => {\n lastPublished = null\n if (present) {\n startObserving()\n lastPublished = measure()\n publish(lastPublished)\n } else {\n stopObserving()\n publish(ZERO)\n }\n }\n\n apply()\n\n return {\n update(next: ViewAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n present = next.present\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n }\n}\n\n// ── Explicit Placement API ────────────────────────────────────────────\n//\n// The modern surface that replaces the magic-`{0,0,0,0}` \"hidden\" value.\n// Visibility is an explicit discriminant, NEVER inferred from geometry — a\n// real 0×0-but-on-screen view is `{ visible:true, bounds:{...,width:0} }`\n// and a hidden one is `{ visible:false }` (no `bounds`). See `Placement`.\n\nexport interface PlacementAnchorOptions {\n /**\n * Caller's INTENT: should the native view be on-screen? `true` →\n * publish the measured rect as `{ visible:true, bounds }`; `false` →\n * publish `{ visible:false }`. Crucially, hiddenness comes from this\n * flag, not from a measured zero size — so a legitimately 0-sized but\n * visible target still publishes `visible:true`.\n */\n visible: boolean\n /** Receives each explicit Placement. Owns IPC → host. */\n publish: (placement: Placement) => void\n /**\n * Opt-in geometry detach. When true, a measured zero-area target (no\n * geometry box — display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` (detach-but-keep) instead of `{ visible:true,\n * bounds:0×0 }`, and an IntersectionObserver is attached so a display:none\n * transition (which ResizeObserver does not report) re-publishes. Default\n * false keeps the legitimate 0×0-visible semantics.\n */\n guardDisplayNone?: boolean\n /**\n * Opt-in capture-phase ancestor-scroll follow. When true, the anchor\n * listens for `scroll` on `window` in the CAPTURE phase (scroll events don't\n * bubble, but reach `window` while capturing), so an ancestor scroll\n * container scrolling the target re-measures and re-publishes. With\n * `followGeometry` off, the scroll callback does a single synchronous\n * `emit()`; with it on, the scroll OPENS the RAF sentinel window so the\n * follow tracks every frame of a scroll burst. Default false.\n */\n followScroll?: boolean\n /**\n * Opt-in windowed RAF geometry sentinel. Catches ancestor\n * transform / reflow moves that no DOM event reports. The sentinel is\n * NON-resident: it is OPENED on demand (a scroll burst, a `[role=\"separator\"]`\n * splitter pointerdown, or an explicit `pulse()`), polls geometry once per\n * animation frame publishing IN-FRAME, and AUTO-CLOSES once the rect goes\n * steady (a few unchanged frames). While closed it schedules no frame, so the\n * static cost when idle is exactly zero. Default false.\n */\n followGeometry?: boolean\n}\n\nexport interface PlacementAnchorHandle {\n /** Apply new options; re-publishes immediately (mirrors `createViewAnchor`). */\n update(opts: PlacementAnchorOptions): void\n /** Stop observing; never publish again. */\n dispose(): void\n /**\n * Open the RAF sentinel window (animation follow); auto-closes after going\n * steady or after `durationMs`. No-op when `followGeometry` is false.\n */\n pulse(durationMs?: number): void\n}\n\n/**\n * Pure measure: read `target`'s rect and wrap it as an explicit visible\n * Placement. Always `{ visible:true }` — hiddenness is a caller decision\n * (see `createPlacementAnchor`), so this never returns `{ visible:false }`\n * and never infers visibility from a 0 size. A collapsed (0×0) but present\n * element therefore yields `{ visible:true, bounds:{...,width:0,height:0} }`,\n * distinct from any hidden Placement.\n */\nexport function measurePlacement(target: HTMLElement): Placement {\n const r = target.getBoundingClientRect()\n return {\n visible: true,\n bounds: clampRect({ x: r.left, y: r.top, width: r.width, height: r.height }),\n }\n}\n\nconst samePlacement = (a: Placement, b: Placement): boolean => {\n // Discriminant-aware dedup: a visibility flip is always a change, even when\n // the geometry would otherwise look identical.\n if (a.visible !== b.visible) return false\n if (a.visible && b.visible) {\n return (\n a.bounds.x === b.bounds.x &&\n a.bounds.y === b.bounds.y &&\n a.bounds.width === b.bounds.width &&\n a.bounds.height === b.bounds.height\n )\n }\n return true\n}\n\n/**\n * The explicit-Placement mirror of `createViewAnchor`. Same observer/dedup/\n * teardown machinery, but the sink receives a `Placement`:\n * - `visible === true` → publish `measurePlacement(target)` and re-publish\n * SYNCHRONOUSLY on every `ResizeObserver`/`resize` tick.\n * - `visible === false` → publish `{ visible:false }` (NOT a ZERO bounds);\n * do not observe.\n *\n * Dedup carries the discriminant (`samePlacement`), so a visibility flip is\n * never coalesced away.\n *\n * Opt-in `guardDisplayNone` (default false): when on, a measured zero-area\n * target (display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` instead of `{ visible:true, bounds:0×0 }`, and an\n * IntersectionObserver is attached so a display:none transition (which\n * ResizeObserver does not report) re-publishes.\n */\nexport function createPlacementAnchor(\n target: HTMLElement,\n opts: PlacementAnchorOptions,\n): PlacementAnchorHandle {\n let visible = opts.visible\n let publish = opts.publish\n const guardDisplayNone = opts.guardDisplayNone ?? false\n // Captured at creation; the follow options are never re-set via update().\n const followScroll = opts.followScroll ?? false\n const followGeometry = opts.followGeometry ?? false\n let observer: ResizeObserver | null = null\n let io: IntersectionObserver | null = null\n let lastPublished: Placement | null = null\n let disposed = false\n\n // ── Windowed RAF geometry sentinel state ──────────────────\n // The sentinel is a windowed poll, opened on demand and auto-closing once\n // the geometry goes steady. It publishes IN-FRAME (no nested defer): each\n // frame measures, and either publishes a changed rect synchronously or\n // counts toward the steady-close threshold. While closed `rafId` is null\n // and no frame is scheduled — zero static cost when idle.\n let rafId: number | null = null\n let steadyFrames = 0\n const STEADY_CLOSE_FRAMES = 2\n // Bounds hidden polls the sentinel FOLLOWS so a no-deadline window can't spin.\n const MAX_HIDDEN_FOLLOW_FRAMES = 30\n // True while a [role=\"separator\"] splitter drag is in progress: set on the\n // capture-phase pointerdown that opened the window, cleared on pointerup.\n // A held pointer means the drag may still resume after a static pause, so a\n // steady run while held must NOT close the sentinel — it only closes once the\n // pointer is released. Without this gate a press that pauses a couple of\n // frames before the drag actually moves would close mid-press and drop the\n // entire subsequent drag.\n let pointerHeld = false\n // Absolute time (performance.now()) past which a `pulse(durationMs)` window\n // force-closes even if the geometry is still changing — the upper bound that\n // prevents a perpetually-animating target from keeping the sentinel resident.\n // null = no time bound (scroll/splitter opens rely on steady-close instead).\n let sentinelDeadline: number | null = null\n\n // Measure the target, applying the opt-in first-frame / display:none guard:\n // a zero-area box (no geometry to anchor) becomes a detach instead of a\n // 0×0-visible Placement. Default off → byte-for-byte the plain measure.\n const computePlacement = (): Placement => {\n const p = measurePlacement(target)\n if (\n guardDisplayNone &&\n p.visible &&\n (p.bounds.width === 0 || p.bounds.height === 0)\n ) {\n return { visible: false }\n }\n return p\n }\n\n const emit = (): void => {\n if (disposed || !visible) return\n const p = computePlacement()\n if (lastPublished && samePlacement(lastPublished, p)) return\n lastPublished = p\n publish(p)\n }\n\n // Hidden sentinel poll → close (true) once RO/IO recorded the real hide or the\n // bounded follow run elapsed, else keep following (false); never publishes.\n const shouldCloseOnHiddenPoll = (): boolean => {\n if (lastPublished?.visible === false) return true\n return steadyFrames++ >= MAX_HIDDEN_FOLLOW_FRAMES\n }\n\n // One sentinel frame: measure, publish-in-frame if changed, else count toward\n // the steady-close threshold. Reads `disposed`/`visible` live so a frame\n // outliving teardown is inert.\n const sentinelFrame = (): void => {\n rafId = null\n if (disposed || !visible) {\n sentinelDeadline = null\n return\n }\n // Upper bound: a pulse window past its deadline closes regardless of\n // motion, so a target that changes every frame can't keep the sentinel alive.\n if (sentinelDeadline !== null && performance.now() >= sentinelDeadline) {\n sentinelDeadline = null\n return // duration elapsed → close (no re-arm)\n }\n const p = computePlacement()\n // The sentinel FOLLOWS visible geometry and NEVER publishes a detach — a\n // hidden poll is a relayout transient to follow until restore (the fix).\n if (!p.visible) {\n if (shouldCloseOnHiddenPoll()) { sentinelDeadline = null; return }\n rafId = requestAnimationFrame(sentinelFrame)\n return\n }\n if (lastPublished && samePlacement(lastPublished, p)) {\n steadyFrames++\n // Steady-close only fires once the pointer is RELEASED: while a\n // splitter drag is held, a static pause is a hesitation, not the end of\n // the drag, so we keep polling (re-arm below) and let `steadyFrames`\n // accrue — it converges to a close within N frames after pointerup.\n if (steadyFrames >= STEADY_CLOSE_FRAMES && !pointerHeld) {\n sentinelDeadline = null\n return // steady (and released) → close\n }\n } else {\n lastPublished = p\n publish(p) // publish synchronously in THIS frame\n steadyFrames = 0\n }\n // `publish` may have synchronously disposed (or hidden) the anchor; re-read\n // live state so a re-entrant teardown leaves ZERO scheduled frames.\n if (!disposed && visible) {\n rafId = requestAnimationFrame(sentinelFrame) // keep polling\n } else {\n sentinelDeadline = null\n }\n }\n\n const openSentinel = (): void => {\n if (!followGeometry || disposed) return\n steadyFrames = 0\n if (rafId === null) rafId = requestAnimationFrame(sentinelFrame)\n }\n\n const closeSentinel = (): void => {\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n steadyFrames = 0\n sentinelDeadline = null\n pointerHeld = false\n }\n\n // An ancestor scroll moved the target's screen rect. With the sentinel on,\n // open the window so the whole scroll burst is followed frame-by-frame;\n // without it, a single synchronous emit() follows the new rect.\n const onScroll = (): void => {\n if (followGeometry) openSentinel()\n else emit()\n }\n\n // A capture-phase pointerdown on a [role=\"separator\"] splitter handle marks\n // the start of a drag that moves the target via ancestor reflow (no RO tick)\n // → open the sentinel.\n const onPointerDown = (e: Event): void => {\n const t = e.target as Element | null\n if (t && t.closest && t.closest('[role=\"separator\"]')) {\n pointerHeld = true\n openSentinel()\n }\n }\n\n // Pointer released: the drag is over, so a steady run may now close the\n // sentinel. Re-open it (a no-op if already polling) so the steady-close\n // threshold is reached even if the geometry was already static at release.\n const onPointerUp = (): void => {\n if (!pointerHeld) return\n pointerHeld = false\n openSentinel()\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n // A display:none transition is invisible to ResizeObserver; an\n // IntersectionObserver re-fires `emit`, which re-measures via\n // `computePlacement` (now-zero box → detach, restored box → visible).\n if (guardDisplayNone && typeof IntersectionObserver !== 'undefined') {\n io = new IntersectionObserver(emit)\n io.observe(target)\n }\n if (followScroll) {\n window.addEventListener('scroll', onScroll, {\n capture: true,\n passive: true,\n })\n }\n if (followGeometry) {\n window.addEventListener('pointerdown', onPointerDown, { capture: true })\n window.addEventListener('pointerup', onPointerUp, { capture: true })\n }\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n if (io) {\n io.disconnect()\n io = null\n }\n window.removeEventListener('resize', emit)\n window.removeEventListener('scroll', onScroll, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerdown', onPointerDown, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerup', onPointerUp, {\n capture: true,\n } as EventListenerOptions)\n closeSentinel()\n }\n\n const apply = (): void => {\n lastPublished = null\n if (visible) {\n startObserving()\n lastPublished = computePlacement()\n publish(lastPublished)\n } else {\n stopObserving()\n const hidden: Placement = { visible: false }\n lastPublished = hidden\n publish(hidden)\n }\n }\n\n apply()\n\n return {\n update(next: PlacementAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n visible = next.visible\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n pulse(durationMs?: number): void {\n // Imperative window open: start the animation-follow window. It\n // closes on steady (N=2 unchanged frames) OR, when `durationMs` is given,\n // at that deadline — whichever comes first. The deadline is the upper bound\n // that guarantees a still-animating target cannot keep the sentinel\n // resident; without it, only steady-close applies.\n if (disposed || !followGeometry) return\n if (durationMs !== undefined && durationMs > 0) {\n const next = performance.now() + durationMs\n // Extend (never shorten) an existing window's deadline.\n sentinelDeadline = sentinelDeadline === null ? next : Math.max(sentinelDeadline, next)\n }\n openSentinel()\n },\n }\n}\n","import type { DesiredView, PlacementSnapshot } from '../layout/index.js'\n\n// Renderer-side single source of truth for native-view placement. Every anchor\n// writes its view's desired placement here instead of invoking IPC directly; a\n// central scheduler reads the WHOLE table once per animation frame and publishes\n// one window-level snapshot (a monotonic epoch shared by all views in the tick).\n//\n// Coalescing a level stream is safe: many set()/remove() calls in one frame\n// collapse to the latest level, so a transient (e.g. a relayout that momentarily\n// measures 0×0 then restores) is overwritten before it is ever published. This\n// is what a per-view edge stream cannot do — it is the producer half of the\n// reconcile design (see ../layout/placement-reconcile.ts).\n\nexport interface PlacementPublisherDeps<Extra = unknown> {\n // Renderer lifetime id stamped on every snapshot; a fresh renderer uses a\n // higher generation so the main reconciler resets its table. A function is\n // re-read on EVERY flush, so a host whose generation is assigned by main (via a\n // grant that can arrive after the publisher is created) can hand a getter and\n // have later snapshots pick up the newer value.\n generation: number | (() => number)\n publish: (snapshot: PlacementSnapshot<Extra>) => void\n // Injectable for tests; default to requestAnimationFrame / cancelAnimationFrame.\n requestFrame?: (cb: () => void) => number\n cancelFrame?: (id: number) => void\n}\n\nexport interface PlacementPublisher<Extra = unknown> {\n // Upsert one view's desired placement and schedule a coalesced publish.\n set(view: DesiredView<Extra>): void\n // Drop a view from the desired table and schedule a coalesced publish.\n remove(viewId: string): void\n dispose(): void\n}\n\nexport function createPlacementPublisher<Extra = unknown>(\n deps: PlacementPublisherDeps<Extra>,\n): PlacementPublisher<Extra> {\n const requestFrame =\n deps.requestFrame ?? ((cb: () => void): number => requestAnimationFrame(cb))\n const cancelFrame = deps.cancelFrame ?? ((id: number): void => cancelAnimationFrame(id))\n const readGeneration =\n typeof deps.generation === 'function' ? deps.generation : (): number => deps.generation as number\n\n const views = new Map<string, DesiredView<Extra>>()\n let dirty = false\n let frameId: number | null = null\n let epoch = 0\n let disposed = false\n\n function schedule(): void {\n if (disposed || frameId !== null) return\n frameId = requestFrame(flush)\n }\n\n function flush(): void {\n frameId = null\n // A frame that fires with nothing dirty (or after dispose) publishes\n // nothing — coalescing means only a real change reaches the wire.\n if (disposed || !dirty) return\n dirty = false\n deps.publish({\n generation: readGeneration(),\n epoch: epoch++,\n views: [...views.values()],\n })\n }\n\n return {\n set(view: DesiredView<Extra>): void {\n if (disposed) return\n views.set(view.viewId, view)\n dirty = true\n schedule()\n },\n remove(viewId: string): void {\n if (disposed) return\n // Removing an absent id is not a change — don't arm a redundant frame.\n if (!views.delete(viewId)) return\n dirty = true\n schedule()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n if (frameId !== null) {\n cancelFrame(frameId)\n frameId = null\n }\n },\n }\n}\n","import { createPlacementAnchor } from '@dimina-kit/view-anchor'\nimport type { Placement } from '@dimina-kit/view-anchor'\nimport { createPlacementPublisher } from './placement-publisher.js'\nimport type { PlacementSnapshot } from '../layout/index.js'\n\n/** Per-view host extra threaded on every DesiredView: the slot's capability\n * token. Main derives the view's identity + z-order from this token (never from\n * the renderer-reported viewId), so a valid token can't be spliced onto a forged\n * viewId. */\nexport interface SlotExtra {\n\tslotToken: string\n}\n\n/** One main→renderer slot grant: a native view (`viewId`) wants to follow the\n * DOM slot `slotId`; `slotToken` is the capability the renderer threads back\n * (as a per-view extra) so main can match a placement to the granted slot.\n * `generation` is a main-assigned, strictly-monotonic-per-wc renderer-lifetime\n * id: it is stamped onto every published snapshot so a reload (new grants at a\n * higher generation) resets main's reconciler regardless of IPC ordering. */\nexport interface SlotGrant {\n\tviewId: string\n\tslotId: string\n\tslotToken: string\n\tgeneration: number\n}\n\n/** The renderer-side transport for the slot-token handshake. `onSlotGrant`\n * registers the grant listener (returns an unsubscribe); `subscribe` asks main\n * to (re)play buffered grants AFTER the listener is attached; `sendSnapshot`\n * forwards the whole window-level desired-placement table (one coalesced frame),\n * each view carrying its slot's token as an extra. */\nexport interface LayoutBridge {\n\tonSlotGrant(cb: (grant: SlotGrant) => void): () => void\n\tsendSnapshot(snapshot: PlacementSnapshot<SlotExtra>): void\n\tsubscribe(): void\n}\n\nexport interface LayoutClientDeps {\n\tbridge: LayoutBridge\n\t/** Resolve a grant's `slotId` to its DOM element. Default:\n\t * `document.querySelector(slotId)`. Returns `null` when the slot is not\n\t * mounted (graceful no-op). */\n\tresolveSlot?(slotId: string): HTMLElement | null\n\t/** Anchor factory. Default: view-anchor's `createPlacementAnchor`. Injected\n\t * in tests to capture `(target, opts)` without real RO/IO/RAF. */\n\tcreateAnchor?: (\n\t\ttarget: HTMLElement,\n\t\topts: {\n\t\t\tvisible: boolean\n\t\t\tpublish: (p: Placement) => void\n\t\t\tfollowScroll?: boolean\n\t\t\tfollowGeometry?: boolean\n\t\t\tguardDisplayNone?: boolean\n\t\t},\n\t) => { dispose(): void }\n\t/** Frame scheduler for the internal placement publisher. Default:\n\t * requestAnimationFrame / cancelAnimationFrame. Injected in tests to drive\n\t * coalesced publishes deterministically. */\n\trequestFrame?: (cb: () => void) => number\n\tcancelFrame?: (id: number) => void\n}\n\n/**\n * Renderer half of the slot-token handshake. Subscribes to main's `slot-grant`\n * pushes FIRST (so a grant replayed synchronously by `subscribe()` cannot be\n * missed), then on each grant anchors the granted DOM slot with this session's\n * hardening opts (followScroll / followGeometry / guardDisplayNone). Each anchor's\n * measured `Placement` is written into a CENTRAL placement publisher keyed by\n * `viewId` — NOT sent per-view. The publisher coalesces every anchor's writes into\n * ONE window-level snapshot per animation frame, so a transient relayout that\n * momentarily measures 0×0 is overwritten before it is ever published. This is the\n * producer half of the level-triggered reconcile design (see\n * ../layout/placement-reconcile.ts); a per-view edge stream cannot self-correct a\n * lost or spurious frame, which is what caused a stuck detached (white-screen)\n * view.\n */\nexport function createDeckLayoutClient(deps: LayoutClientDeps): {\n\tdispose(): void\n} {\n\tconst resolveSlot =\n\t\tdeps.resolveSlot ??\n\t\t((id: string): HTMLElement | null => document.querySelector(id))\n\tconst createAnchor = deps.createAnchor ?? createPlacementAnchor\n\n\t// Max generation seen across all grants. Main assigns strictly-monotonic\n\t// generations, so taking the max means a reload's higher-generation grants\n\t// bump every subsequent snapshot's generation → main's reconciler resets.\n\tlet maxGeneration = 0\n\n\t// Central publisher: every anchor writes its view's desired placement here; the\n\t// publisher reads the whole table once per frame and publishes one snapshot.\n\tconst publisher = createPlacementPublisher<SlotExtra>({\n\t\tgeneration: () => maxGeneration,\n\t\tpublish: snapshot => deps.bridge.sendSnapshot(snapshot),\n\t\trequestFrame: deps.requestFrame,\n\t\tcancelFrame: deps.cancelFrame,\n\t})\n\n\t// One live anchor per `viewId`. Main intentionally re-delivers a grant on\n\t// per-wc replay, so we dedup: a same-token replay is a no-op (keep the live\n\t// anchor); a new token for an existing `viewId` replaces it (dispose old,\n\t// create new). The map holds only CURRENTLY-LIVE anchors — a replaced anchor\n\t// is disposed at replacement time and removed, so `dispose()` never\n\t// double-disposes it.\n\tconst byViewId = new Map<string, { token: string; anchor: { dispose(): void } }>()\n\tlet disposed = false\n\n\t// Register the grant listener BEFORE requesting replay (handshake):\n\t// a grant the main side replays synchronously inside `subscribe()` must find\n\t// the listener already attached.\n\tconst unsub = deps.bridge.onSlotGrant((grant) => {\n\t\tif (disposed) return\n\t\tif (grant.generation > maxGeneration) maxGeneration = grant.generation\n\t\tconst existing = byViewId.get(grant.viewId)\n\t\t// Same viewId+token re-delivered (per-wc replay) → pure no-op: keep the\n\t\t// live anchor, do not create a second, do not dispose the first.\n\t\tif (existing && existing.token === grant.slotToken) return\n\t\t// A new token for this viewId → REVOKE the stale anchor FIRST: dispose it,\n\t\t// drop it from the publisher table, and remove it from the map BEFORE\n\t\t// resolving the new slot, so a stale anchor can never keep contributing a\n\t\t// revoked token even when the new slot isn't mounted yet.\n\t\tif (existing) {\n\t\t\t// try/finally: the map delete + publisher.remove MUST run even if the old\n\t\t\t// anchor's dispose throws — otherwise the disposed-but-still-mapped anchor\n\t\t\t// keeps its snapshot entry AND a later client.dispose() would dispose it a\n\t\t\t// second time.\n\t\t\ttry {\n\t\t\t\texisting.anchor.dispose()\n\t\t\t}\n\t\t\tfinally {\n\t\t\t\tbyViewId.delete(grant.viewId)\n\t\t\t\tpublisher.remove(grant.viewId)\n\t\t\t}\n\t\t}\n\t\tconst el = resolveSlot(grant.slotId)\n\t\tif (!el) return // new slot not mounted → graceful no-op; old anchor already revoked\n\t\tconst token = grant.slotToken\n\t\tconst anchor = createAnchor(el, {\n\t\t\tvisible: true,\n\t\t\tfollowScroll: true,\n\t\t\tfollowGeometry: true,\n\t\t\tguardDisplayNone: true,\n\t\t\t// Capture THIS grant's viewId + slotToken so the measured placement lands\n\t\t\t// in the central publisher under the right key with its own token (no\n\t\t\t// cross-talk between slots). layer is 0: z-order is host-controlled\n\t\t\t// (compositor zone), not renderer-driven.\n\t\t\tpublish: (placement) => {\n\t\t\t\tpublisher.set({ viewId: grant.viewId, placement, layer: 0, extra: { slotToken: token } })\n\t\t\t},\n\t\t})\n\t\tbyViewId.set(grant.viewId, { token, anchor })\n\t})\n\n\t// Request replay AFTER the listener is attached.\n\tdeps.bridge.subscribe()\n\n\treturn {\n\t\tdispose(): void {\n\t\t\tif (disposed) return\n\t\t\tdisposed = true\n\t\t\tunsub()\n\t\t\tfor (const { anchor } of byViewId.values()) anchor.dispose()\n\t\t\tbyViewId.clear()\n\t\t\t// Dispose the publisher LAST so no anchor teardown races a pending frame.\n\t\t\tpublisher.dispose()\n\t\t},\n\t}\n}\n","import {\n\tDeckClientNotReadyError,\n\tDeckRemoteError,\n} from '../errors.js'\nimport {\n\tBRIDGE_PROTOCOL_VERSION,\n\tDEFAULT_BRIDGE_GLOBAL,\n} from '../shared/protocol.js'\nimport type { EventEnvelope, DeckBridge } from '../shared/protocol.js'\nimport type { Disposable, HostEvent, JsonValue } from '../types.js'\n\nexport { DeckClientNotReadyError, DeckRemoteError }\n\nexport { createDeckLayoutClient } from './layout-client.js'\nexport type {\n\tSlotGrant,\n\tLayoutBridge,\n\tLayoutClientDeps,\n} from './layout-client.js'\n\nexport { createPlacementPublisher } from './placement-publisher.js'\nexport type {\n\tPlacementPublisher,\n\tPlacementPublisherDeps,\n} from './placement-publisher.js'\n\nexport interface CreateDeckClientOptions {\n\t/** 默认 `__electronDeckBridge`;必须与 host preload 对齐 */\n\treadonly globalName?: string\n}\n\n// HS / EV 使用 `any` 而非 `JsonValue[]` 约束:\n// (1) host 侧 HostServiceHandler 同样是 `(...args: any[]) => unknown`,两侧对称;\n// (2) JsonValue 索引签名挡掉常见 host 写法(`(p: { code: string }) => ...`),\n// 强迫 rest-only 签名时 `Parameters<HS[K]>` 推不出真实参数类型;\n// (3) Electron IPC 实际走 structured clone(非 JSON),原约束的\"前提\"本就错;\n// (4) 真要 runtime 校验,在 transport 边界用 `runtime.ipc.handle({ validator })`\n// 与 protocol envelope(`InvokeRequest.args: readonly JsonValue[]`)已经够。\nexport interface DeckClient<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tHS extends Record<keyof HS, (...args: any[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n> {\n\tready(): Promise<void>\n\tinvoke<K extends keyof HS & string>(\n\t\tname: K,\n\t\t...args: Parameters<HS[K]>\n\t): Promise<Awaited<ReturnType<HS[K]>>>\n\ton<E extends EV[number]>(\n\t\tevent: E,\n\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t): Disposable\n}\n\nexport function createDeckClient<\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any\n\tHS extends Record<keyof HS, (...args: any[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n>(options?: CreateDeckClientOptions): DeckClient<HS, EV> {\n\tconst globalName = options?.globalName ?? DEFAULT_BRIDGE_GLOBAL\n\n\tfunction getBridge(): DeckBridge {\n\t\tconst bridge = readBridgeFromGlobal(globalName)\n\t\tif (bridge === undefined) {\n\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t`Deck framework bridge is missing on window[\"${globalName}\"] — did the host preload call exposeDeckBridge()?`,\n\t\t\t)\n\t\t}\n\t\treturn bridge\n\t}\n\n\treturn {\n\t\tasync ready(): Promise<void> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst remoteMajor = parseMajor(bridge.version)\n\t\t\tconst localMajor = parseMajor(BRIDGE_PROTOCOL_VERSION)\n\t\t\tif (remoteMajor !== localMajor) {\n\t\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t\t`Deck bridge protocol version mismatch: bridge=${String(bridge.version)}, client expected major ${localMajor}.x.x (${BRIDGE_PROTOCOL_VERSION})`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tasync invoke<K extends keyof HS & string>(\n\t\t\tname: K,\n\t\t\t...args: Parameters<HS[K]>\n\t\t): Promise<Awaited<ReturnType<HS[K]>>> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst response = await bridge.invoke({\n\t\t\t\tkind: 'host',\n\t\t\t\tname,\n\t\t\t\targs: args as readonly JsonValue[],\n\t\t\t})\n\t\t\tif (response.ok) {\n\t\t\t\treturn response.result as Awaited<ReturnType<HS[K]>>\n\t\t\t}\n\t\t\tconst { remoteName, message, code } = response.error\n\t\t\tthrow new DeckRemoteError(remoteName, message, code)\n\t\t},\n\n\t\ton<E extends EV[number]>(\n\t\t\tevent: E,\n\t\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t\t): Disposable {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst wrapped = (env: EventEnvelope): void => {\n\t\t\t\tif (env.name === event.name) {\n\t\t\t\t\tlistener(env.payload as E extends HostEvent<infer P> ? P : never)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unsubscribe = bridge.onEvent(wrapped)\n\t\t\tlet disposed = false\n\t\t\treturn {\n\t\t\t\tdispose: () => {\n\t\t\t\t\tif (disposed) return\n\t\t\t\t\tdisposed = true\n\t\t\t\t\tunsubscribe()\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n\nexport function readBridgeFromGlobal(\n\tglobalName: string = DEFAULT_BRIDGE_GLOBAL,\n): DeckBridge | undefined {\n\tconst g = globalThis as unknown as Record<string, unknown>\n\tconst bridge = g[globalName]\n\tif (bridge === undefined || bridge === null) return undefined\n\treturn bridge as DeckBridge\n}\n\nfunction parseMajor(version: unknown): number {\n\tif (typeof version !== 'string') return Number.NaN\n\tconst head = version.split('.')[0] ?? ''\n\tconst n = Number.parseInt(head, 10)\n\treturn Number.isNaN(n) ? Number.NaN : n\n}\n\nexport { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL }\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../view-anchor/src/view-anchor.ts","../../src/client/placement-publisher.ts","../../src/client/layout-client.ts","../../src/client/index.ts"],"sourcesContent":["import type {\n Bounds,\n Placement,\n ViewAnchorOptions,\n ViewAnchorHandle,\n} from './types.js'\n\nconst ZERO: Bounds = { x: 0, y: 0, width: 0, height: 0 }\n\n// Round to integers (setBounds rejects fractionals). Width/height are clamped\n// to ≥0 (negative area is meaningless and `0` is the canonical \"hidden\"\n// signal). x/y are NOT clamped: a position is a position — an anchored overlay\n// scrolled past the top/left edge has a legitimately NEGATIVE origin, and\n// flooring it to 0 would pin the native view at the edge instead of letting it\n// track its element off-screen. (Each consumer's IPC schema enforces its own\n// origin policy — the simulator's allows negative; the DevTools placeholder\n// never goes negative, so its NonNegInt schema is unaffected.)\nconst clampRect = (r: {\n x: number\n y: number\n width: number\n height: number\n}): Bounds => ({\n x: Math.round(r.x),\n y: Math.round(r.y),\n width: Math.max(0, Math.round(r.width)),\n height: Math.max(0, Math.round(r.height)),\n})\n\n/**\n * Create an anchor binding ONE native view's bounds to `target`'s geometry.\n *\n * Imperative core — no React, no Electron. Behaviour:\n * - `present === true`: publish `target.getBoundingClientRect()` (x/y rounded,\n * width/height `Math.max(0, Math.round(...))`) immediately, then re-publish\n * SYNCHRONOUSLY on every `ResizeObserver` tick and window `resize`.\n * - `present === false`: publish `{0,0,0,0}` immediately; do not observe.\n * - `update(opts)`: re-apply synchronously.\n * - `dispose()`: stop observing, never publish again.\n *\n * Synchronous, NOT RAF-deferred: the native overlay is a cross-process\n * `WebContentsView` whose `setBounds` already lands ~1 compositor frame behind\n * the renderer's DOM paint (the two processes composite on different frames).\n * Deferring the measure+publish to a RAF stacked a SECOND frame on top — during\n * a height/splitter drag that read as the overlay visibly trailing the region\n * edge (worst when GROWING, where the not-yet-followed edge exposes background).\n * Publishing in the observer tick itself removes that self-inflicted frame and\n * leaves only the unavoidable cross-process frame (masked by matching the\n * placeholder/desk background colour). The anti-flood role the RAF used to play\n * — collapsing a burst of RO+resize ticks in one frame into one publish — is now\n * served by `lastPublished` dedup: a tick whose measured rect is byte-identical\n * to the last published one is dropped, so a continuous drag still emits at most\n * one publish per distinct rect.\n *\n * Teardown safety: there is no queued frame to outrun a state change — every\n * emit reads `disposed`/`present` synchronously, so a tick after\n * `update`/`dispose` can never write a stale rect over the live one.\n */\nexport function createViewAnchor(\n target: HTMLElement,\n opts: ViewAnchorOptions,\n): ViewAnchorHandle {\n let present = opts.present\n let publish = opts.publish\n let observer: ResizeObserver | null = null\n // The last rect handed to `publish`, for dedup-coalescing (see header). Reset\n // to `null` on every `apply()` so a state change (e.g. zoom, which rides in\n // the `publish` closure, not in `Bounds`) always forces one fresh publish\n // even when the geometry is unchanged.\n let lastPublished: Bounds | null = null\n let disposed = false\n\n const measure = (): Bounds => {\n const r = target.getBoundingClientRect()\n return clampRect({ x: r.left, y: r.top, width: r.width, height: r.height })\n }\n\n const sameRect = (a: Bounds, b: Bounds): boolean =>\n a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height\n\n // Measure + publish SYNCHRONOUSLY on the triggering tick — no RAF defer (see\n // header for why). Bail if torn down or detached, and dedup a rect\n // byte-identical to the last published one (collapses a same-frame RO+resize\n // burst, and a steady drag that re-fires the same final rect, into one).\n const emit = (): void => {\n if (disposed || !present) return\n const m = measure()\n if (lastPublished && sameRect(lastPublished, m)) return\n lastPublished = m\n publish(m)\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n window.removeEventListener('resize', emit)\n }\n\n // Apply the current (present, publish) synchronously. Reset `lastPublished`\n // first so the publish below is never dedup-skipped — a state change (zoom,\n // present flip, new publish target) must always re-emit even if the geometry\n // is byte-identical to the previous emit.\n const apply = (): void => {\n lastPublished = null\n if (present) {\n startObserving()\n lastPublished = measure()\n publish(lastPublished)\n } else {\n stopObserving()\n publish(ZERO)\n }\n }\n\n apply()\n\n return {\n update(next: ViewAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n present = next.present\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n }\n}\n\n// ── Explicit Placement API ────────────────────────────────────────────\n//\n// The modern surface that replaces the magic-`{0,0,0,0}` \"hidden\" value.\n// Visibility is an explicit discriminant, NEVER inferred from geometry — a\n// real 0×0-but-on-screen view is `{ visible:true, bounds:{...,width:0} }`\n// and a hidden one is `{ visible:false }` (no `bounds`). See `Placement`.\n\nexport interface PlacementAnchorOptions {\n /**\n * Caller's INTENT: should the native view be on-screen? `true` →\n * publish the measured rect as `{ visible:true, bounds }`; `false` →\n * publish `{ visible:false }`. Crucially, hiddenness comes from this\n * flag, not from a measured zero size — so a legitimately 0-sized but\n * visible target still publishes `visible:true`.\n */\n visible: boolean\n /** Receives each explicit Placement. Owns IPC → host. */\n publish: (placement: Placement) => void\n /**\n * Opt-in geometry detach. When true, a measured zero-area target (no\n * geometry box — display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` (detach-but-keep) instead of `{ visible:true,\n * bounds:0×0 }`, and an IntersectionObserver is attached so a display:none\n * transition (which ResizeObserver does not report) re-publishes. Default\n * false keeps the legitimate 0×0-visible semantics.\n */\n guardDisplayNone?: boolean\n /**\n * Opt-in capture-phase ancestor-scroll follow. When true, the anchor\n * listens for `scroll` on `window` in the CAPTURE phase (scroll events don't\n * bubble, but reach `window` while capturing), so an ancestor scroll\n * container scrolling the target re-measures and re-publishes. With\n * `followGeometry` off, the scroll callback does a single synchronous\n * `emit()`; with it on, the scroll OPENS the RAF sentinel window so the\n * follow tracks every frame of a scroll burst. Default false.\n */\n followScroll?: boolean\n /**\n * Opt-in windowed RAF geometry sentinel. Catches ancestor\n * transform / reflow moves that no DOM event reports. The sentinel is\n * NON-resident: it is OPENED on demand (a scroll burst, a `[role=\"separator\"]`\n * splitter pointerdown, or an explicit `pulse()`), polls geometry once per\n * animation frame publishing IN-FRAME, and AUTO-CLOSES once the rect goes\n * steady (a few unchanged frames). While closed it schedules no frame, so the\n * static cost when idle is exactly zero. Default false.\n */\n followGeometry?: boolean\n}\n\nexport interface PlacementAnchorHandle {\n /** Apply new options; re-publishes immediately (mirrors `createViewAnchor`). */\n update(opts: PlacementAnchorOptions): void\n /** Stop observing; never publish again. */\n dispose(): void\n /**\n * Open the RAF sentinel window (animation follow); auto-closes after going\n * steady or after `durationMs`. No-op when `followGeometry` is false.\n */\n pulse(durationMs?: number): void\n}\n\n/**\n * Pure measure: read `target`'s rect and wrap it as an explicit visible\n * Placement. Always `{ visible:true }` — hiddenness is a caller decision\n * (see `createPlacementAnchor`), so this never returns `{ visible:false }`\n * and never infers visibility from a 0 size. A collapsed (0×0) but present\n * element therefore yields `{ visible:true, bounds:{...,width:0,height:0} }`,\n * distinct from any hidden Placement.\n */\nexport function measurePlacement(target: HTMLElement): Placement {\n const r = target.getBoundingClientRect()\n return {\n visible: true,\n bounds: clampRect({ x: r.left, y: r.top, width: r.width, height: r.height }),\n }\n}\n\nconst samePlacement = (a: Placement, b: Placement): boolean => {\n // Discriminant-aware dedup: a visibility flip is always a change, even when\n // the geometry would otherwise look identical.\n if (a.visible !== b.visible) return false\n if (a.visible && b.visible) {\n return (\n a.bounds.x === b.bounds.x &&\n a.bounds.y === b.bounds.y &&\n a.bounds.width === b.bounds.width &&\n a.bounds.height === b.bounds.height\n )\n }\n return true\n}\n\n/**\n * The explicit-Placement mirror of `createViewAnchor`. Same observer/dedup/\n * teardown machinery, but the sink receives a `Placement`:\n * - `visible === true` → publish `measurePlacement(target)` and re-publish\n * SYNCHRONOUSLY on every `ResizeObserver`/`resize` tick.\n * - `visible === false` → publish `{ visible:false }` (NOT a ZERO bounds);\n * do not observe.\n *\n * Dedup carries the discriminant (`samePlacement`), so a visibility flip is\n * never coalesced away.\n *\n * Opt-in `guardDisplayNone` (default false): when on, a measured zero-area\n * target (display:none / unmounted / unstable first layout) publishes\n * `{ visible:false }` instead of `{ visible:true, bounds:0×0 }`, and an\n * IntersectionObserver is attached so a display:none transition (which\n * ResizeObserver does not report) re-publishes.\n */\nexport function createPlacementAnchor(\n target: HTMLElement,\n opts: PlacementAnchorOptions,\n): PlacementAnchorHandle {\n let visible = opts.visible\n let publish = opts.publish\n const guardDisplayNone = opts.guardDisplayNone ?? false\n // Captured at creation; the follow options are never re-set via update().\n const followScroll = opts.followScroll ?? false\n const followGeometry = opts.followGeometry ?? false\n let observer: ResizeObserver | null = null\n let io: IntersectionObserver | null = null\n let lastPublished: Placement | null = null\n let disposed = false\n\n // ── Windowed RAF geometry sentinel state ──────────────────\n // The sentinel is a windowed poll, opened on demand and auto-closing once\n // the geometry goes steady. It publishes IN-FRAME (no nested defer): each\n // frame measures, and either publishes a changed rect synchronously or\n // counts toward the steady-close threshold. While closed `rafId` is null\n // and no frame is scheduled — zero static cost when idle.\n let rafId: number | null = null\n let steadyFrames = 0\n const STEADY_CLOSE_FRAMES = 2\n // Bounds hidden polls the sentinel FOLLOWS so a no-deadline window can't spin.\n const MAX_HIDDEN_FOLLOW_FRAMES = 30\n // True while a [role=\"separator\"] splitter drag is in progress: set on the\n // capture-phase pointerdown that opened the window, cleared on pointerup.\n // A held pointer means the drag may still resume after a static pause, so a\n // steady run while held must NOT close the sentinel — it only closes once the\n // pointer is released. Without this gate a press that pauses a couple of\n // frames before the drag actually moves would close mid-press and drop the\n // entire subsequent drag.\n let pointerHeld = false\n // Absolute time (performance.now()) past which a `pulse(durationMs)` window\n // force-closes even if the geometry is still changing — the upper bound that\n // prevents a perpetually-animating target from keeping the sentinel resident.\n // null = no time bound (scroll/splitter opens rely on steady-close instead).\n let sentinelDeadline: number | null = null\n\n // Measure the target, applying the opt-in first-frame / display:none guard:\n // a zero-area box (no geometry to anchor) becomes a detach instead of a\n // 0×0-visible Placement. Default off → byte-for-byte the plain measure.\n const computePlacement = (): Placement => {\n const p = measurePlacement(target)\n if (\n guardDisplayNone &&\n p.visible &&\n (p.bounds.width === 0 || p.bounds.height === 0)\n ) {\n return { visible: false }\n }\n return p\n }\n\n const emit = (): void => {\n if (disposed || !visible) return\n const p = computePlacement()\n if (lastPublished && samePlacement(lastPublished, p)) return\n lastPublished = p\n publish(p)\n }\n\n // Hidden sentinel poll → close (true) once RO/IO recorded the real hide or the\n // bounded follow run elapsed, else keep following (false); never publishes.\n const shouldCloseOnHiddenPoll = (): boolean => {\n if (lastPublished?.visible === false) return true\n return steadyFrames++ >= MAX_HIDDEN_FOLLOW_FRAMES\n }\n\n // One sentinel frame: measure, publish-in-frame if changed, else count toward\n // the steady-close threshold. Reads `disposed`/`visible` live so a frame\n // outliving teardown is inert.\n const sentinelFrame = (): void => {\n rafId = null\n if (disposed || !visible) {\n sentinelDeadline = null\n return\n }\n // Upper bound: a pulse window past its deadline closes regardless of\n // motion, so a target that changes every frame can't keep the sentinel alive.\n if (sentinelDeadline !== null && performance.now() >= sentinelDeadline) {\n sentinelDeadline = null\n return // duration elapsed → close (no re-arm)\n }\n const p = computePlacement()\n // The sentinel FOLLOWS visible geometry and NEVER publishes a detach — a\n // hidden poll is a relayout transient to follow until restore (the fix).\n if (!p.visible) {\n if (shouldCloseOnHiddenPoll()) { sentinelDeadline = null; return }\n rafId = requestAnimationFrame(sentinelFrame)\n return\n }\n if (lastPublished && samePlacement(lastPublished, p)) {\n steadyFrames++\n // Steady-close only fires once the pointer is RELEASED: while a\n // splitter drag is held, a static pause is a hesitation, not the end of\n // the drag, so we keep polling (re-arm below) and let `steadyFrames`\n // accrue — it converges to a close within N frames after pointerup.\n if (steadyFrames >= STEADY_CLOSE_FRAMES && !pointerHeld) {\n sentinelDeadline = null\n return // steady (and released) → close\n }\n } else {\n lastPublished = p\n publish(p) // publish synchronously in THIS frame\n steadyFrames = 0\n }\n // `publish` may have synchronously disposed (or hidden) the anchor; re-read\n // live state so a re-entrant teardown leaves ZERO scheduled frames.\n if (!disposed && visible) {\n rafId = requestAnimationFrame(sentinelFrame) // keep polling\n } else {\n sentinelDeadline = null\n }\n }\n\n const openSentinel = (): void => {\n if (!followGeometry || disposed) return\n steadyFrames = 0\n if (rafId === null) rafId = requestAnimationFrame(sentinelFrame)\n }\n\n const closeSentinel = (): void => {\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n steadyFrames = 0\n sentinelDeadline = null\n pointerHeld = false\n }\n\n // An ancestor scroll moved the target's screen rect. With the sentinel on,\n // open the window so the whole scroll burst is followed frame-by-frame;\n // without it, a single synchronous emit() follows the new rect.\n const onScroll = (): void => {\n if (followGeometry) openSentinel()\n else emit()\n }\n\n // A capture-phase pointerdown on a [role=\"separator\"] splitter handle marks\n // the start of a drag that moves the target via ancestor reflow (no RO tick)\n // → open the sentinel.\n const onPointerDown = (e: Event): void => {\n const t = e.target as Element | null\n if (t && t.closest && t.closest('[role=\"separator\"]')) {\n pointerHeld = true\n openSentinel()\n }\n }\n\n // Pointer released: the drag is over, so a steady run may now close the\n // sentinel. Re-open it (a no-op if already polling) so the steady-close\n // threshold is reached even if the geometry was already static at release.\n const onPointerUp = (): void => {\n if (!pointerHeld) return\n pointerHeld = false\n openSentinel()\n }\n\n const startObserving = (): void => {\n if (observer) return\n observer = new ResizeObserver(emit)\n observer.observe(target)\n window.addEventListener('resize', emit)\n // A display:none transition is invisible to ResizeObserver; an\n // IntersectionObserver re-fires `emit`, which re-measures via\n // `computePlacement` (now-zero box → detach, restored box → visible).\n if (guardDisplayNone && typeof IntersectionObserver !== 'undefined') {\n io = new IntersectionObserver(emit)\n io.observe(target)\n }\n if (followScroll) {\n window.addEventListener('scroll', onScroll, {\n capture: true,\n passive: true,\n })\n }\n if (followGeometry) {\n window.addEventListener('pointerdown', onPointerDown, { capture: true })\n window.addEventListener('pointerup', onPointerUp, { capture: true })\n }\n }\n\n const stopObserving = (): void => {\n if (observer) {\n observer.disconnect()\n observer = null\n }\n if (io) {\n io.disconnect()\n io = null\n }\n window.removeEventListener('resize', emit)\n window.removeEventListener('scroll', onScroll, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerdown', onPointerDown, {\n capture: true,\n } as EventListenerOptions)\n window.removeEventListener('pointerup', onPointerUp, {\n capture: true,\n } as EventListenerOptions)\n closeSentinel()\n }\n\n const apply = (): void => {\n lastPublished = null\n if (visible) {\n startObserving()\n lastPublished = computePlacement()\n publish(lastPublished)\n } else {\n stopObserving()\n const hidden: Placement = { visible: false }\n lastPublished = hidden\n publish(hidden)\n }\n }\n\n apply()\n\n return {\n update(next: PlacementAnchorOptions): void {\n if (disposed) return\n publish = next.publish\n visible = next.visible\n apply()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n stopObserving()\n },\n pulse(durationMs?: number): void {\n // Imperative window open: start the animation-follow window. It\n // closes on steady (N=2 unchanged frames) OR, when `durationMs` is given,\n // at that deadline — whichever comes first. The deadline is the upper bound\n // that guarantees a still-animating target cannot keep the sentinel\n // resident; without it, only steady-close applies.\n if (disposed || !followGeometry) return\n if (durationMs !== undefined && durationMs > 0) {\n const next = performance.now() + durationMs\n // Extend (never shorten) an existing window's deadline.\n sentinelDeadline = sentinelDeadline === null ? next : Math.max(sentinelDeadline, next)\n }\n openSentinel()\n },\n }\n}\n","import type { DesiredView, PlacementSnapshot } from '../layout/index.js'\n\n// Renderer-side single source of truth for native-view placement. Every anchor\n// writes its view's desired placement here instead of invoking IPC directly; a\n// central scheduler reads the WHOLE table once per animation frame and publishes\n// one window-level snapshot (a monotonic epoch shared by all views in the tick).\n//\n// Coalescing a level stream is safe: many set()/remove() calls in one frame\n// collapse to the latest level, so a transient (e.g. a relayout that momentarily\n// measures 0×0 then restores) is overwritten before it is ever published. This\n// is what a per-view edge stream cannot do — it is the producer half of the\n// reconcile design (see ../layout/placement-reconcile.ts).\n\nexport interface PlacementPublisherDeps<Extra = unknown> {\n // Renderer lifetime id stamped on every snapshot; a fresh renderer uses a\n // higher generation so the main reconciler resets its table. A function is\n // re-read on EVERY flush, so a host whose generation is assigned by main (via a\n // grant that can arrive after the publisher is created) can hand a getter and\n // have later snapshots pick up the newer value.\n generation: number | (() => number)\n publish: (snapshot: PlacementSnapshot<Extra>) => void\n // Injectable for tests; default to requestAnimationFrame / cancelAnimationFrame.\n requestFrame?: (cb: () => void) => number\n cancelFrame?: (id: number) => void\n}\n\nexport interface PlacementPublisher<Extra = unknown> {\n // Upsert one view's desired placement and schedule a coalesced publish.\n set(view: DesiredView<Extra>): void\n // Drop a view from the desired table and schedule a coalesced publish.\n remove(viewId: string): void\n dispose(): void\n}\n\nexport function createPlacementPublisher<Extra = unknown>(\n deps: PlacementPublisherDeps<Extra>,\n): PlacementPublisher<Extra> {\n const requestFrame =\n deps.requestFrame ?? ((cb: () => void): number => requestAnimationFrame(cb))\n const cancelFrame = deps.cancelFrame ?? ((id: number): void => cancelAnimationFrame(id))\n const readGeneration =\n typeof deps.generation === 'function' ? deps.generation : (): number => deps.generation as number\n\n const views = new Map<string, DesiredView<Extra>>()\n let dirty = false\n let frameId: number | null = null\n let epoch = 0\n let disposed = false\n\n function schedule(): void {\n if (disposed || frameId !== null) return\n frameId = requestFrame(flush)\n }\n\n function flush(): void {\n frameId = null\n // A frame that fires with nothing dirty (or after dispose) publishes\n // nothing — coalescing means only a real change reaches the wire.\n if (disposed || !dirty) return\n dirty = false\n deps.publish({\n generation: readGeneration(),\n epoch: epoch++,\n views: [...views.values()],\n })\n }\n\n return {\n set(view: DesiredView<Extra>): void {\n if (disposed) return\n views.set(view.viewId, view)\n dirty = true\n schedule()\n },\n remove(viewId: string): void {\n if (disposed) return\n // Removing an absent id is not a change — don't arm a redundant frame.\n if (!views.delete(viewId)) return\n dirty = true\n schedule()\n },\n dispose(): void {\n if (disposed) return\n disposed = true\n if (frameId !== null) {\n cancelFrame(frameId)\n frameId = null\n }\n },\n }\n}\n","import { createPlacementAnchor } from '@dimina-kit/view-anchor'\nimport type { Placement } from '@dimina-kit/view-anchor'\nimport { createPlacementPublisher } from './placement-publisher.js'\nimport type { PlacementSnapshot } from '../layout/index.js'\n\n/** Per-view host extra threaded on every DesiredView: the slot's capability\n * token. Main derives the view's identity + z-order from this token (never from\n * the renderer-reported viewId), so a valid token can't be spliced onto a forged\n * viewId. */\nexport interface SlotExtra {\n\tslotToken: string\n}\n\n/** One main→renderer slot grant: a native view (`viewId`) wants to follow the\n * DOM slot `slotId`; `slotToken` is the capability the renderer threads back\n * (as a per-view extra) so main can match a placement to the granted slot.\n * `generation` is a main-assigned, strictly-monotonic-per-wc renderer-lifetime\n * id: it is stamped onto every published snapshot so a reload (new grants at a\n * higher generation) resets main's reconciler regardless of IPC ordering. */\nexport interface SlotGrant {\n\tviewId: string\n\tslotId: string\n\tslotToken: string\n\tgeneration: number\n}\n\n/** The renderer-side transport for the slot-token handshake. `onSlotGrant`\n * registers the grant listener (returns an unsubscribe); `subscribe` asks main\n * to (re)play buffered grants AFTER the listener is attached; `sendSnapshot`\n * forwards the whole window-level desired-placement table (one coalesced frame),\n * each view carrying its slot's token as an extra. */\nexport interface LayoutBridge {\n\tonSlotGrant(cb: (grant: SlotGrant) => void): () => void\n\tsendSnapshot(snapshot: PlacementSnapshot<SlotExtra>): void\n\tsubscribe(): void\n}\n\nexport interface LayoutClientDeps {\n\tbridge: LayoutBridge\n\t/** Resolve a grant's `slotId` to its DOM element. Default:\n\t * `document.querySelector(slotId)`. Returns `null` when the slot is not\n\t * mounted (graceful no-op). */\n\tresolveSlot?(slotId: string): HTMLElement | null\n\t/** Anchor factory. Default: view-anchor's `createPlacementAnchor`. Injected\n\t * in tests to capture `(target, opts)` without real RO/IO/RAF. */\n\tcreateAnchor?: (\n\t\ttarget: HTMLElement,\n\t\topts: {\n\t\t\tvisible: boolean\n\t\t\tpublish: (p: Placement) => void\n\t\t\tfollowScroll?: boolean\n\t\t\tfollowGeometry?: boolean\n\t\t\tguardDisplayNone?: boolean\n\t\t},\n\t) => { dispose(): void }\n\t/** Frame scheduler for the internal placement publisher. Default:\n\t * requestAnimationFrame / cancelAnimationFrame. Injected in tests to drive\n\t * coalesced publishes deterministically. */\n\trequestFrame?: (cb: () => void) => number\n\tcancelFrame?: (id: number) => void\n}\n\n/**\n * Renderer half of the slot-token handshake. Subscribes to main's `slot-grant`\n * pushes FIRST (so a grant replayed synchronously by `subscribe()` cannot be\n * missed), then on each grant anchors the granted DOM slot with this session's\n * hardening opts (followScroll / followGeometry / guardDisplayNone). Each anchor's\n * measured `Placement` is written into a CENTRAL placement publisher keyed by\n * `viewId` — NOT sent per-view. The publisher coalesces every anchor's writes into\n * ONE window-level snapshot per animation frame, so a transient relayout that\n * momentarily measures 0×0 is overwritten before it is ever published. This is the\n * producer half of the level-triggered reconcile design (see\n * ../layout/placement-reconcile.ts); a per-view edge stream cannot self-correct a\n * lost or spurious frame, which is what caused a stuck detached (white-screen)\n * view.\n */\nexport function createDeckLayoutClient(deps: LayoutClientDeps): {\n\tdispose(): void\n} {\n\tconst resolveSlot =\n\t\tdeps.resolveSlot ??\n\t\t((id: string): HTMLElement | null => document.querySelector(id))\n\tconst createAnchor = deps.createAnchor ?? createPlacementAnchor\n\n\t// Max generation seen across all grants. Main assigns strictly-monotonic\n\t// generations, so taking the max means a reload's higher-generation grants\n\t// bump every subsequent snapshot's generation → main's reconciler resets.\n\tlet maxGeneration = 0\n\n\t// Central publisher: every anchor writes its view's desired placement here; the\n\t// publisher reads the whole table once per frame and publishes one snapshot.\n\tconst publisher = createPlacementPublisher<SlotExtra>({\n\t\tgeneration: () => maxGeneration,\n\t\tpublish: snapshot => deps.bridge.sendSnapshot(snapshot),\n\t\trequestFrame: deps.requestFrame,\n\t\tcancelFrame: deps.cancelFrame,\n\t})\n\n\t// One live anchor per `viewId`. Main intentionally re-delivers a grant on\n\t// per-wc replay, so we dedup: a same-token replay is a no-op (keep the live\n\t// anchor); a new token for an existing `viewId` replaces it (dispose old,\n\t// create new). The map holds only CURRENTLY-LIVE anchors — a replaced anchor\n\t// is disposed at replacement time and removed, so `dispose()` never\n\t// double-disposes it.\n\tconst byViewId = new Map<string, { token: string; anchor: { dispose(): void } }>()\n\tlet disposed = false\n\n\t// Register the grant listener BEFORE requesting replay (handshake):\n\t// a grant the main side replays synchronously inside `subscribe()` must find\n\t// the listener already attached.\n\tconst unsub = deps.bridge.onSlotGrant((grant) => {\n\t\tif (disposed) return\n\t\tif (grant.generation > maxGeneration) maxGeneration = grant.generation\n\t\tconst existing = byViewId.get(grant.viewId)\n\t\t// Same viewId+token re-delivered (per-wc replay) → pure no-op: keep the\n\t\t// live anchor, do not create a second, do not dispose the first.\n\t\tif (existing && existing.token === grant.slotToken) return\n\t\t// A new token for this viewId → REVOKE the stale anchor FIRST: dispose it,\n\t\t// drop it from the publisher table, and remove it from the map BEFORE\n\t\t// resolving the new slot, so a stale anchor can never keep contributing a\n\t\t// revoked token even when the new slot isn't mounted yet.\n\t\tif (existing) {\n\t\t\t// try/finally: the map delete + publisher.remove MUST run even if the old\n\t\t\t// anchor's dispose throws — otherwise the disposed-but-still-mapped anchor\n\t\t\t// keeps its snapshot entry AND a later client.dispose() would dispose it a\n\t\t\t// second time.\n\t\t\ttry {\n\t\t\t\texisting.anchor.dispose()\n\t\t\t}\n\t\t\tfinally {\n\t\t\t\tbyViewId.delete(grant.viewId)\n\t\t\t\tpublisher.remove(grant.viewId)\n\t\t\t}\n\t\t}\n\t\tconst el = resolveSlot(grant.slotId)\n\t\tif (!el) return // new slot not mounted → graceful no-op; old anchor already revoked\n\t\tconst token = grant.slotToken\n\t\tconst anchor = createAnchor(el, {\n\t\t\tvisible: true,\n\t\t\tfollowScroll: true,\n\t\t\tfollowGeometry: true,\n\t\t\tguardDisplayNone: true,\n\t\t\t// Capture THIS grant's viewId + slotToken so the measured placement lands\n\t\t\t// in the central publisher under the right key with its own token (no\n\t\t\t// cross-talk between slots). layer is 0: z-order is host-controlled\n\t\t\t// (compositor zone), not renderer-driven.\n\t\t\tpublish: (placement) => {\n\t\t\t\tpublisher.set({ viewId: grant.viewId, placement, layer: 0, extra: { slotToken: token } })\n\t\t\t},\n\t\t})\n\t\tbyViewId.set(grant.viewId, { token, anchor })\n\t})\n\n\t// Request replay AFTER the listener is attached.\n\tdeps.bridge.subscribe()\n\n\treturn {\n\t\tdispose(): void {\n\t\t\tif (disposed) return\n\t\t\tdisposed = true\n\t\t\tunsub()\n\t\t\tfor (const { anchor } of byViewId.values()) anchor.dispose()\n\t\t\tbyViewId.clear()\n\t\t\t// Dispose the publisher LAST so no anchor teardown races a pending frame.\n\t\t\tpublisher.dispose()\n\t\t},\n\t}\n}\n","import {\n\tDeckClientNotReadyError,\n\tDeckRemoteError,\n} from '../errors.js'\nimport {\n\tBRIDGE_PROTOCOL_VERSION,\n\tDEFAULT_BRIDGE_GLOBAL,\n} from '../shared/protocol.js'\nimport type { EventEnvelope, DeckBridge } from '../shared/protocol.js'\nimport type { Disposable, HostEvent, JsonValue } from '../types.js'\n\nexport { DeckClientNotReadyError, DeckRemoteError }\n\nexport { createDeckLayoutClient } from './layout-client.js'\nexport type {\n\tSlotGrant,\n\tLayoutBridge,\n\tLayoutClientDeps,\n} from './layout-client.js'\n\nexport { createPlacementPublisher } from './placement-publisher.js'\nexport type {\n\tPlacementPublisher,\n\tPlacementPublisherDeps,\n} from './placement-publisher.js'\n\nexport interface CreateDeckClientOptions {\n\t/** 默认 `__electronDeckBridge`;必须与 host preload 对齐 */\n\treadonly globalName?: string\n}\n\n// HS / EV 使用 `never[]` 而非 `JsonValue[]` 约束:\n// (1) host 侧 HostServiceHandler 同样是 `(...args: never[]) => unknown`,两侧对称;\n// (2) JsonValue 索引签名挡掉常见 host 写法(`(p: { code: string }) => ...`),\n// 强迫 rest-only 签名时 `Parameters<HS[K]>` 推不出真实参数类型;\n// (3) Electron IPC 实际走 structured clone(非 JSON),原约束的\"前提\"本就错;\n// (4) 真要 runtime 校验,在 transport 边界用 `runtime.ipc.handle({ validator })`\n// 与 protocol envelope(`InvokeRequest.args: readonly JsonValue[]`)已经够。\n// `never[]` (not `unknown[]`) because rest-parameter position is checked\n// contravariantly: a handler like `(p: { code: string }) => ...` is only\n// assignable into `(...args: T[]) => unknown` when T is assignable INTO its\n// param type — `never` always is, `unknown` is not (it would reject every\n// narrower host handler, defeating point (2) above).\n/** @experimental No production consumer yet — pairs with `DeckConfig.hostServices`\n * / `events`, which only `examples/` / `spike/` set; the devtools `backend`\n * assembly never touches this client. `createDeckLayoutClient` and\n * `createPlacementPublisher` (re-exported above) are unaffected — they have\n * real consumers. */\nexport interface DeckClient<\n\tHS extends Record<keyof HS, (...args: never[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n> {\n\tready(): Promise<void>\n\tinvoke<K extends keyof HS & string>(\n\t\tname: K,\n\t\t...args: Parameters<HS[K]>\n\t): Promise<Awaited<ReturnType<HS[K]>>>\n\ton<E extends EV[number]>(\n\t\tevent: E,\n\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t): Disposable\n}\n\n/** @experimental No production consumer yet — see the note on {@link DeckClient}. */\nexport function createDeckClient<\n\tHS extends Record<keyof HS, (...args: never[]) => unknown>,\n\tEV extends readonly HostEvent<JsonValue>[],\n>(options?: CreateDeckClientOptions): DeckClient<HS, EV> {\n\tconst globalName = options?.globalName ?? DEFAULT_BRIDGE_GLOBAL\n\n\tfunction getBridge(): DeckBridge {\n\t\tconst bridge = readBridgeFromGlobal(globalName)\n\t\tif (bridge === undefined) {\n\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t`Deck framework bridge is missing on window[\"${globalName}\"] — did the host preload call exposeDeckBridge()?`,\n\t\t\t)\n\t\t}\n\t\treturn bridge\n\t}\n\n\treturn {\n\t\tasync ready(): Promise<void> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst remoteMajor = parseMajor(bridge.version)\n\t\t\tconst localMajor = parseMajor(BRIDGE_PROTOCOL_VERSION)\n\t\t\tif (remoteMajor !== localMajor) {\n\t\t\t\tthrow new DeckClientNotReadyError(\n\t\t\t\t\t`Deck bridge protocol version mismatch: bridge=${String(bridge.version)}, client expected major ${localMajor}.x.x (${BRIDGE_PROTOCOL_VERSION})`,\n\t\t\t\t)\n\t\t\t}\n\t\t},\n\n\t\tasync invoke<K extends keyof HS & string>(\n\t\t\tname: K,\n\t\t\t...args: Parameters<HS[K]>\n\t\t): Promise<Awaited<ReturnType<HS[K]>>> {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst response = await bridge.invoke({\n\t\t\t\tkind: 'host',\n\t\t\t\tname,\n\t\t\t\targs: args as readonly JsonValue[],\n\t\t\t})\n\t\t\tif (response.ok) {\n\t\t\t\treturn response.result as Awaited<ReturnType<HS[K]>>\n\t\t\t}\n\t\t\tconst { remoteName, message, code } = response.error\n\t\t\tthrow new DeckRemoteError(remoteName, message, code)\n\t\t},\n\n\t\ton<E extends EV[number]>(\n\t\t\tevent: E,\n\t\t\tlistener: (payload: E extends HostEvent<infer P> ? P : never) => void,\n\t\t): Disposable {\n\t\t\tconst bridge = getBridge()\n\t\t\tconst wrapped = (env: EventEnvelope): void => {\n\t\t\t\tif (env.name === event.name) {\n\t\t\t\t\tlistener(env.payload as E extends HostEvent<infer P> ? P : never)\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst unsubscribe = bridge.onEvent(wrapped)\n\t\t\tlet disposed = false\n\t\t\treturn {\n\t\t\t\tdispose: () => {\n\t\t\t\t\tif (disposed) return\n\t\t\t\t\tdisposed = true\n\t\t\t\t\tunsubscribe()\n\t\t\t\t},\n\t\t\t}\n\t\t},\n\t}\n}\n\nexport function readBridgeFromGlobal(\n\tglobalName: string = DEFAULT_BRIDGE_GLOBAL,\n): DeckBridge | undefined {\n\tconst g = globalThis as unknown as Record<string, unknown>\n\tconst bridge = g[globalName]\n\tif (bridge === undefined || bridge === null) return undefined\n\treturn bridge as DeckBridge\n}\n\nfunction parseMajor(version: unknown): number {\n\tif (typeof version !== 'string') return Number.NaN\n\tconst head = version.split('.')[0] ?? ''\n\tconst n = Number.parseInt(head, 10)\n\treturn Number.isNaN(n) ? Number.NaN : n\n}\n\nexport { BRIDGE_PROTOCOL_VERSION, DEFAULT_BRIDGE_GLOBAL }\n"],"mappings":";;;AAiBA,IAAM,aAAa,OAKJ;CACb,GAAG,KAAK,MAAM,EAAE,CAAC;CACjB,GAAG,KAAK,MAAM,EAAE,CAAC;CACjB,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,KAAK,CAAC;CACtC,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,EAAE,MAAM,CAAC;AAC1C;;;;;;;;;AAsLA,SAAgB,iBAAiB,QAAgC;CAC/D,MAAM,IAAI,OAAO,sBAAsB;CACvC,OAAO;EACL,SAAS;EACT,QAAQ,UAAU;GAAE,GAAG,EAAE;GAAM,GAAG,EAAE;GAAK,OAAO,EAAE;GAAO,QAAQ,EAAE;EAAO,CAAC;CAC7E;AACF;AAEA,IAAM,iBAAiB,GAAc,MAA0B;CAG7D,IAAI,EAAE,YAAY,EAAE,SAAS,OAAO;CACpC,IAAI,EAAE,WAAW,EAAE,SACjB,OACE,EAAE,OAAO,MAAM,EAAE,OAAO,KACxB,EAAE,OAAO,MAAM,EAAE,OAAO,KACxB,EAAE,OAAO,UAAU,EAAE,OAAO,SAC5B,EAAE,OAAO,WAAW,EAAE,OAAO;CAGjC,OAAO;AACT;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,sBACd,QACA,MACuB;CACvB,IAAI,UAAU,KAAK;CACnB,IAAI,UAAU,KAAK;CACnB,MAAM,mBAAmB,KAAK,oBAAoB;CAElD,MAAM,eAAe,KAAK,gBAAgB;CAC1C,MAAM,iBAAiB,KAAK,kBAAkB;CAC9C,IAAI,WAAkC;CACtC,IAAI,KAAkC;CACtC,IAAI,gBAAkC;CACtC,IAAI,WAAW;CAQf,IAAI,QAAuB;CAC3B,IAAI,eAAe;CACnB,MAAM,sBAAsB;CAE5B,MAAM,2BAA2B;CAQjC,IAAI,cAAc;CAKlB,IAAI,mBAAkC;CAKtC,MAAM,yBAAoC;EACxC,MAAM,IAAI,iBAAiB,MAAM;EACjC,IACE,oBACA,EAAE,YACD,EAAE,OAAO,UAAU,KAAK,EAAE,OAAO,WAAW,IAE7C,OAAO,EAAE,SAAS,MAAM;EAE1B,OAAO;CACT;CAEA,MAAM,aAAmB;EACvB,IAAI,YAAY,CAAC,SAAS;EAC1B,MAAM,IAAI,iBAAiB;EAC3B,IAAI,iBAAiB,cAAc,eAAe,CAAC,GAAG;EACtD,gBAAgB;EAChB,QAAQ,CAAC;CACX;CAIA,MAAM,gCAAyC;EAC7C,IAAI,eAAe,YAAY,OAAO,OAAO;EAC7C,OAAO,kBAAkB;CAC3B;CAKA,MAAM,sBAA4B;EAChC,QAAQ;EACR,IAAI,YAAY,CAAC,SAAS;GACxB,mBAAmB;GACnB;EACF;EAGA,IAAI,qBAAqB,QAAQ,YAAY,IAAI,KAAK,kBAAkB;GACtE,mBAAmB;GACnB;EACF;EACA,MAAM,IAAI,iBAAiB;EAG3B,IAAI,CAAC,EAAE,SAAS;GACd,IAAI,wBAAwB,GAAG;IAAE,mBAAmB;IAAM;GAAO;GACjE,QAAQ,sBAAsB,aAAa;GAC3C;EACF;EACA,IAAI,iBAAiB,cAAc,eAAe,CAAC,GAAG;GACpD;GAKA,IAAI,gBAAgB,uBAAuB,CAAC,aAAa;IACvD,mBAAmB;IACnB;GACF;EACF,OAAO;GACL,gBAAgB;GAChB,QAAQ,CAAC;GACT,eAAe;EACjB;EAGA,IAAI,CAAC,YAAY,SACf,QAAQ,sBAAsB,aAAa;OAE3C,mBAAmB;CAEvB;CAEA,MAAM,qBAA2B;EAC/B,IAAI,CAAC,kBAAkB,UAAU;EACjC,eAAe;EACf,IAAI,UAAU,MAAM,QAAQ,sBAAsB,aAAa;CACjE;CAEA,MAAM,sBAA4B;EAChC,IAAI,UAAU,MAAM;GAClB,qBAAqB,KAAK;GAC1B,QAAQ;EACV;EACA,eAAe;EACf,mBAAmB;EACnB,cAAc;CAChB;CAKA,MAAM,iBAAuB;EAC3B,IAAI,gBAAgB,aAAa;OAC5B,KAAK;CACZ;CAKA,MAAM,iBAAiB,MAAmB;EACxC,MAAM,IAAI,EAAE;EACZ,IAAI,KAAK,EAAE,WAAW,EAAE,QAAQ,sBAAoB,GAAG;GACrD,cAAc;GACd,aAAa;EACf;CACF;CAKA,MAAM,oBAA0B;EAC9B,IAAI,CAAC,aAAa;EAClB,cAAc;EACd,aAAa;CACf;CAEA,MAAM,uBAA6B;EACjC,IAAI,UAAU;EACd,WAAW,IAAI,eAAe,IAAI;EAClC,SAAS,QAAQ,MAAM;EACvB,OAAO,iBAAiB,UAAU,IAAI;EAItC,IAAI,oBAAoB,OAAO,yBAAyB,aAAa;GACnE,KAAK,IAAI,qBAAqB,IAAI;GAClC,GAAG,QAAQ,MAAM;EACnB;EACA,IAAI,cACF,OAAO,iBAAiB,UAAU,UAAU;GAC1C,SAAS;GACT,SAAS;EACX,CAAC;EAEH,IAAI,gBAAgB;GAClB,OAAO,iBAAiB,eAAe,eAAe,EAAE,SAAS,KAAK,CAAC;GACvE,OAAO,iBAAiB,aAAa,aAAa,EAAE,SAAS,KAAK,CAAC;EACrE;CACF;CAEA,MAAM,sBAA4B;EAChC,IAAI,UAAU;GACZ,SAAS,WAAW;GACpB,WAAW;EACb;EACA,IAAI,IAAI;GACN,GAAG,WAAW;GACd,KAAK;EACP;EACA,OAAO,oBAAoB,UAAU,IAAI;EACzC,OAAO,oBAAoB,UAAU,UAAU,EAC7C,SAAS,KACX,CAAyB;EACzB,OAAO,oBAAoB,eAAe,eAAe,EACvD,SAAS,KACX,CAAyB;EACzB,OAAO,oBAAoB,aAAa,aAAa,EACnD,SAAS,KACX,CAAyB;EACzB,cAAc;CAChB;CAEA,MAAM,cAAoB;EACxB,gBAAgB;EAChB,IAAI,SAAS;GACX,eAAe;GACf,gBAAgB,iBAAiB;GACjC,QAAQ,aAAa;EACvB,OAAO;GACL,cAAc;GACd,MAAM,SAAoB,EAAE,SAAS,MAAM;GAC3C,gBAAgB;GAChB,QAAQ,MAAM;EAChB;CACF;CAEA,MAAM;CAEN,OAAO;EACL,OAAO,MAAoC;GACzC,IAAI,UAAU;GACd,UAAU,KAAK;GACf,UAAU,KAAK;GACf,MAAM;EACR;EACA,UAAgB;GACd,IAAI,UAAU;GACd,WAAW;GACX,cAAc;EAChB;EACA,MAAM,YAA2B;GAM/B,IAAI,YAAY,CAAC,gBAAgB;GACjC,IAAI,eAAe,KAAA,KAAa,aAAa,GAAG;IAC9C,MAAM,OAAO,YAAY,IAAI,IAAI;IAEjC,mBAAmB,qBAAqB,OAAO,OAAO,KAAK,IAAI,kBAAkB,IAAI;GACvF;GACA,aAAa;EACf;CACF;AACF;;;ACjdA,SAAgB,yBACd,MAC2B;CAC3B,MAAM,eACJ,KAAK,kBAAkB,OAA2B,sBAAsB,EAAE;CAC5E,MAAM,cAAc,KAAK,iBAAiB,OAAqB,qBAAqB,EAAE;CACtF,MAAM,iBACJ,OAAO,KAAK,eAAe,aAAa,KAAK,mBAA2B,KAAK;CAE/E,MAAM,wBAAQ,IAAI,IAAgC;CAClD,IAAI,QAAQ;CACZ,IAAI,UAAyB;CAC7B,IAAI,QAAQ;CACZ,IAAI,WAAW;CAEf,SAAS,WAAiB;EACxB,IAAI,YAAY,YAAY,MAAM;EAClC,UAAU,aAAa,KAAK;CAC9B;CAEA,SAAS,QAAc;EACrB,UAAU;EAGV,IAAI,YAAY,CAAC,OAAO;EACxB,QAAQ;EACR,KAAK,QAAQ;GACX,YAAY,eAAe;GAC3B,OAAO;GACP,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC;EAC3B,CAAC;CACH;CAEA,OAAO;EACL,IAAI,MAAgC;GAClC,IAAI,UAAU;GACd,MAAM,IAAI,KAAK,QAAQ,IAAI;GAC3B,QAAQ;GACR,SAAS;EACX;EACA,OAAO,QAAsB;GAC3B,IAAI,UAAU;GAEd,IAAI,CAAC,MAAM,OAAO,MAAM,GAAG;GAC3B,QAAQ;GACR,SAAS;EACX;EACA,UAAgB;GACd,IAAI,UAAU;GACd,WAAW;GACX,IAAI,YAAY,MAAM;IACpB,YAAY,OAAO;IACnB,UAAU;GACZ;EACF;CACF;AACF;;;;;;;;;;;;;;;;;ACdA,SAAgB,uBAAuB,MAErC;CACD,MAAM,cACL,KAAK,iBACH,OAAmC,SAAS,cAAc,EAAE;CAC/D,MAAM,eAAe,KAAK,gBAAgB;CAK1C,IAAI,gBAAgB;CAIpB,MAAM,YAAY,yBAAoC;EACrD,kBAAkB;EAClB,UAAS,aAAY,KAAK,OAAO,aAAa,QAAQ;EACtD,cAAc,KAAK;EACnB,aAAa,KAAK;CACnB,CAAC;CAQD,MAAM,2BAAW,IAAI,IAA4D;CACjF,IAAI,WAAW;CAKf,MAAM,QAAQ,KAAK,OAAO,aAAa,UAAU;EAChD,IAAI,UAAU;EACd,IAAI,MAAM,aAAa,eAAe,gBAAgB,MAAM;EAC5D,MAAM,WAAW,SAAS,IAAI,MAAM,MAAM;EAG1C,IAAI,YAAY,SAAS,UAAU,MAAM,WAAW;EAKpD,IAAI,UAKH,IAAI;GACH,SAAS,OAAO,QAAQ;EACzB,UACQ;GACP,SAAS,OAAO,MAAM,MAAM;GAC5B,UAAU,OAAO,MAAM,MAAM;EAC9B;EAED,MAAM,KAAK,YAAY,MAAM,MAAM;EACnC,IAAI,CAAC,IAAI;EACT,MAAM,QAAQ,MAAM;EACpB,MAAM,SAAS,aAAa,IAAI;GAC/B,SAAS;GACT,cAAc;GACd,gBAAgB;GAChB,kBAAkB;GAKlB,UAAU,cAAc;IACvB,UAAU,IAAI;KAAE,QAAQ,MAAM;KAAQ;KAAW,OAAO;KAAG,OAAO,EAAE,WAAW,MAAM;IAAE,CAAC;GACzF;EACD,CAAC;EACD,SAAS,IAAI,MAAM,QAAQ;GAAE;GAAO;EAAO,CAAC;CAC7C,CAAC;CAGD,KAAK,OAAO,UAAU;CAEtB,OAAO,EACN,UAAgB;EACf,IAAI,UAAU;EACd,WAAW;EACX,MAAM;EACN,KAAK,MAAM,EAAE,YAAY,SAAS,OAAO,GAAG,OAAO,QAAQ;EAC3D,SAAS,MAAM;EAEf,UAAU,QAAQ;CACnB,EACD;AACD;;;;ACvGA,SAAgB,iBAGd,SAAuD;CACxD,MAAM,aAAa,SAAS,cAAA;CAE5B,SAAS,YAAwB;EAChC,MAAM,SAAS,qBAAqB,UAAU;EAC9C,IAAI,WAAW,KAAA,GACd,MAAM,IAAI,wBACT,+CAA+C,WAAW,mDAC3D;EAED,OAAO;CACR;CAEA,OAAO;EACN,MAAM,QAAuB;GAC5B,MAAM,SAAS,UAAU;GACzB,MAAM,cAAc,WAAW,OAAO,OAAO;GAC7C,MAAM,aAAa,WAAW,uBAAuB;GACrD,IAAI,gBAAgB,YACnB,MAAM,IAAI,wBACT,iDAAiD,OAAO,OAAO,OAAO,EAAE,0BAA0B,WAAW,QAAQ,wBAAwB,EAC9I;EAEF;EAEA,MAAM,OACL,MACA,GAAG,MACmC;GAEtC,MAAM,WAAW,MADF,UACQ,EAAO,OAAO;IACpC,MAAM;IACN;IACM;GACP,CAAC;GACD,IAAI,SAAS,IACZ,OAAO,SAAS;GAEjB,MAAM,EAAE,YAAY,SAAS,SAAS,SAAS;GAC/C,MAAM,IAAI,gBAAgB,YAAY,SAAS,IAAI;EACpD;EAEA,GACC,OACA,UACa;GACb,MAAM,SAAS,UAAU;GACzB,MAAM,WAAW,QAA6B;IAC7C,IAAI,IAAI,SAAS,MAAM,MACtB,SAAS,IAAI,OAAmD;GAElE;GACA,MAAM,cAAc,OAAO,QAAQ,OAAO;GAC1C,IAAI,WAAW;GACf,OAAO,EACN,eAAe;IACd,IAAI,UAAU;IACd,WAAW;IACX,YAAY;GACb,EACD;EACD;CACD;AACD;AAEA,SAAgB,qBACf,aAAqB,uBACI;CAEzB,MAAM,SAAS,WAAE;CACjB,IAAI,WAAW,KAAA,KAAa,WAAW,MAAM,OAAO,KAAA;CACpD,OAAO;AACR;AAEA,SAAS,WAAW,SAA0B;CAC7C,IAAI,OAAO,YAAY,UAAU,OAAO;CACxC,MAAM,OAAO,QAAQ,MAAM,GAAG,EAAE,MAAM;CACtC,MAAM,IAAI,OAAO,SAAS,MAAM,EAAE;CAClC,OAAO,OAAO,MAAM,CAAC,IAAI,MAAa;AACvC"}
@@ -8,10 +8,14 @@
8
8
  * contract doc-block in `dock-view.test.tsx`.
9
9
  *
10
10
  * This file lives under `src/dock-react/` (NOT `src/layout/`), so importing
11
- * react here does not violate the pure-TS layout boundary.
11
+ * react here does not violate the pure-TS layout boundary. The stateful child
12
+ * views (`SplitView`, `GroupView`) and the body renderer live in sibling
13
+ * modules; this file owns the orchestration (`renderNode`) + the shared
14
+ * `RenderContext`.
12
15
  */
13
16
  import { type ReactNode } from 'react';
14
- import type { LayoutModel, PanelRegistry, SizeConstraint } from '../layout/index.js';
17
+ import type { LayoutModel, LayoutNode, PanelRegistry } from '../layout/index.js';
18
+ import { type DropZone } from './drag-redock.js';
15
19
  export interface DockViewProps {
16
20
  model: LayoutModel;
17
21
  registry: PanelRegistry;
@@ -50,22 +54,42 @@ export interface DockViewProps {
50
54
  * Returns 0 when used outside a `<DockView>`.
51
55
  */
52
56
  export declare function useDockLayoutEpoch(): number;
53
- /**
54
- * Compute the `defaultSize` percentage for each FLEXIBLE child, keyed by its
55
- * ORIGINAL child index. Px-sized children (a non-null `constraint` — `fixedPx`
56
- * locked OR `minPx` floored) are EXCLUDED from the pool: their size is px, not a
57
- * weight, so it must never pollute the flexible siblings' normalization (FIX E1).
58
- * `constraints[i]` non-null ⇒ child i is px-sized and absent from the returned map.
59
- */
60
- export declare function computeFlexiblePercentages(sizes: readonly number[], constraints: readonly (SizeConstraint | null)[] | undefined): Map<number, number>;
61
57
  export declare function DockView(props: DockViewProps): ReactNode;
62
- /** Are two panelId→percentage maps equivalent within `epsilon` percentage
63
- * points? Both maps must cover EXACTLY the `ids` key set a missing key OR an
64
- * extra key (a key present in `a`/`b` but absent from `ids`) counts as NOT
65
- * equivalent so the sync is not falsely suppressed. A non-finite value
66
- * (NaN/±Infinity) is likewise NOT equivalent — `typeof NaN === 'number'` and
67
- * `Math.abs(NaN - x) > eps` is `false`, so without the `Number.isFinite` guard a
68
- * NaN would slip through as "equivalent" and wrongly suppress a legitimate
69
- * sync/write-back (N1). EXPORTED (pure) for direct unit coverage. */
70
- export declare function layoutsEquivalent(a: Record<string, number>, b: Record<string, number>, ids: readonly string[], epsilon?: number): boolean;
58
+ /** The shared per-render context threaded from `DockView` down through
59
+ * `renderNode` into every `SplitView`/`GroupView`. Exported so the sibling view
60
+ * modules type their `ctx` against the single source. */
61
+ export interface RenderContext {
62
+ registry: PanelRegistry;
63
+ renderDomPanel: (panelId: string, opts: {
64
+ active: boolean;
65
+ }) => ReactNode;
66
+ bindNativeSlot: (panelId: string, el: HTMLElement | null) => void;
67
+ onActivate: (groupId: string, panelId: string) => void;
68
+ onActiveTabClick: ((panelId: string) => void) | undefined;
69
+ onApplyLayout: (splitId: string, weights: number[]) => void;
70
+ onRedock: (groupId: string, activePanelId: string, draggedPanelId: string, zone: DropZone,
71
+ /** Pointer-derived insertion index for a within-group reorder
72
+ * (`dropPolicy:'reorder-only'`). Omitted by the imperative seam, which
73
+ * falls back to the active anchor's current index. */
74
+ reorderIndex?: number) => void;
75
+ onClose: (panelId: string) => void;
76
+ /** False when the whole tree has a single panel — suppresses every close
77
+ * button so the layout can't be emptied (closePanel would no-op anyway). */
78
+ canClose: boolean;
79
+ /** True iff `panelId` is present in the CURRENT layout tree (M2). Registry
80
+ * membership is NOT enough: a panel can be registered but absent from the tree
81
+ * (closed out / never docked), and driving a re-dock on it makes the mutation
82
+ * layer throw `panel not found`. A drop carrying such an id must be a no-op. */
83
+ isPanelInTree: (panelId: string) => boolean;
84
+ /** See {@link DockViewProps.suppressReorderOnlyDropIndicator}. */
85
+ suppressReorderOnlyDropIndicator: boolean;
86
+ /** The in-flight tab-drag panel id, INSTANCE-scoped to the owning DockView
87
+ * (set on `dragstart`, cleared on `dragend`/`drop`). The drop-indicator path
88
+ * reads it during `dragover`, where the DataTransfer value is unreadable. */
89
+ activeDragPanelId: {
90
+ current: string | null;
91
+ };
92
+ }
93
+ declare function renderNode(node: LayoutNode, ctx: RenderContext): ReactNode;
94
+ export { renderNode };
71
95
  //# sourceMappingURL=dock-view.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dock-view.d.ts","sourceRoot":"","sources":["../../src/dock-react/dock-view.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EASN,KAAK,SAAS,EACd,MAAM,OAAO,CAAA;AAWd,OAAO,KAAK,EACX,WAAW,EAGX,aAAa,EACb,cAAc,EAGd,MAAM,oBAAoB,CAAA;AAwB3B,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,aAAa,CAAA;IACvB;;;;;;;OAOG;IACH,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,SAAS,CAAA;IACzE,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACjE;;;;;;;OAOG;IACH,gCAAgC,CAAC,EAAE,OAAO,CAAA;IAC1C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAqBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAaD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CACzC,KAAK,EAAE,SAAS,MAAM,EAAE,EACxB,WAAW,EAAE,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,EAAE,GAAG,SAAS,GACzD,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAUrB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAwJxD;AA6KD;;;;;;;qEAOqE;AACrE,wBAAgB,iBAAiB,CAChC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACzB,GAAG,EAAE,SAAS,MAAM,EAAE,EACtB,OAAO,GAAE,MAAuB,GAC9B,OAAO,CAaT"}
1
+ {"version":3,"file":"dock-view.d.ts","sourceRoot":"","sources":["../../src/dock-react/dock-view.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EAON,KAAK,SAAS,EACd,MAAM,OAAO,CAAA;AAYd,OAAO,KAAK,EACX,WAAW,EACX,UAAU,EAEV,aAAa,EAGb,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EAIN,KAAK,QAAQ,EACb,MAAM,kBAAkB,CAAA;AAIzB,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,WAAW,CAAA;IAClB,QAAQ,EAAE,aAAa,CAAA;IACvB;;;;;;;OAOG;IACH,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,SAAS,CAAA;IACzE,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACjE;;;;;;;OAOG;IACH,gCAAgC,CAAC,EAAE,OAAO,CAAA;IAC1C;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAC5C;AAqBD;;;;;GAKG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAyCD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,aAAa,GAAG,SAAS,CAkKxD;AAED;;yDAEyD;AACzD,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,aAAa,CAAA;IACvB,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,SAAS,CAAA;IACzE,cAAc,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;IACjE,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACtD,gBAAgB,EAAE,CAAC,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;IACzD,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,IAAI,CAAA;IAC3D,QAAQ,EAAE,CACT,OAAO,EAAE,MAAM,EACf,aAAa,EAAE,MAAM,EACrB,cAAc,EAAE,MAAM,EACtB,IAAI,EAAE,QAAQ;IACd;;0DAEsD;IACtD,YAAY,CAAC,EAAE,MAAM,KACjB,IAAI,CAAA;IACT,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC;gFAC4E;IAC5E,QAAQ,EAAE,OAAO,CAAA;IACjB;;;oFAGgF;IAChF,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAA;IAC3C,kEAAkE;IAClE,gCAAgC,EAAE,OAAO,CAAA;IACzC;;iFAE6E;IAC7E,iBAAiB,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC7C;AAED,iBAAS,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,aAAa,GAAG,SAAS,CAInE;AAiBD,OAAO,EAAE,UAAU,EAAE,CAAA"}
@@ -107,4 +107,22 @@ export declare function computeReorderIndex(tabRects: readonly {
107
107
  left: number;
108
108
  width: number;
109
109
  }[], pointerX: number): number;
110
+ /**
111
+ * Translate a tab strip's VISIBLE-tab drop index into the insertion index
112
+ * `movePanel`'s same-group reorder expects.
113
+ *
114
+ * `computeReorderIndex` reports how many VISIBLE-tab midpoints the pointer has
115
+ * passed (0..`visibleTabIds.length`), COUNTING the dragged tab's own midpoint and
116
+ * measured over rects that OMIT `hideTab` panels. `movePanel` inserts into
117
+ * `panels.filter(p => p !== dragged)` — a different coordinate space. Two shifts
118
+ * reconcile them:
119
+ *
120
+ * 1. Once the pointer passes the dragged tab's OWN midpoint the strip index has
121
+ * counted the dragged slot, so drop it back out (−1) to get the insertion
122
+ * slot among the OTHER visible tabs.
123
+ * 2. Map that visible slot onto the full `panels` order (which may carry hidden
124
+ * tabs the strip never measured): insert before whichever visible tab now
125
+ * occupies the slot, or append when the slot is past the last visible tab.
126
+ */
127
+ export declare function resolveReorderInsertIndex(panels: readonly string[], visibleTabIds: readonly string[], draggedPanelId: string, stripInsertIndex: number): number;
110
128
  //# sourceMappingURL=drag-redock.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"drag-redock.d.ts","sourceRoot":"","sources":["../../src/dock-react/drag-redock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAErE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACtD;IACD,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,KAAK,GAAG,QAAQ,CAAA;IACrB,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACvC,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/B,YAAY,SAAO,GACjB,QAAQ,CAmEV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC1C,cAAc,CAOhB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC5C,IAAI,EAAE,QAAQ,GACZ,OAAO,CAMT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EACpD,QAAQ,EAAE,MAAM,GACd,MAAM,CASR"}
1
+ {"version":3,"file":"drag-redock.d.ts","sourceRoot":"","sources":["../../src/dock-react/drag-redock.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAErE;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GACvB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACtD;IACD,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,KAAK,GAAG,QAAQ,CAAA;IACrB,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAA;IACxB,UAAU,EAAE,MAAM,CAAA;CAClB,CAAA;AA6FF;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC9B,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACvC,KAAK,EAAE;IAAE,CAAC,EAAE,MAAM,CAAC;IAAC,CAAC,EAAE,MAAM,CAAA;CAAE,EAC/B,YAAY,SAAO,GACjB,QAAQ,CAYV;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CACjC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC1C,cAAc,CAOhB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,YAAY,CAC3B,OAAO,EAAE,MAAM,EACf,cAAc,EAAE,MAAM,GAAG,SAAS,EAClC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAC5C,IAAI,EAAE,QAAQ,GACZ,OAAO,CAMT;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,EACpD,QAAQ,EAAE,MAAM,GACd,MAAM,CASR;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,yBAAyB,CACxC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,aAAa,EAAE,SAAS,MAAM,EAAE,EAChC,cAAc,EAAE,MAAM,EACtB,gBAAgB,EAAE,MAAM,GACtB,MAAM,CAcR"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * `GroupView` — one tab GROUP: the draggable tab strip, the active body, the
3
+ * imperative `__deckHandleDrop` seam, and a geometry-driven drop indicator while
4
+ * a drag hovers. Owns all tab drag/drop handlers.
5
+ */
6
+ import { type ReactNode } from 'react';
7
+ import type { TabGroupNode } from '../layout/index.js';
8
+ import type { RenderContext } from './dock-view.js';
9
+ export interface GroupViewProps {
10
+ node: TabGroupNode;
11
+ ctx: RenderContext;
12
+ }
13
+ /**
14
+ * One tab GROUP: tab strip (draggable tabs) + active body + the imperative
15
+ * `__deckHandleDrop` seam + a geometry-driven drop indicator while a drag hovers.
16
+ */
17
+ export declare function GroupView(props: GroupViewProps): ReactNode;
18
+ //# sourceMappingURL=group-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"group-view.d.ts","sourceRoot":"","sources":["../../src/dock-react/group-view.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAoC,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAGtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAiBnD,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,YAAY,CAAA;IAClB,GAAG,EAAE,aAAa,CAAA;CAClB;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CAiQ1D"}
@@ -5,8 +5,9 @@
5
5
  * `PanelRegistry` (panelId -> descriptor). See `dock-view.test.tsx` for the
6
6
  * `data-*` contract.
7
7
  */
8
- export { DockView, computeFlexiblePercentages, layoutsEquivalent, useDockLayoutEpoch } from './dock-view.js';
8
+ export { DockView, useDockLayoutEpoch } from './dock-view.js';
9
9
  export type { DockViewProps } from './dock-view.js';
10
- export { computeDropZone, computeReorderIndex, dropZoneToMutation, isNoopRedock } from './drag-redock.js';
10
+ export { computeFlexiblePercentages, layoutsEquivalent } from './split-sizing.js';
11
+ export { computeDropZone, computeReorderIndex, dropZoneToMutation, isNoopRedock, resolveReorderInsertIndex } from './drag-redock.js';
11
12
  export type { DropZone, RedockMutation } from './drag-redock.js';
12
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dock-react/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAC5G,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAInD,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACzG,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dock-react/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,QAAQ,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAC7D,YAAY,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACnD,OAAO,EAAE,0BAA0B,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAIjF,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AACpI,YAAY,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}