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.
- package/README.md +118 -34
- package/README.zh-CN.md +128 -44
- package/dist/index.d.ts +4 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -14
- package/dist/measure-loop.d.ts +9 -28
- package/dist/measure-loop.d.ts.map +1 -1
- package/dist/measure-loop.js +37 -16
- package/dist/protocol-publisher.d.ts +41 -0
- package/dist/protocol-publisher.d.ts.map +1 -0
- package/dist/protocol-publisher.js +191 -0
- package/dist/protocol-types.d.ts +36 -0
- package/dist/protocol-types.d.ts.map +1 -0
- package/dist/protocol-types.js +10 -0
- package/dist/protocol.d.ts +35 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +131 -0
- package/dist/react.d.ts +18 -30
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +125 -122
- package/dist/size-advertiser.d.ts +9 -14
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +20 -28
- package/dist/types.d.ts +29 -73
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -15
- package/dist/view-anchor.d.ts +36 -77
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +206 -174
- package/docs/bidirectional-design.md +64 -96
- package/docs/{anchor-3d.html → index.html} +215 -73
- package/docs/mechanism.mdx +55 -49
- package/docs/performance-report.md +63 -0
- package/docs/protocol.md +79 -0
- package/package.json +30 -4
- package/src/index.ts +6 -15
- package/src/measure-loop.ts +36 -41
- package/src/protocol-publisher.ts +236 -0
- package/src/protocol-types.ts +43 -0
- package/src/protocol.ts +193 -0
- package/src/react.ts +186 -141
- package/src/size-advertiser.ts +24 -31
- package/src/types.ts +34 -79
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
collapseAndDispose.current();
|
|
63
|
+
else if (previous) {
|
|
64
|
+
deferDetach(previous);
|
|
65
|
+
return undefined;
|
|
71
66
|
}
|
|
72
67
|
}
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
101
|
-
|
|
102
|
-
|
|
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 =
|
|
110
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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,
|
|
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"}
|
package/dist/size-advertiser.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import { createMeasureLoop } from './measure-loop.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
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
|
-
*
|
|
10
|
-
*
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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;
|
|
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
|
|
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
|
|
28
|
+
return Math.max(0, Math.round(raw));
|
|
35
29
|
};
|
|
36
30
|
const loop = createMeasureLoop({
|
|
37
31
|
produce,
|
|
38
|
-
same: (a, b) => a
|
|
39
|
-
sink: (
|
|
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
|
-
//
|
|
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
|
|
53
|
-
`
|
|
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-
|
|
65
|
-
//
|
|
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
|
-
*
|
|
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
|
|
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
|
|
28
|
-
* magic-`{0,0,0,0}` "hidden" convention (`present:false → ZERO bounds`).
|
|
19
|
+
* Explicit visibility and bounds for an anchored view.
|
|
29
20
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
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
|
|
53
|
-
publish:
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
56
|
+
/** The single axis this advertiser owns. Fixed for the advertiser's lifetime. */
|
|
84
57
|
axis: AdvertisedAxis;
|
|
85
|
-
/** Receives each advertised size.
|
|
86
|
-
|
|
87
|
-
publish: (size: AdvertisedSize) => void;
|
|
58
|
+
/** Receives each advertised size. */
|
|
59
|
+
publish: Publisher<AdvertisedSize>;
|
|
88
60
|
}
|
|
89
61
|
export interface SizeAdvertiserHandle {
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
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
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA
|
|
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
|
-
*
|
|
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 {};
|