view-anchor 0.1.2 → 0.2.0

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 (44) hide show
  1. package/README.md +118 -34
  2. package/README.zh-CN.md +128 -44
  3. package/dist/index.d.ts +4 -16
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +2 -14
  6. package/dist/measure-loop.d.ts +9 -28
  7. package/dist/measure-loop.d.ts.map +1 -1
  8. package/dist/measure-loop.js +37 -16
  9. package/dist/protocol-publisher.d.ts +41 -0
  10. package/dist/protocol-publisher.d.ts.map +1 -0
  11. package/dist/protocol-publisher.js +191 -0
  12. package/dist/protocol-types.d.ts +36 -0
  13. package/dist/protocol-types.d.ts.map +1 -0
  14. package/dist/protocol-types.js +10 -0
  15. package/dist/protocol.d.ts +35 -0
  16. package/dist/protocol.d.ts.map +1 -0
  17. package/dist/protocol.js +131 -0
  18. package/dist/react.d.ts +18 -30
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +125 -122
  21. package/dist/size-advertiser.d.ts +9 -14
  22. package/dist/size-advertiser.d.ts.map +1 -1
  23. package/dist/size-advertiser.js +20 -28
  24. package/dist/types.d.ts +29 -73
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +1 -15
  27. package/dist/view-anchor.d.ts +36 -77
  28. package/dist/view-anchor.d.ts.map +1 -1
  29. package/dist/view-anchor.js +206 -174
  30. package/docs/bidirectional-design.md +64 -96
  31. package/docs/{anchor-3d.html → index.html} +215 -73
  32. package/docs/mechanism.mdx +55 -49
  33. package/docs/performance-report.md +63 -0
  34. package/docs/protocol.md +79 -0
  35. package/package.json +30 -4
  36. package/src/index.ts +6 -15
  37. package/src/measure-loop.ts +36 -41
  38. package/src/protocol-publisher.ts +236 -0
  39. package/src/protocol-types.ts +43 -0
  40. package/src/protocol.ts +193 -0
  41. package/src/react.ts +186 -141
  42. package/src/size-advertiser.ts +24 -31
  43. package/src/types.ts +34 -79
  44. package/src/view-anchor.ts +228 -212
package/dist/react.js CHANGED
@@ -1,145 +1,148 @@
1
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: 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
- * 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) {
2
+ import { createPlacementAnchor, createViewAnchor, } from './view-anchor.js';
3
+ // Callback refs own the imperative anchor because React invokes them during commit.
4
+ // React 19 may call the cleanup returned from a ref and immediately reattach the
5
+ // same element in development mode. Collapse is deferred by one microtask so that
6
+ // immediate reattachment cancels the collapse.
7
+ function useAnchorRef(options, applied, adapter) {
20
8
  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(() => {
9
+ const elementRef = useRef(null);
10
+ const optionsRef = useRef(options);
11
+ // eslint-disable-next-line react-hooks/refs
12
+ optionsRef.current = options;
13
+ const adapterRef = useRef(adapter);
14
+ // eslint-disable-next-line react-hooks/refs
15
+ adapterRef.current = adapter;
16
+ const appliedRef = useRef(applied);
17
+ const currentAppliedRef = useRef(applied);
18
+ // eslint-disable-next-line react-hooks/refs
19
+ currentAppliedRef.current = applied;
20
+ // Options handed to the adapter on the last create/update call.
21
+ // Tracks applied state across renders where the deps array reference changes.
22
+ const lastAppliedOptionsRef = useRef(options);
23
+ const detachTokenRef = useRef(0);
24
+ const cancelPendingDetach = () => {
25
+ detachTokenRef.current++;
26
+ };
27
+ const collapseAndDispose = () => {
47
28
  const handle = handleRef.current;
48
29
  if (!handle)
49
30
  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;
31
+ const adapter = adapterRef.current;
32
+ const alreadyCollapsed = adapter.isCollapsed(lastAppliedOptionsRef.current);
33
+ try {
34
+ if (!alreadyCollapsed)
35
+ adapter.collapse(handle, optionsRef.current);
36
+ }
37
+ finally {
38
+ handleRef.current = null;
39
+ handle.dispose();
40
+ }
41
+ };
42
+ const deferDetach = (element) => {
43
+ const token = ++detachTokenRef.current;
44
+ queueMicrotask(() => {
45
+ if (detachTokenRef.current !== token || elementRef.current !== element)
46
+ return;
47
+ elementRef.current = null;
48
+ collapseAndDispose();
49
+ });
50
+ };
51
+ const ref = useCallback((element) => {
52
+ if (element === elementRef.current) {
53
+ cancelPendingDetach();
54
+ return element ? () => deferDetach(element) : undefined;
55
+ }
56
+ cancelPendingDetach();
57
+ const previous = elementRef.current;
58
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.
59
+ if (element) {
64
60
  handleRef.current.dispose();
65
61
  handleRef.current = null;
66
62
  }
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();
63
+ else if (previous) {
64
+ deferDetach(previous);
65
+ return undefined;
71
66
  }
72
67
  }
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
- ];
68
+ if (element) {
69
+ elementRef.current = element;
70
+ handleRef.current = adapterRef.current.create(element, optionsRef.current);
71
+ appliedRef.current = currentAppliedRef.current;
72
+ lastAppliedOptionsRef.current = optionsRef.current;
73
+ return () => deferDetach(element);
87
74
  }
75
+ return undefined;
76
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
88
77
  }, []);
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
78
  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]));
79
+ const previous = appliedRef.current;
80
+ const changed = applied.length !== previous.length ||
81
+ applied.some((value, index) => !Object.is(value, previous[index]));
107
82
  if (!changed)
108
83
  return;
109
- appliedRef.current = next;
110
- handleRef.current?.update({ present: opts.present, publish: opts.publish });
84
+ appliedRef.current = applied;
85
+ const handle = handleRef.current;
86
+ if (handle) {
87
+ adapterRef.current.update(handle, optionsRef.current);
88
+ lastAppliedOptionsRef.current = optionsRef.current;
89
+ }
111
90
  // 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.
91
+ }, applied);
131
92
  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
- }
93
+ cancelPendingDetach();
139
94
  return () => {
140
- if (elRef.current === null)
141
- collapse();
95
+ const element = elementRef.current;
96
+ if (element)
97
+ deferDetach(element);
142
98
  };
99
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- helpers only read stable refs
143
100
  }, []);
144
101
  return ref;
145
102
  }
103
+ const viewAdapter = {
104
+ create: createViewAnchor,
105
+ update(handle, options) {
106
+ handle.update(options);
107
+ },
108
+ collapse(handle, options) {
109
+ handle.update({ present: false, publish: options.publish });
110
+ },
111
+ isCollapsed(options) {
112
+ return !options.present;
113
+ },
114
+ };
115
+ /** Bind zero-bounds visibility to a DOM element callback ref. */
116
+ export function useViewAnchor(options) {
117
+ return useAnchorRef(options, [options.present, options.publish, ...(options.deps ?? [])], viewAdapter);
118
+ }
119
+ const placementAdapter = {
120
+ create: createPlacementAnchor,
121
+ update(handle, options) {
122
+ // In React, an omitted option represents "off" for that render,
123
+ // rather than keeping the previous value.
124
+ handle.update({
125
+ ...options,
126
+ guardDisplayNone: options.guardDisplayNone ?? false,
127
+ followScroll: options.followScroll ?? false,
128
+ followGeometry: options.followGeometry ?? false,
129
+ });
130
+ },
131
+ collapse(handle, options) {
132
+ handle.update({ ...options, visible: false });
133
+ },
134
+ isCollapsed(options) {
135
+ return !options.visible;
136
+ },
137
+ };
138
+ /** Bind the explicit Placement API to a DOM element callback ref. */
139
+ export function usePlacementAnchor(options) {
140
+ return useAnchorRef(options, [
141
+ options.visible,
142
+ options.publish,
143
+ options.guardDisplayNone,
144
+ options.followScroll,
145
+ options.followGeometry,
146
+ ...(options.deps ?? []),
147
+ ], placementAdapter);
148
+ }
@@ -1,21 +1,16 @@
1
1
  import type { SizeAdvertiserOptions, SizeAdvertiserHandle } from './types.js';
2
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`).
3
+ * Report content size for a single axis back to the host.
8
4
  *
9
- * The extent is `Math.round`ed and clamped to `>= 0`; non-finite measurements
10
- * drop the frame.
5
+ * Runs in a downstream document, reads the content size from
6
+ * `ResizeObserverEntry.borderBoxSize` without triggering reflow, and
7
+ * publishes updates through an animation frame loop (`createMeasureLoop`).
11
8
  *
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`'s single-axis-ownership and trust-boundary
18
- * sections.
9
+ * Measurements are rounded to integer pixels and clamped to >= 0.
10
+ *
11
+ * Note: `target` should be a shrink-to-fit wrapper on the owned axis.
12
+ * If its size is driven by the host view itself (such as `<body>` or `<html>`),
13
+ * updates will not shrink back to content size.
19
14
  */
20
15
  export declare function createSizeAdvertiser(target: HTMLElement, opts: SizeAdvertiserOptions): SizeAdvertiserHandle;
21
16
  //# sourceMappingURL=size-advertiser.d.ts.map
@@ -1 +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;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CAkEtB"}
1
+ {"version":3,"file":"size-advertiser.d.ts","sourceRoot":"","sources":["../src/size-advertiser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,qBAAqB,EACrB,oBAAoB,EACrB,MAAM,YAAY,CAAA;AAGnB;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,qBAAqB,GAC1B,oBAAoB,CA+DtB"}
@@ -1,29 +1,23 @@
1
1
  import { createMeasureLoop } from './measure-loop.js';
2
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`).
3
+ * Report content size for a single axis back to the host.
8
4
  *
9
- * The extent is `Math.round`ed and clamped to `>= 0`; non-finite measurements
10
- * drop the frame.
5
+ * Runs in a downstream document, reads the content size from
6
+ * `ResizeObserverEntry.borderBoxSize` without triggering reflow, and
7
+ * publishes updates through an animation frame loop (`createMeasureLoop`).
11
8
  *
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`'s single-axis-ownership and trust-boundary
18
- * sections.
9
+ * Measurements are rounded to integer pixels and clamped to >= 0.
10
+ *
11
+ * Note: `target` should be a shrink-to-fit wrapper on the owned axis.
12
+ * If its size is driven by the host view itself (such as `<body>` or `<html>`),
13
+ * updates will not shrink back to content size.
19
14
  */
20
15
  export function createSizeAdvertiser(target, opts) {
21
- const axis = opts.axis; // immutable for the advertiser's life
16
+ const axis = opts.axis;
22
17
  let publish = opts.publish;
23
18
  let observer = null;
24
19
  let disposed = false;
25
- // Latest border-box, stashed by the RO callback and read by `produce` in the
26
- // RAF body (keep the entry out of the shared, DOM-agnostic loop).
20
+ // Latest border-box recorded by the ResizeObserver callback.
27
21
  let latest = null;
28
22
  const produce = () => {
29
23
  if (!latest)
@@ -31,12 +25,12 @@ export function createSizeAdvertiser(target, opts) {
31
25
  const raw = axis === 'block' ? latest.blockSize : latest.inlineSize;
32
26
  if (!Number.isFinite(raw))
33
27
  return null;
34
- return { axis, extent: Math.max(0, Math.round(raw)) };
28
+ return Math.max(0, Math.round(raw));
35
29
  };
36
30
  const loop = createMeasureLoop({
37
31
  produce,
38
- same: (a, b) => a.extent === b.extent, // axis is constant
39
- sink: (size) => publish(size),
32
+ same: (a, b) => a === b,
33
+ sink: (extent) => publish({ axis, extent }),
40
34
  });
41
35
  const onResize = (entries) => {
42
36
  const entry = entries[entries.length - 1];
@@ -45,13 +39,12 @@ export function createSizeAdvertiser(target, opts) {
45
39
  }
46
40
  loop.schedule();
47
41
  };
48
- // One cheap, once-per-advertiser guard for the textbook feedback-loop footgun.
42
+ // Warn if measuring body or documentElement, whose size matches the view.
49
43
  const doc = target.ownerDocument;
50
44
  if (target === doc.body || target === doc.documentElement) {
51
45
  console.warn(`[view-anchor] size-advertiser: <${target === doc.body ? 'body' : 'html'}>'s ` +
52
- `${axis} size is the host-given view size, not the content size the ` +
53
- `advertiser will never shrink to content. Measure a shrink-to-fit wrapper. ` +
54
- `See bidirectional-design.md's single-axis-ownership section.`);
46
+ `${axis} size is the view size, not content size. The advertiser will ` +
47
+ `never shrink to content; measure a shrink-to-fit wrapper instead.`);
55
48
  }
56
49
  loop.setActive(true);
57
50
  observer = new ResizeObserver(onResize);
@@ -61,11 +54,10 @@ export function createSizeAdvertiser(target, opts) {
61
54
  if (disposed)
62
55
  return;
63
56
  publish = nextPublish;
64
- // Re-advertise the current size to the new sink immediately (mirrors the
65
- // forward anchor's re-publish on update) so the new channel is not left
66
- // sizeless until the next ResizeObserver tick.
57
+ // Re-publish the current size to the new sink immediately so it is not
58
+ // empty until the next ResizeObserver tick.
67
59
  const cur = produce();
68
- if (cur)
60
+ if (cur !== null)
69
61
  loop.emitNow(cur);
70
62
  },
71
63
  dispose() {
package/dist/types.d.ts CHANGED
@@ -1,38 +1,25 @@
1
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 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`.
2
+ * Core geometry and transport types for view-anchor.
17
3
  */
18
- /** A screen-space rectangle, in CSS pixels. Structurally compatible with
19
- * the host's `ViewBounds` so a publisher typed against either works. */
4
+ /** A screen-space rectangle in CSS pixels. */
20
5
  export interface Bounds {
21
6
  x: number;
22
7
  y: number;
23
8
  width: number;
24
9
  height: number;
25
10
  }
11
+ /** Returning false declines a value; true and void accept it. */
12
+ export type PublishResult = void | boolean;
13
+ /**
14
+ * Synchronous publish callback. A batching transport returns true once
15
+ * the value is queued; subsequent delivery is handled by the transport.
16
+ */
17
+ export type Publisher<T> = (value: T) => PublishResult;
26
18
  /**
27
- * Explicit visibility + geometry for a native view, replacing the legacy
28
- * magic-`{0,0,0,0}` "hidden" convention (`present:false → ZERO bounds`).
19
+ * Explicit visibility and bounds for an anchored view.
29
20
  *
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.
21
+ * Distinguishes an intentionally visible but zero-sized element ({ visible: true, bounds: 0x0 })
22
+ * from a hidden or detached element ({ visible: false }).
36
23
  */
37
24
  export type Placement = {
38
25
  visible: true;
@@ -42,70 +29,39 @@ export type Placement = {
42
29
  };
43
30
  export interface ViewAnchorOptions {
44
31
  /**
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.
32
+ * Whether the native view should be attached. When false, publishes
33
+ * zero bounds ({ x: 0, y: 0, width: 0, height: 0 }) so the host can detach
34
+ * the view while keeping its instance alive.
50
35
  */
51
36
  present: boolean;
52
- /** Receives the live rect, or `{0,0,0,0}` when detached. Owns IPC. */
53
- publish: (bounds: Bounds) => void;
37
+ /** Receives the live rect, or zero bounds when detached. */
38
+ publish: Publisher<Bounds>;
54
39
  }
55
40
  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
- */
41
+ /** Apply new options and re-publish immediately. */
60
42
  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). */
43
+ /** Stop observing and clean up listeners. After disposal no further values are published. */
64
44
  dispose(): void;
65
45
  }
66
- /** Which axis this advertiser owns. `block` = height, `inline` = width
67
- * (logical-property naming, axis-agnostic to writing mode). */
46
+ /** Which axis this advertiser reports: 'block' (height) or 'inline' (width). */
68
47
  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
- */
48
+ /** One frame of advertised size on the owned axis. */
74
49
  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. */
50
+ /** The axis this advertiser reports ('block' or 'inline'). */
77
51
  readonly axis: AdvertisedAxis;
78
- /** The owned axis's content extent, in CSS px — already rounded and clamped
79
- * to `>= 0`. */
52
+ /** The content extent in CSS pixels, rounded and non-negative. */
80
53
  readonly extent: number;
81
54
  }
82
55
  export interface SizeAdvertiserOptions {
83
- /** The single axis this advertiser owns. Fixed for the advertiser's life. */
56
+ /** The single axis this advertiser owns. Fixed for the advertiser's lifetime. */
84
57
  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;
58
+ /** Receives each advertised size. */
59
+ publish: Publisher<AdvertisedSize>;
88
60
  }
89
61
  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
- */
62
+ /** Swap the publish callback and re-advertise the current size immediately. */
63
+ update(publish: Publisher<AdvertisedSize>): void;
64
+ /** Stop observing and cancel any pending animation frame. */
109
65
  dispose(): void;
110
66
  }
111
67
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;yEACyE;AACzE,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAEtB,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,sEAAsE;IACtE,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC;;iDAE6C;IAC7C,OAAO,IAAI,IAAI,CAAA;CAChB;AAUD;gEACgE;AAChE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/C;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;4DACwD;IACxD,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B;qBACiB;IACjB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,IAAI,EAAE,cAAc,CAAA;IACpB;kFAC8E;IAC9E,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,CAAA;CACxC;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;OASG;IACH,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,cAAc,KAAK,IAAI,GAAG,IAAI,CAAA;IACrD;;;;;;;OAOG;IACH,OAAO,IAAI,IAAI,CAAA;CAChB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,8CAA8C;AAC9C,MAAM,WAAW,MAAM;IACrB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,iEAAiE;AACjE,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,OAAO,CAAA;AAE1C;;;GAGG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,aAAa,CAAA;AAEtD;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GACjB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,CAAA;AAEtB,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,4DAA4D;IAC5D,OAAO,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,oDAAoD;IACpD,MAAM,CAAC,IAAI,EAAE,iBAAiB,GAAG,IAAI,CAAA;IACrC,6FAA6F;IAC7F,OAAO,IAAI,IAAI,CAAA;CAChB;AAOD,gFAAgF;AAChF,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/C,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAA;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,EAAE,cAAc,CAAA;IACpB,qCAAqC;IACrC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,CAAA;CACnC;AAED,MAAM,WAAW,oBAAoB;IACnC,+EAA+E;IAC/E,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG,IAAI,CAAA;IAChD,6DAA6D;IAC7D,OAAO,IAAI,IAAI,CAAA;CAChB"}
package/dist/types.js CHANGED
@@ -1,18 +1,4 @@
1
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 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`.
2
+ * Core geometry and transport types for view-anchor.
17
3
  */
18
4
  export {};