@dimina-kit/view-anchor 0.1.0-dev.20260610082053
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +87 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/measure-loop.d.ts +43 -0
- package/dist/measure-loop.d.ts.map +1 -0
- package/dist/measure-loop.js +49 -0
- package/dist/react.d.ts +38 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +145 -0
- package/dist/size-advertiser.d.ts +20 -0
- package/dist/size-advertiser.d.ts.map +1 -0
- package/dist/size-advertiser.js +82 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/view-anchor.d.ts +111 -0
- package/dist/view-anchor.d.ts.map +1 -0
- package/dist/view-anchor.js +396 -0
- package/docs/anchor-3d.html +602 -0
- package/docs/bidirectional-design.md +151 -0
- package/docs/mechanism.mdx +96 -0
- package/package.json +76 -0
- package/src/follow-geometry-press-drag.fix.test.ts +255 -0
- package/src/index.ts +39 -0
- package/src/measure-loop.ts +87 -0
- package/src/placement.test.ts +338 -0
- package/src/react.test.tsx +566 -0
- package/src/react.ts +171 -0
- package/src/size-advertiser.extras.test.ts +294 -0
- package/src/size-advertiser.test.ts +444 -0
- package/src/size-advertiser.ts +94 -0
- package/src/types.ts +123 -0
- package/src/view-anchor.test.ts +1184 -0
- package/src/view-anchor.ts +486 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EchoTechFE and dimina-kit contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @dimina-kit/view-anchor
|
|
2
|
+
|
|
3
|
+
让一块主进程原生视图(Electron `WebContentsView`)始终贴住某个 DOM 元素的屏幕矩形,一对一绑定。
|
|
4
|
+
|
|
5
|
+
DOM 布局库(flexbox、dockview、react-resizable-panels……)都不管渲染进程与主进程之间那道边界,它们移动的永远只是 DOM 节点。`view-anchor` 就是跨过这道边界的桥:它测量目标元素的 `getBoundingClientRect()`,把得到的矩形交给一个 `publish` 回调(由你接上 IPC → `setBounds`),并在元素发生位移或缩放时重新发布。
|
|
6
|
+
|
|
7
|
+
核心不依赖 React、Electron 或任何宿主布局引擎。涉及 React 的代码只在适配层。
|
|
8
|
+
|
|
9
|
+
## 安装
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pnpm add @dimina-kit/view-anchor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 快速上手
|
|
16
|
+
|
|
17
|
+
命令式核心:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createViewAnchor } from '@dimina-kit/view-anchor'
|
|
21
|
+
|
|
22
|
+
const handle = createViewAnchor(target, {
|
|
23
|
+
present: true, // 挂载原生视图
|
|
24
|
+
publish: (bounds) => { ... }, // 接收实时矩形;由它负责 IPC → setBounds
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
handle.update({ present, publish }) // 应用新选项(会立即重新发布)
|
|
28
|
+
handle.dispose() // 停止观察;此后不再发布
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
React 适配层:
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useViewAnchor } from '@dimina-kit/view-anchor'
|
|
35
|
+
|
|
36
|
+
function DebugPanel({ visible }: { visible: boolean }) {
|
|
37
|
+
const ref = useViewAnchor({
|
|
38
|
+
present: visible,
|
|
39
|
+
publish: publishSimulatorDevtoolsBounds,
|
|
40
|
+
})
|
|
41
|
+
// 原生视图会跟随这个占位 div;隐藏面板
|
|
42
|
+
// (visible=false 或卸载)会让它收起,但不销毁。
|
|
43
|
+
return <div ref={ref} className="h-full w-full" />
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 工作原理(一句话版)
|
|
48
|
+
|
|
49
|
+
- `present: true`:立即发布测量矩形,之后在每次 `ResizeObserver` 触发、窗口 `resize` 时**同步**重新发布(不走 RAF——原生 overlay 的 setBounds 本就慢一帧,RAF 再叠一帧会拖尾)。与上次逐字段相同的矩形会被去重跳过。x/y 取整但**允许负**(滚出边缘时跟随),width/height 钳到 ≥0。
|
|
50
|
+
- `present: false`:发布一次 `{0,0,0,0}` 并停止观察(已有的观察器一并拆除)。宿主把「面积为零」读作「摘除子视图,但保留其 `WebContents` 存活」,即收起而非销毁。
|
|
51
|
+
- `dispose()` 之后不再发布任何内容,也不会补发一帧零矩形(那是调用方的责任;React 适配层已替你在元素消失时处理好)。
|
|
52
|
+
|
|
53
|
+
## 反向:`createSizeAdvertiser`(内容尺寸回流)
|
|
54
|
+
|
|
55
|
+
正向让原生视图跟随 DOM;反向让 DOM 占位跟随内容。当一块 `WebContentsView` 的尺寸由它**自己的内容**主导时(例如交给下游控制的 toolbar),在**下游视图自己的渲染进程**里跑:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { createSizeAdvertiser } from '@dimina-kit/view-anchor'
|
|
59
|
+
|
|
60
|
+
const handle = createSizeAdvertiser(contentWrapper, {
|
|
61
|
+
axis: 'block', // 这个 advertiser 只主导一条轴(block=高 / inline=宽),不可变
|
|
62
|
+
publish: (size) => { ... }, // 接收 { axis, extent },由它负责 IPC → 宿主
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
handle.update(publish) // 换 publish(IPC 通道),并立即把当前尺寸发给它
|
|
66
|
+
handle.dispose() // 停止观察;此后不再上报
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
它从 `ResizeObserver` 的 border-box 取主导轴尺寸(不调 `getBoundingClientRect`、不强制 reflow),`Math.round` + 钳零,复用与正向同一套 RAF 合并 + 去重。宿主收到尺寸后调整占位、再由正向把视图贴上去——两个单向原语经占位 div 串成一座双向桥。**注意 footgun**:`target` 必须在主导轴上 shrink-to-fit(其尺寸不能被宿主灌入的视图尺寸反向决定),否则跨进程环不收敛。详见 [`docs/bidirectional-design.md`](./docs/bidirectional-design.md)。
|
|
70
|
+
|
|
71
|
+
## 文档
|
|
72
|
+
|
|
73
|
+
- [`docs/mechanism.mdx`](./docs/mechanism.mdx) —— 正向完整机制:RAF 与陈旧帧安全性、`present` / 零矩形 / 卸载契约、React 18 StrictMode 生命周期。内嵌可交互 3D 演示 [`docs/anchor-3d.html`](./docs/anchor-3d.html)。
|
|
74
|
+
- [`docs/bidirectional-design.md`](./docs/bidirectional-design.md) —— 双向化设计:共享 `createMeasureLoop` 核心、单轴所有权与收敛性、信任边界、以及两个原语如何「锚」到一起。
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
| 导出 | 类型 | 作用 |
|
|
79
|
+
|---|---|---|
|
|
80
|
+
| `createViewAnchor(target, opts)` | 函数 | 正向命令式核心,返回 `{ update, dispose }`。不依赖 React、不依赖 Electron。 |
|
|
81
|
+
| `useViewAnchor(opts)` | Hook | 正向 React 适配层,返回一个挂到占位元素上的 ref 回调。 |
|
|
82
|
+
| `createSizeAdvertiser(target, opts)` | 函数 | 反向命令式核心,返回 `{ update, dispose }`。下游量内容尺寸回流给宿主。 |
|
|
83
|
+
| `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
|
|
84
|
+
| `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | 正向核心的选项与句柄形状。 |
|
|
85
|
+
| `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | 正向适配层的选项与 ref 回调形状。 |
|
|
86
|
+
| `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向的轴(`'block'\|'inline'`)与帧载荷 `{ axis, extent }`。 |
|
|
87
|
+
| `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | 反向核心的选项与句柄形状。 |
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* view-anchor — engine-agnostic primitive that keeps a main-process native
|
|
3
|
+
* view (Electron `WebContentsView`) aligned to a DOM element's geometry.
|
|
4
|
+
*
|
|
5
|
+
* Public surface:
|
|
6
|
+
* - `createViewAnchor` — forward: DOM rect → native view bounds.
|
|
7
|
+
* - `useViewAnchor` — React adapter returning a ref callback.
|
|
8
|
+
* - `createSizeAdvertiser`— reverse: downstream content size → host.
|
|
9
|
+
* - `Bounds` / `AdvertisedSize` / option + handle types.
|
|
10
|
+
*
|
|
11
|
+
* Self-contained on purpose: the only runtime deps are `react` (adapter
|
|
12
|
+
* only) and browser APIs (`ResizeObserver` / `requestAnimationFrame` /
|
|
13
|
+
* `getBoundingClientRect`). See the design notes and the interactive 3D
|
|
14
|
+
* walkthrough in `docs/` (`mechanism.mdx` / `anchor-3d.html`).
|
|
15
|
+
*/
|
|
16
|
+
export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
|
|
17
|
+
export type { PlacementAnchorOptions, PlacementAnchorHandle, } from './view-anchor.js';
|
|
18
|
+
export type { Bounds, Placement, ViewAnchorOptions, ViewAnchorHandle, } from './types.js';
|
|
19
|
+
export { useViewAnchor } from './react.js';
|
|
20
|
+
export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js';
|
|
21
|
+
export { createSizeAdvertiser } from './size-advertiser.js';
|
|
22
|
+
export type { AdvertisedAxis, AdvertisedSize, SizeAdvertiserOptions, SizeAdvertiserHandle, } from './types.js';
|
|
23
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kBAAkB,CAAA;AACzB,YAAY,EACV,MAAM,EACN,SAAS,EACT,iBAAiB,EACjB,gBAAgB,GACjB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAC1C,YAAY,EAAE,oBAAoB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAC3D,YAAY,EACV,cAAc,EACd,cAAc,EACd,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,YAAY,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* view-anchor — engine-agnostic primitive that keeps a main-process native
|
|
3
|
+
* view (Electron `WebContentsView`) aligned to a DOM element's geometry.
|
|
4
|
+
*
|
|
5
|
+
* Public surface:
|
|
6
|
+
* - `createViewAnchor` — forward: DOM rect → native view bounds.
|
|
7
|
+
* - `useViewAnchor` — React adapter returning a ref callback.
|
|
8
|
+
* - `createSizeAdvertiser`— reverse: downstream content size → host.
|
|
9
|
+
* - `Bounds` / `AdvertisedSize` / option + handle types.
|
|
10
|
+
*
|
|
11
|
+
* Self-contained on purpose: the only runtime deps are `react` (adapter
|
|
12
|
+
* only) and browser APIs (`ResizeObserver` / `requestAnimationFrame` /
|
|
13
|
+
* `getBoundingClientRect`). See the design notes and the interactive 3D
|
|
14
|
+
* walkthrough in `docs/` (`mechanism.mdx` / `anchor-3d.html`).
|
|
15
|
+
*/
|
|
16
|
+
export { createViewAnchor, measurePlacement, createPlacementAnchor, } from './view-anchor.js';
|
|
17
|
+
export { useViewAnchor } from './react.js';
|
|
18
|
+
export { createSizeAdvertiser } from './size-advertiser.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal — the RAF-coalesced measure/dedupe/dispose engine behind the REVERSE
|
|
3
|
+
* primitive `createSizeAdvertiser`.
|
|
4
|
+
*
|
|
5
|
+
* The forward `createViewAnchor` deliberately does NOT use this: it publishes
|
|
6
|
+
* SYNCHRONOUSLY (a native overlay's `setBounds` already lands a cross-process
|
|
7
|
+
* frame late, and a RAF stacked a second frame of visible trailing). The
|
|
8
|
+
* reverse direction is different — it is a cross-process FEEDBACK loop
|
|
9
|
+
* (advertise → host resizes the view → content re-measures → re-advertise), so
|
|
10
|
+
* the RAF's one-publish-per-frame coalescing is a useful damper. The two
|
|
11
|
+
* directions thus have different optimal emit timing; this engine serves only
|
|
12
|
+
* the reverse.
|
|
13
|
+
*
|
|
14
|
+
* NOT exported from the package: it is pure mechanism with no knowledge of
|
|
15
|
+
* direction, the DOM, `ResizeObserver`, or the structure of the value `T` it
|
|
16
|
+
* carries. The wrapping primitive injects `produce` / `same` / `sink` and
|
|
17
|
+
* drives the lifecycle.
|
|
18
|
+
*
|
|
19
|
+
* - `schedule()` — coalesce a burst of triggers into ONE RAF; the frame
|
|
20
|
+
* body re-`produce()`s, dedupes against the last emit (`same`), and `sink`s.
|
|
21
|
+
* Bails if inactive or disposed (stale-RAF safe).
|
|
22
|
+
* - `emitNow(v)` — explicit synchronous emit (create / update path). Always
|
|
23
|
+
* fires, bypassing the dedupe check, and refreshes the dedupe baseline.
|
|
24
|
+
* - `setActive` — gate the observer stream; a queued frame bails on `!active`.
|
|
25
|
+
* - `cancel` — drop any in-flight RAF.
|
|
26
|
+
* - `dispose` — cancel + go inert; after dispose nothing emits again.
|
|
27
|
+
*/
|
|
28
|
+
export interface MeasureLoop<T> {
|
|
29
|
+
schedule(): void;
|
|
30
|
+
emitNow(value: T): void;
|
|
31
|
+
setActive(on: boolean): void;
|
|
32
|
+
cancel(): void;
|
|
33
|
+
dispose(): void;
|
|
34
|
+
}
|
|
35
|
+
export declare function createMeasureLoop<T>(cfg: {
|
|
36
|
+
/** Produce the value to emit in the RAF body. Return `null` to decline the
|
|
37
|
+
* frame entirely (no dedupe, no sink, baseline untouched) — e.g. a
|
|
38
|
+
* non-finite or unavailable measurement. */
|
|
39
|
+
produce: () => T | null;
|
|
40
|
+
same: (a: T, b: T) => boolean;
|
|
41
|
+
sink: (value: T) => void;
|
|
42
|
+
}): MeasureLoop<T>;
|
|
43
|
+
//# sourceMappingURL=measure-loop.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"measure-loop.d.ts","sourceRoot":"","sources":["../src/measure-loop.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,QAAQ,IAAI,IAAI,CAAA;IAChB,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAA;IACvB,SAAS,CAAC,EAAE,EAAE,OAAO,GAAG,IAAI,CAAA;IAC5B,MAAM,IAAI,IAAI,CAAA;IACd,OAAO,IAAI,IAAI,CAAA;CAChB;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE;IACxC;;iDAE6C;IAC7C,OAAO,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;IACvB,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO,CAAA;IAC7B,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAA;CACzB,GAAG,WAAW,CAAC,CAAC,CAAC,CA4CjB"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function createMeasureLoop(cfg) {
|
|
2
|
+
const { produce, same, sink } = cfg;
|
|
3
|
+
let rafId = null;
|
|
4
|
+
let active = false;
|
|
5
|
+
let disposed = false;
|
|
6
|
+
let last = null;
|
|
7
|
+
const cancel = () => {
|
|
8
|
+
if (rafId !== null) {
|
|
9
|
+
cancelAnimationFrame(rafId);
|
|
10
|
+
rafId = null;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
schedule() {
|
|
15
|
+
if (disposed || !active || rafId !== null)
|
|
16
|
+
return;
|
|
17
|
+
rafId = requestAnimationFrame(() => {
|
|
18
|
+
rafId = null;
|
|
19
|
+
if (disposed || !active)
|
|
20
|
+
return;
|
|
21
|
+
const value = produce();
|
|
22
|
+
if (value === null)
|
|
23
|
+
return; // producer declined this frame
|
|
24
|
+
// last-value dedupe: a frame whose produced value equals the last one
|
|
25
|
+
// we emitted costs nothing (no IPC / setBounds).
|
|
26
|
+
if (last !== null && same(value, last))
|
|
27
|
+
return;
|
|
28
|
+
last = value;
|
|
29
|
+
sink(value);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
emitNow(value) {
|
|
33
|
+
if (disposed)
|
|
34
|
+
return;
|
|
35
|
+
last = value;
|
|
36
|
+
sink(value);
|
|
37
|
+
},
|
|
38
|
+
setActive(on) {
|
|
39
|
+
active = on;
|
|
40
|
+
},
|
|
41
|
+
cancel,
|
|
42
|
+
dispose() {
|
|
43
|
+
if (disposed)
|
|
44
|
+
return;
|
|
45
|
+
disposed = true;
|
|
46
|
+
cancel();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Bounds, ViewAnchorOptions } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* React adapter over the imperative `createViewAnchor` core.
|
|
4
|
+
*
|
|
5
|
+
* (React lint forces the `use` prefix on any hook returning a ref
|
|
6
|
+
* callback; the library's identity is still the `ViewAnchor` core — this is
|
|
7
|
+
* just the React binding.)
|
|
8
|
+
*/
|
|
9
|
+
export interface UseViewAnchorOptions extends ViewAnchorOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Non-DOM dependencies that move the target's rect and must force a
|
|
12
|
+
* re-publish (layout signature, project path, a tab toggle's
|
|
13
|
+
* `display:none`, …). A `ResizeObserver` covers pure geometry; `deps`
|
|
14
|
+
* covers state it cannot see. Keep the array length stable across
|
|
15
|
+
* renders (React effect-deps rule).
|
|
16
|
+
*/
|
|
17
|
+
deps?: ReadonlyArray<unknown>;
|
|
18
|
+
}
|
|
19
|
+
export type ViewAnchorRef = (el: HTMLElement | null) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Bind a native view's bounds to whichever DOM element the returned ref
|
|
22
|
+
* callback is attached to. On attach → `createViewAnchor(el, opts)`; on
|
|
23
|
+
* detach (`null`) → publish ZERO then `dispose()`; on `opts`/`deps` change →
|
|
24
|
+
* `update`; on unmount → publish ZERO then `dispose`.
|
|
25
|
+
*
|
|
26
|
+
* Why ZERO on disappearance (P1 fix): the anchor's follower is a *main-process*
|
|
27
|
+
* `WebContentsView`, not a DOM node. When the anchored element vanishes, core
|
|
28
|
+
* `dispose()` only stops observing — it deliberately never publishes again
|
|
29
|
+
* (its Contract 6/7). But the host only collapses the native view when it
|
|
30
|
+
* receives `{0,0,0,0}` (isHidden). In production the debug cell is *unmounted*
|
|
31
|
+
* (not `display:none`) when hidden, so the ref goes to `null` and the native
|
|
32
|
+
* DevTools view would otherwise stay frozen at its last bounds, floating on
|
|
33
|
+
* top and occluding content. So the adapter (not core) must emit one ZERO via
|
|
34
|
+
* the already-tested `update({ present:false })` path before disposing.
|
|
35
|
+
*/
|
|
36
|
+
export declare function useViewAnchor(opts: UseViewAnchorOptions): ViewAnchorRef;
|
|
37
|
+
export type { Bounds };
|
|
38
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAoB,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE7E;;;;;;GAMG;AACH,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAA;AAE5D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,oBAAoB,GAAG,aAAa,CAgIvE;AAED,YAAY,EAAE,MAAM,EAAE,CAAA"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { createViewAnchor } from './view-anchor.js';
|
|
3
|
+
/**
|
|
4
|
+
* Bind a native view's bounds to whichever DOM element the returned ref
|
|
5
|
+
* callback is attached to. On attach → `createViewAnchor(el, opts)`; on
|
|
6
|
+
* detach (`null`) → publish ZERO then `dispose()`; on `opts`/`deps` change →
|
|
7
|
+
* `update`; on unmount → publish ZERO then `dispose`.
|
|
8
|
+
*
|
|
9
|
+
* Why ZERO on disappearance (P1 fix): the anchor's follower is a *main-process*
|
|
10
|
+
* `WebContentsView`, not a DOM node. When the anchored element vanishes, core
|
|
11
|
+
* `dispose()` only stops observing — it deliberately never publishes again
|
|
12
|
+
* (its Contract 6/7). But the host only collapses the native view when it
|
|
13
|
+
* receives `{0,0,0,0}` (isHidden). In production the debug cell is *unmounted*
|
|
14
|
+
* (not `display:none`) when hidden, so the ref goes to `null` and the native
|
|
15
|
+
* DevTools view would otherwise stay frozen at its last bounds, floating on
|
|
16
|
+
* top and occluding content. So the adapter (not core) must emit one ZERO via
|
|
17
|
+
* the already-tested `update({ present:false })` path before disposing.
|
|
18
|
+
*/
|
|
19
|
+
export function useViewAnchor(opts) {
|
|
20
|
+
const handleRef = useRef(null);
|
|
21
|
+
const elRef = useRef(null);
|
|
22
|
+
// Latest opts, read by the stable ref callback when it creates the anchor.
|
|
23
|
+
// Synced render-synchronously (NOT in an effect): the ref callback reads
|
|
24
|
+
// `optsRef.current` during *commit* (when the element attaches), which runs
|
|
25
|
+
// before passive effects. An effect-synced ref would be one render stale at
|
|
26
|
+
// that point, so a hidden→shown remount (`present` flips false→true together
|
|
27
|
+
// with the element re-mounting, exactly what the debug cell does) would
|
|
28
|
+
// create the anchor with the old `present:false` and emit a spurious ZERO
|
|
29
|
+
// before the real rect. A render write keeps it current at commit, and is
|
|
30
|
+
// idempotent under StrictMode's double render.
|
|
31
|
+
const optsRef = useRef(opts);
|
|
32
|
+
// eslint-disable-next-line react-hooks/refs -- see above: must be current at commit, before effects run
|
|
33
|
+
optsRef.current = opts;
|
|
34
|
+
// Baseline for the re-apply effect's change detection. Declared here (before
|
|
35
|
+
// the ref callback) so the callback can re-seed it on (re)create.
|
|
36
|
+
const appliedRef = useRef([
|
|
37
|
+
opts.present,
|
|
38
|
+
opts.publish,
|
|
39
|
+
...(opts.deps ?? []),
|
|
40
|
+
]);
|
|
41
|
+
// Collapse the native view (publish ZERO) and tear the anchor down. Reuse
|
|
42
|
+
// the existing, tested `update({ present:false })` path: it synchronously
|
|
43
|
+
// publishes `{0,0,0,0}` and stops observing (core Contract 5), then
|
|
44
|
+
// `dispose()` makes the anchor inert. Idempotent via the `handleRef.current`
|
|
45
|
+
// null-check so the two callers below can never double-emit ZERO.
|
|
46
|
+
const collapseAndDispose = useRef(() => {
|
|
47
|
+
const handle = handleRef.current;
|
|
48
|
+
if (!handle)
|
|
49
|
+
return;
|
|
50
|
+
handle.update({ present: false, publish: optsRef.current.publish });
|
|
51
|
+
handle.dispose();
|
|
52
|
+
handleRef.current = null;
|
|
53
|
+
});
|
|
54
|
+
const ref = useCallback((el) => {
|
|
55
|
+
if (el === elRef.current)
|
|
56
|
+
return;
|
|
57
|
+
elRef.current = el;
|
|
58
|
+
if (handleRef.current) {
|
|
59
|
+
if (el) {
|
|
60
|
+
// Swapping to *another* live element: dispose the old anchor without a
|
|
61
|
+
// ZERO. The new element publishes its real rect immediately below, so
|
|
62
|
+
// a transient ZERO between the two would only cause a needless
|
|
63
|
+
// detach/re-attach flicker of the native view.
|
|
64
|
+
handleRef.current.dispose();
|
|
65
|
+
handleRef.current = null;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Element detached (ref → null): the anchor point is gone, so collapse
|
|
69
|
+
// the native view (one ZERO) before disposing.
|
|
70
|
+
collapseAndDispose.current();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (el) {
|
|
74
|
+
handleRef.current = createViewAnchor(el, {
|
|
75
|
+
present: optsRef.current.present,
|
|
76
|
+
publish: optsRef.current.publish,
|
|
77
|
+
});
|
|
78
|
+
// The anchor was just created at the current (present, publish, deps), so
|
|
79
|
+
// seed the re-apply baseline to match. Otherwise the post-commit re-apply
|
|
80
|
+
// effect would see this fresh state as a change and publish a second time
|
|
81
|
+
// — on a remount with a changed `present` that is a double-emit.
|
|
82
|
+
appliedRef.current = [
|
|
83
|
+
optsRef.current.present,
|
|
84
|
+
optsRef.current.publish,
|
|
85
|
+
...(optsRef.current.deps ?? []),
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
}, []);
|
|
89
|
+
// Re-apply on opts/deps change. We must `update` whenever the
|
|
90
|
+
// (present, publish, …deps) tuple actually changes, but NOT on the mount run
|
|
91
|
+
// (the ref callback already created the anchor and published once) and NOT on
|
|
92
|
+
// a StrictMode replay (dev double-fires this effect's setup with the *same*
|
|
93
|
+
// tuple — a blind `update` then re-publishes the mount rect a second time).
|
|
94
|
+
// So instead of guessing "is this the first run?", compare against the
|
|
95
|
+
// last-applied tuple and apply only on a genuine change. The tuple is seeded
|
|
96
|
+
// with the mount opts, so the mount run and its StrictMode replay both see
|
|
97
|
+
// "unchanged" and skip — idempotent by construction. `deps` keeps a stable
|
|
98
|
+
// length across renders (documented above), so positional compare is sound.
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
const next = [
|
|
101
|
+
opts.present,
|
|
102
|
+
opts.publish,
|
|
103
|
+
...(opts.deps ?? []),
|
|
104
|
+
];
|
|
105
|
+
const prev = appliedRef.current;
|
|
106
|
+
const changed = next.length !== prev.length || next.some((v, i) => !Object.is(v, prev[i]));
|
|
107
|
+
if (!changed)
|
|
108
|
+
return;
|
|
109
|
+
appliedRef.current = next;
|
|
110
|
+
handleRef.current?.update({ present: opts.present, publish: opts.publish });
|
|
111
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
112
|
+
}, [opts.present, opts.publish, ...(opts.deps ?? [])]);
|
|
113
|
+
// Collapse the native view + dispose on teardown.
|
|
114
|
+
//
|
|
115
|
+
// StrictMode-safe lifecycle: this effect's setup/cleanup is double-fired in
|
|
116
|
+
// dev (setup → cleanup → setup). The anchor itself is created/owned by the
|
|
117
|
+
// ref callback, which in React 18 fires exactly once on mount and once with
|
|
118
|
+
// `null` on a real detach — it is NOT replayed by StrictMode. So this effect
|
|
119
|
+
// must not destroy the ref-owned anchor on a *throwaway* unmount, or the
|
|
120
|
+
// re-setup would have nothing to restore.
|
|
121
|
+
//
|
|
122
|
+
// Discriminator: on a real teardown React detaches the element first
|
|
123
|
+
// (`ref(null)` → `elRef.current === null`, and that path already emitted the
|
|
124
|
+
// single ZERO + disposed); on a StrictMode throwaway unmount the element is
|
|
125
|
+
// still attached (`elRef.current !== null`, ref never fired `null`). So we
|
|
126
|
+
// only collapse here when the element is genuinely gone, and otherwise leave
|
|
127
|
+
// the live anchor intact for the immediate re-setup.
|
|
128
|
+
//
|
|
129
|
+
// The setup re-establishes the anchor if a prior cleanup ever tore it down
|
|
130
|
+
// while the element is still attached, keeping setup/cleanup symmetric.
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
const collapse = collapseAndDispose.current;
|
|
133
|
+
if (elRef.current && !handleRef.current) {
|
|
134
|
+
handleRef.current = createViewAnchor(elRef.current, {
|
|
135
|
+
present: optsRef.current.present,
|
|
136
|
+
publish: optsRef.current.publish,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return () => {
|
|
140
|
+
if (elRef.current === null)
|
|
141
|
+
collapse();
|
|
142
|
+
};
|
|
143
|
+
}, []);
|
|
144
|
+
return ref;
|
|
145
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SizeAdvertiserOptions, SizeAdvertiserHandle } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reverse of `createViewAnchor`: runs in a downstream WebContentsView's own
|
|
4
|
+
* renderer, measures the content's own size on ONE owned axis (from the
|
|
5
|
+
* `ResizeObserver` border-box — no `getBoundingClientRect`, no forced reflow),
|
|
6
|
+
* and advertises it via the injected `publish`. Shares the forward primitive's
|
|
7
|
+
* measure/coalesce/dedupe/dispose engine (`createMeasureLoop`).
|
|
8
|
+
*
|
|
9
|
+
* The extent is `Math.round`ed and clamped to `>= 0`; non-finite measurements
|
|
10
|
+
* drop the frame.
|
|
11
|
+
*
|
|
12
|
+
* FOOTGUN — `target` must be shrink-to-fit on the owned axis: its owned-axis
|
|
13
|
+
* size must NOT be driven by the host-applied view size, or the cross-process
|
|
14
|
+
* loop (advertise → host resizes view → remeasure) never converges (it
|
|
15
|
+
* oscillates or stays "stable but wrong"). Measuring `<body>`/`<html>` is the
|
|
16
|
+
* classic mistake — their size *is* the view size. See
|
|
17
|
+
* `docs/bidirectional-design.md` §4/§5.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createSizeAdvertiser(target: HTMLElement, opts: SizeAdvertiserOptions): SizeAdvertiserHandle;
|
|
20
|
+
//# sourceMappingURL=size-advertiser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAGnB;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CAkEtB"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { createMeasureLoop } from './measure-loop.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reverse of `createViewAnchor`: runs in a downstream WebContentsView's own
|
|
4
|
+
* renderer, measures the content's own size on ONE owned axis (from the
|
|
5
|
+
* `ResizeObserver` border-box — no `getBoundingClientRect`, no forced reflow),
|
|
6
|
+
* and advertises it via the injected `publish`. Shares the forward primitive's
|
|
7
|
+
* measure/coalesce/dedupe/dispose engine (`createMeasureLoop`).
|
|
8
|
+
*
|
|
9
|
+
* The extent is `Math.round`ed and clamped to `>= 0`; non-finite measurements
|
|
10
|
+
* drop the frame.
|
|
11
|
+
*
|
|
12
|
+
* FOOTGUN — `target` must be shrink-to-fit on the owned axis: its owned-axis
|
|
13
|
+
* size must NOT be driven by the host-applied view size, or the cross-process
|
|
14
|
+
* loop (advertise → host resizes view → remeasure) never converges (it
|
|
15
|
+
* oscillates or stays "stable but wrong"). Measuring `<body>`/`<html>` is the
|
|
16
|
+
* classic mistake — their size *is* the view size. See
|
|
17
|
+
* `docs/bidirectional-design.md` §4/§5.
|
|
18
|
+
*/
|
|
19
|
+
export function createSizeAdvertiser(target, opts) {
|
|
20
|
+
const axis = opts.axis; // immutable for the advertiser's life
|
|
21
|
+
let publish = opts.publish;
|
|
22
|
+
let observer = null;
|
|
23
|
+
let disposed = false;
|
|
24
|
+
// Latest border-box, stashed by the RO callback and read by `produce` in the
|
|
25
|
+
// RAF body (keep the entry out of the shared, DOM-agnostic loop).
|
|
26
|
+
let latest = null;
|
|
27
|
+
const produce = () => {
|
|
28
|
+
if (!latest)
|
|
29
|
+
return null;
|
|
30
|
+
const raw = axis === 'block' ? latest.blockSize : latest.inlineSize;
|
|
31
|
+
if (!Number.isFinite(raw))
|
|
32
|
+
return null;
|
|
33
|
+
return { axis, extent: Math.max(0, Math.round(raw)) };
|
|
34
|
+
};
|
|
35
|
+
const loop = createMeasureLoop({
|
|
36
|
+
produce,
|
|
37
|
+
same: (a, b) => a.extent === b.extent, // axis is constant
|
|
38
|
+
sink: (size) => publish(size),
|
|
39
|
+
});
|
|
40
|
+
const onResize = (entries) => {
|
|
41
|
+
const entry = entries[entries.length - 1];
|
|
42
|
+
if (entry) {
|
|
43
|
+
latest = entry.borderBoxSize?.[0] ?? entry.contentBoxSize?.[0] ?? latest;
|
|
44
|
+
}
|
|
45
|
+
loop.schedule();
|
|
46
|
+
};
|
|
47
|
+
// One cheap, once-per-advertiser guard for the textbook feedback-loop footgun.
|
|
48
|
+
const doc = target.ownerDocument;
|
|
49
|
+
if (target === doc.body || target === doc.documentElement) {
|
|
50
|
+
console.warn(`[view-anchor] size-advertiser: <${target === doc.body ? 'body' : 'html'}>'s ` +
|
|
51
|
+
`${axis} size is the host-given view size, not the content size — the ` +
|
|
52
|
+
`advertiser will never shrink to content. Measure a shrink-to-fit wrapper. ` +
|
|
53
|
+
`See bidirectional-design.md §4.`);
|
|
54
|
+
}
|
|
55
|
+
loop.setActive(true);
|
|
56
|
+
observer = new ResizeObserver(onResize);
|
|
57
|
+
observer.observe(target);
|
|
58
|
+
return {
|
|
59
|
+
update(nextPublish) {
|
|
60
|
+
if (disposed)
|
|
61
|
+
return;
|
|
62
|
+
publish = nextPublish;
|
|
63
|
+
// Re-advertise the current size to the new sink immediately (mirrors the
|
|
64
|
+
// forward anchor's re-publish on update) so the new channel is not left
|
|
65
|
+
// sizeless until the next ResizeObserver tick.
|
|
66
|
+
const cur = produce();
|
|
67
|
+
if (cur)
|
|
68
|
+
loop.emitNow(cur);
|
|
69
|
+
},
|
|
70
|
+
dispose() {
|
|
71
|
+
if (disposed)
|
|
72
|
+
return;
|
|
73
|
+
disposed = true;
|
|
74
|
+
loop.cancel();
|
|
75
|
+
if (observer) {
|
|
76
|
+
observer.disconnect();
|
|
77
|
+
observer = null;
|
|
78
|
+
}
|
|
79
|
+
loop.dispose();
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* view-anchor — sync a main-process native view's bounds to a DOM element.
|
|
3
|
+
*
|
|
4
|
+
* Self-contained, engine-agnostic primitive. It knows nothing about
|
|
5
|
+
* Electron (the `publish` callback owns the IPC → `setBounds`), nothing
|
|
6
|
+
* about React (the core is imperative; see `react.ts` for the adapter),
|
|
7
|
+
* and nothing about the host layout engine (the `target` element may come
|
|
8
|
+
* from our own `compile`/`FrameTree`, or — later — a dockview panel's
|
|
9
|
+
* `content.element`; the mechanism is identical).
|
|
10
|
+
*
|
|
11
|
+
* It is the modern, alive replacement for the archived
|
|
12
|
+
* `react-electron-browser-view`: the cross-process bridge that DOM layout
|
|
13
|
+
* libraries (dockview included) deliberately do not provide. dockview's
|
|
14
|
+
* internal `OverlayRenderContainer` does the same getBoundingClientRect →
|
|
15
|
+
* RAF → reposition dance, but its follower is a DOM node; ours is a native
|
|
16
|
+
* `WebContentsView` positioned via the injected `publish`.
|
|
17
|
+
*/
|
|
18
|
+
/** A screen-space rectangle, in CSS pixels. Structurally compatible with
|
|
19
|
+
* the host's `ViewBounds` so a publisher typed against either works. */
|
|
20
|
+
export interface Bounds {
|
|
21
|
+
x: number;
|
|
22
|
+
y: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Explicit visibility + geometry for a native view, replacing the legacy
|
|
28
|
+
* magic-`{0,0,0,0}` "hidden" convention (`present:false → ZERO bounds`).
|
|
29
|
+
*
|
|
30
|
+
* Visibility is a DISCRIMINANT, never inferred from geometry. The whole
|
|
31
|
+
* reason this type exists: a genuinely zero-SIZED but on-screen view
|
|
32
|
+
* (`{ visible:true, bounds:{...,width:0,height:0} }`) is now distinct from a
|
|
33
|
+
* detached/hidden one (`{ visible:false }`, which carries no `bounds` at
|
|
34
|
+
* all). Under the old ZERO convention both collapsed to the same value and
|
|
35
|
+
* were indistinguishable.
|
|
36
|
+
*/
|
|
37
|
+
export type Placement = {
|
|
38
|
+
visible: true;
|
|
39
|
+
bounds: Bounds;
|
|
40
|
+
} | {
|
|
41
|
+
visible: false;
|
|
42
|
+
};
|
|
43
|
+
export interface ViewAnchorOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Whether the native view should be attached. When `false`, the anchor
|
|
46
|
+
* publishes zero bounds (`{0,0,0,0}`) — the host treats `width === 0 ||
|
|
47
|
+
* height === 0` as "detach the child view but keep its WebContents
|
|
48
|
+
* alive" (detach-but-keep-alive). No DOM measurement is needed in this
|
|
49
|
+
* state.
|
|
50
|
+
*/
|
|
51
|
+
present: boolean;
|
|
52
|
+
/** Receives the live rect, or `{0,0,0,0}` when detached. Owns IPC. */
|
|
53
|
+
publish: (bounds: Bounds) => void;
|
|
54
|
+
}
|
|
55
|
+
export interface ViewAnchorHandle {
|
|
56
|
+
/**
|
|
57
|
+
* Apply new options. Re-publishes immediately to reflect the new state
|
|
58
|
+
* (present=true → measure + observe; present=false → zero bounds).
|
|
59
|
+
*/
|
|
60
|
+
update(opts: ViewAnchorOptions): void;
|
|
61
|
+
/** Stop observing and remove listeners. After dispose the anchor never
|
|
62
|
+
* publishes again (every emit reads `disposed` synchronously, so there is
|
|
63
|
+
* no queued frame that could fire late). */
|
|
64
|
+
dispose(): void;
|
|
65
|
+
}
|
|
66
|
+
/** Which axis this advertiser owns. `block` = height, `inline` = width
|
|
67
|
+
* (logical-property naming, axis-agnostic to writing mode). */
|
|
68
|
+
export type AdvertisedAxis = 'block' | 'inline';
|
|
69
|
+
/**
|
|
70
|
+
* One frame of advertised size. A pure scalar plus the owning axis — there is
|
|
71
|
+
* deliberately no field for the *other* axis, so "advertise two axes" is not
|
|
72
|
+
* expressible (single-axis ownership is enforced in the type, not at runtime).
|
|
73
|
+
*/
|
|
74
|
+
export interface AdvertisedSize {
|
|
75
|
+
/** Mirrors the factory's `axis`; constant across frames. Lets the host
|
|
76
|
+
* whitelist-check the axis it is willing to accept. */
|
|
77
|
+
readonly axis: AdvertisedAxis;
|
|
78
|
+
/** The owned axis's content extent, in CSS px — already rounded and clamped
|
|
79
|
+
* to `>= 0`. */
|
|
80
|
+
readonly extent: number;
|
|
81
|
+
}
|
|
82
|
+
export interface SizeAdvertiserOptions {
|
|
83
|
+
/** The single axis this advertiser owns. Fixed for the advertiser's life. */
|
|
84
|
+
axis: AdvertisedAxis;
|
|
85
|
+
/** Receives each advertised size. Owns the IPC/postMessage → host. Mirrors
|
|
86
|
+
* the forward `publish` (same role: the injected, transport-owning sink). */
|
|
87
|
+
publish: (size: AdvertisedSize) => void;
|
|
88
|
+
}
|
|
89
|
+
export interface SizeAdvertiserHandle {
|
|
90
|
+
/**
|
|
91
|
+
* Swap the `publish` sink (e.g. a new IPC channel) and immediately
|
|
92
|
+
* re-advertise the current size to it (mirrors the forward anchor's
|
|
93
|
+
* re-publish on update), so the new channel is not left sizeless until the
|
|
94
|
+
* next `ResizeObserver` tick.
|
|
95
|
+
*
|
|
96
|
+
* Takes only the new sink — `axis` is immutable by construction, so it is
|
|
97
|
+
* deliberately not expressible here (you cannot attempt to change it). To
|
|
98
|
+
* advertise a different axis, dispose and create a new advertiser.
|
|
99
|
+
*/
|
|
100
|
+
update(publish: (size: AdvertisedSize) => void): void;
|
|
101
|
+
/**
|
|
102
|
+
* Stop observing, cancel any pending RAF. After dispose nothing is
|
|
103
|
+
* advertised again. (There is no ZERO/terminal value — collapsing is the
|
|
104
|
+
* host's policy, unlike the forward anchor's `present:false`.)
|
|
105
|
+
*
|
|
106
|
+
* The first advertised value is asynchronous: it awaits the observer's first
|
|
107
|
+
* frame, and a `display:none` target advertises nothing until shown.
|
|
108
|
+
*/
|
|
109
|
+
dispose(): void;
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=types.d.ts.map
|