view-anchor 1.0.0-beta.1 → 1.0.0-beta.2

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 CHANGED
@@ -68,15 +68,19 @@ const publish = (placement) => {
68
68
  const handle = createViewAnchor(target, {
69
69
  visible: true,
70
70
  publish,
71
- followScroll: true, // re-measure when any ancestor scrolls
72
- followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
71
+ followScroll: true, // re-measure on scroll (window capture fallback + ancestor listeners)
72
+ followGeometry: true, // poll animation frames during scrolls / pulse(), stop when steady
73
73
  treatZeroAreaAsHidden: true, // zero-area or display:none target → { visible: false }
74
- holdSelector: '[role="separator"]', // default; pointerdown on a match keeps followGeometry open while held, null disables
75
74
  dedupe: true, // default; set false to receive every measurement, even unchanged ones
76
75
  })
77
76
 
78
- handle.update({ visible: true, publish }) // apply new options and publish right away
79
- handle.pulse() // re-measure after a move no observer sees (e.g. a class toggle moved the target without resizing it); closes once steady
77
+ // update() applies new options; omitted fields reset to defaults (e.g. omitting
78
+ // followScroll resets it to false).
79
+ handle.update({ visible: true, publish, followScroll: true, followGeometry: true })
80
+ // pulse() opens frame-following for a limited duration; use it to track animation
81
+ // after a move no observer sees. For splitter drags, call pulse() after each
82
+ // pointermove that changes layout; a single pointerdown pulse stops once steady.
83
+ handle.pulse()
80
84
  handle.dispose() // stop observing; never publishes again
81
85
  ```
82
86
 
@@ -94,7 +98,7 @@ controller.abort() // same cleanup as handle.dispose()
94
98
  ### React
95
99
 
96
100
  ```tsx
97
- import { useViewAnchor } from 'view-anchor/react'
101
+ import { useViewAnchor, useSizeAnchor } from 'view-anchor/react'
98
102
 
99
103
  function DebugPanel({ visible }: { visible: boolean }) {
100
104
  const ref = useViewAnchor({
@@ -107,9 +111,20 @@ function DebugPanel({ visible }: { visible: boolean }) {
107
111
  // sends { visible: false }, without deciding how the surface is stored.
108
112
  return <div ref={ref} className="h-full w-full" />
109
113
  }
114
+
115
+ function ContentSizer() {
116
+ const ref = useSizeAnchor({
117
+ axis: 'block', // report height; use 'inline' for width
118
+ publish: updatePlaceholderHeight,
119
+ })
120
+ // The ref attaches to the element whose content size drives the host placeholder.
121
+ return <div ref={ref}>{/* dynamic content */}</div>
122
+ }
110
123
  ```
111
124
 
112
- The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames. It re-applies options on every update with the same reset semantics as `createViewAnchor` and `update()`: an omitted option resets to its default, not the previous value. An omitted `treatZeroAreaAsHidden`, `followScroll`, or `followGeometry` counts as `false`; an omitted `holdSelector` resets to `[role="separator"]`; an omitted `dedupe` resets to `true`. Pass `null` / `false` to turn `holdSelector` / `dedupe` off.
125
+ For a splitter controlled by React, call `ref.pulse()` after each active pointer move updates the layout. The `useViewAnchor` ref remains a callback ref; its `pulse()` method forwards to the current handle and does nothing when detached or when `followGeometry` is off. A single pulse on pointerdown does not keep following through a pause.
126
+
127
+ Both hooks survive React 18 and 19 StrictMode double-mounting without publishing stale frames. They use `update()` semantics: an omitted option resets to its default, not the previous value. An omitted `treatZeroAreaAsHidden`, `followScroll`, or `followGeometry` counts as `false`; an omitted `dedupe` resets to `true`.
113
128
 
114
129
  ### Let content drive the size
115
130
 
@@ -165,20 +180,22 @@ The full contract is in [docs/protocol.md](./docs/protocol.md).
165
180
 
166
181
  ## API
167
182
 
168
- | Export | Kind | Purpose |
169
- | ----------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
170
- | `createViewAnchor(target, opts)` | function | Publish a `Placement`, with opt-in `followScroll` / `followGeometry` / `treatZeroAreaAsHidden` / `holdSelector` / `dedupe`, and `pulse()`. |
171
- | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
172
- | `createSizeAnchor(target, opts)` | function | Call `publish({ axis, extent })` with one content-size axis. |
173
- | `useViewAnchor(opts)` from `view-anchor/react` | hook | React adapter for `createViewAnchor`, returning a ref callback for a placeholder element. |
174
- | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
175
- | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
176
- | `Publisher<T>` / `PublishResult` | type | Signature of the `publish` callback and its return value (`void \| boolean`). |
177
- | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
178
- | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
179
- | `SizeAxis` / `SizeMeasurement` | type | Axis and payload types for the reverse direction. |
180
- | `SizeAnchorOptions` / `SizeAnchorHandle` | type | Options and handle for `createSizeAnchor`. |
181
- | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
183
+ | Export | Kind | Purpose |
184
+ | ----------------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------- |
185
+ | `createViewAnchor(target, opts)` | function | Publish a `Placement`, with opt-in `followScroll` / `followGeometry` / `treatZeroAreaAsHidden` / `dedupe`, and `pulse()`. |
186
+ | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
187
+ | `createSizeAnchor(target, opts)` | function | Call `publish({ axis, extent })` with one content-size axis. |
188
+ | `useViewAnchor(opts)` from `view-anchor/react` | hook | React adapter for `createViewAnchor`, returning a ref callback for a placeholder element. |
189
+ | `useSizeAnchor(opts)` from `view-anchor/react` | hook | React adapter for `createSizeAnchor`, returning a ref callback for a content element. |
190
+ | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
191
+ | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
192
+ | `Publisher<T>` / `PublishResult` | type | Signature of the `publish` callback and its return value (`void \| boolean`). |
193
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. Handle includes `update()`, `pulse()`, and `dispose()`. |
194
+ | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and callback ref for `useViewAnchor`; the ref also has `pulse()`. |
195
+ | `UseSizeAnchorOptions` / `SizeAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useSizeAnchor`. |
196
+ | `SizeAxis` / `SizeMeasurement` | type | Axis and payload types for the reverse direction. |
197
+ | `SizeAnchorOptions` / `SizeAnchorHandle` | type | Options and handle for `createSizeAnchor`. Handle includes `update()` and `dispose()`. |
198
+ | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
182
199
 
183
200
  ## Versioning
184
201
 
package/README.zh-CN.md CHANGED
@@ -52,15 +52,16 @@ const publish = (placement) => {
52
52
  const handle = createViewAnchor(placeholderEl, {
53
53
  visible: true,
54
54
  publish,
55
- followScroll: true, // 祖先容器滚动时重新测量
56
- followGeometry: true, // 拖拽或滚动期间逐帧测量;稳定后停止
55
+ followScroll: true, // 滚动时重新测量(window 捕获阶段 + 祖先监听)
56
+ followGeometry: true, // 滚动或 pulse() 期间逐帧测量;稳定后停止
57
57
  treatZeroAreaAsHidden: true, // 零面积或 display:none 时发布 { visible: false }
58
- holdSelector: '[role="separator"]', // 默认值;命中该选择器的 pointerdown 按住期间保持 followGeometry 开启,传 null 关闭
59
58
  dedupe: true, // 默认值;传 false 让每次测量都发布,即使值没变
60
59
  })
61
60
 
62
- handle.update({ visible: true, publish }) // 更新选项并立即重新发布
63
- handle.pulse() // 位置变了但没有任何观察器能感知时(如一次类名切换让目标移动了但尺寸没变)主动重测;稳定后自动停止
61
+ // update() 应用新选项;省略的字段重置为默认值(例如省略 followScroll 会重置为 false)。
62
+ handle.update({ visible: true, publish, followScroll: true, followGeometry: true })
63
+ // pulse() 开启一段时间的帧跟踪;用于跟踪观察器无法感知的变化。分隔条拖拽时,每次 pointermove 更新布局后都要调用 pulse();仅在 pointerdown 调用一次会在几何稳定后停止跟随。
64
+ handle.pulse()
64
65
  handle.dispose() // 停止观察;之后不会再发布
65
66
  ```
66
67
 
@@ -82,7 +83,7 @@ controller.abort()
82
83
  ### React
83
84
 
84
85
  ```tsx
85
- import { useViewAnchor } from 'view-anchor/react'
86
+ import { useViewAnchor, useSizeAnchor } from 'view-anchor/react'
86
87
 
87
88
  function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
88
89
  const ref = useViewAnchor({
@@ -94,11 +95,20 @@ function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
94
95
 
95
96
  return <div ref={ref} className="h-full w-full" />
96
97
  }
98
+
99
+ function ContentSizer() {
100
+ const ref = useSizeAnchor({
101
+ axis: 'block', // 报告高度;使用 'inline' 报告宽度
102
+ publish: updatePlaceholderHeight,
103
+ })
104
+ // ref 附加到内容尺寸驱动宿主占位的元素
105
+ return <div ref={ref}>{/* 动态内容 */}</div>
106
+ }
97
107
  ```
98
108
 
99
- 元素卸载时,Hook 会先发布 `{ visible: false }`,再释放监听。它兼容 React 18 和 19 的 StrictMode 重挂载,不会因此发布过期帧。
109
+ React 分隔条每次拖动更新布局后可调用 `ref.pulse()`。`useViewAnchor` 返回的仍是回调 ref,额外提供 `pulse()`;元素未挂载或 `followGeometry` 关闭时调用不会起作用。只在按下时调用一次无法覆盖停顿后的移动。
100
110
 
101
- 它每次调用都会重新应用选项,规则与 `createViewAnchor` 和 `update()` 一致:省略任何一项都会重置为默认值,而不是沿用上次的值。省略 `treatZeroAreaAsHidden`、`followScroll` 或 `followGeometry` 会重置为 `false`;省略 `holdSelector` 会重置为 `[role="separator"]`;省略 `dedupe` 会重置为 `true`。要关闭 `holdSelector` / `dedupe`,传 `null` / `false`。
111
+ 两个 hook 都能在 React 18 和 19 的 StrictMode 重挂载中正常工作,不会发布过期帧。它们使用 `update()` 语义:省略的选项重置为默认值,而不是沿用上次的值。省略 `treatZeroAreaAsHidden`、`followScroll` 或 `followGeometry` 会重置为 `false`;省略 `dedupe` 会重置为 `true`。
102
112
 
103
113
  ### 由内容决定占位尺寸
104
114
 
@@ -159,20 +169,22 @@ if (decoded.ok) {
159
169
 
160
170
  ## API
161
171
 
162
- | 导出 | 类型 | 用途 |
163
- | ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
164
- | `createViewAnchor(target, opts)` | 函数 | 发布带可见性的 `Placement`,可选 `followScroll`、`followGeometry`、`treatZeroAreaAsHidden`、`holdSelector`、`dedupe`,含 `pulse()`。 |
165
- | `measurePlacement(target)` | 函数 | 读取当前矩形,返回 `{ visible: true, bounds }`。 |
166
- | `createSizeAnchor(target, opts)` | 函数 | 用 `publish({ axis, extent })` 上报一个内容尺寸轴。 |
167
- | `useViewAnchor(opts)` | Hook | `createViewAnchor` 的 React 适配,返回占位元素的 ref 回调。 |
168
- | `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
169
- | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
170
- | `Publisher<T>` / `PublishResult` | 类型 | `publish` 回调的签名及其返回值(`void \| boolean`)。 |
171
- | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项与句柄。 |
172
- | `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | `useViewAnchor` 的选项与 ref 类型。 |
173
- | `SizeAxis` / `SizeMeasurement` | 类型 | 内容尺寸上报的轴和数据。 |
174
- | `SizeAnchorOptions` / `SizeAnchorHandle` | 类型 | `createSizeAnchor` 的选项与句柄。 |
175
- | `view-anchor/protocol` | 函数 + 类型 | 消息封装、校验、排序和批处理。 |
172
+ | 导出 | 类型 | 用途 |
173
+ | ---------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------- |
174
+ | `createViewAnchor(target, opts)` | 函数 | 发布带可见性的 `Placement`,可选 `followScroll`、`followGeometry`、`treatZeroAreaAsHidden`、`dedupe`,含 `pulse()`。 |
175
+ | `measurePlacement(target)` | 函数 | 读取当前矩形,返回 `{ visible: true, bounds }`。 |
176
+ | `createSizeAnchor(target, opts)` | 函数 | 用 `publish({ axis, extent })` 上报一个内容尺寸轴。 |
177
+ | `useViewAnchor(opts)` | Hook | `createViewAnchor` 的 React 适配,返回占位元素的 ref 回调。 |
178
+ | `useSizeAnchor(opts)` | Hook | `createSizeAnchor` 的 React 适配,返回内容元素的 ref 回调。 |
179
+ | `Bounds` | 类型 | `{ x, y, width, height }`,单位为 CSS 像素。 |
180
+ | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
181
+ | `Publisher<T>` / `PublishResult` | 类型 | `publish` 回调的签名及其返回值(`void \| boolean`)。 |
182
+ | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项与句柄。句柄包含 `update()`、`pulse()` 和 `dispose()`。 |
183
+ | `UseViewAnchorOptions` / `ViewAnchorRef` | 类型 | `useViewAnchor` 的选项与回调 ref 类型;ref 还提供 `pulse()`。 |
184
+ | `UseSizeAnchorOptions` / `SizeAnchorRef` | 类型 | `useSizeAnchor` 的选项与 ref 类型。 |
185
+ | `SizeAxis` / `SizeMeasurement` | 类型 | 内容尺寸上报的轴和数据。 |
186
+ | `SizeAnchorOptions` / `SizeAnchorHandle` | 类型 | `createSizeAnchor` 的选项与句柄。句柄包含 `update()` 和 `dispose()`。 |
187
+ | `view-anchor/protocol` | 函数 + 类型 | 消息封装、校验、排序和批处理。 |
176
188
 
177
189
  ## 版本承诺
178
190
 
package/dist/react.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { ViewAnchorOptions } from './view-anchor.js';
2
+ import type { SizeAnchorOptions, SizeAnchorHandle, SizeAxis, SizeMeasurement } from './types.js';
3
+ export type { SizeAnchorOptions, SizeAnchorHandle, SizeAxis, SizeMeasurement };
2
4
  export interface UseViewAnchorOptions extends ViewAnchorOptions {
3
5
  /**
4
6
  * Values that re-apply the anchor when changed. Keep this array's length
@@ -7,7 +9,25 @@ export interface UseViewAnchorOptions extends ViewAnchorOptions {
7
9
  deps?: ReadonlyArray<unknown>;
8
10
  }
9
11
  /** Compatible with React 18's null callback and React 19's ref cleanup. */
10
- export type ViewAnchorRef = (el: HTMLElement | null) => void | (() => void);
12
+ type AnchorRef = (el: HTMLElement | null) => void | (() => void);
13
+ /** Callback ref with an imperative trigger for geometry changes React cannot observe. */
14
+ export type ViewAnchorRef = AnchorRef & {
15
+ pulse(durationMs?: number): void;
16
+ };
11
17
  /** Bind the explicit Placement API to a DOM element callback ref. */
12
18
  export declare function useViewAnchor(options: UseViewAnchorOptions): ViewAnchorRef;
19
+ export interface UseSizeAnchorOptions extends Omit<SizeAnchorOptions, 'signal'> {
20
+ /**
21
+ * Values that re-apply the anchor when changed. Keep this array's length
22
+ * stable across renders.
23
+ */
24
+ deps?: ReadonlyArray<unknown>;
25
+ }
26
+ /** Compatible with React 18's null callback and React 19's ref cleanup. */
27
+ export type SizeAnchorRef = AnchorRef;
28
+ /**
29
+ * Bind a size anchor to a DOM element callback ref. Reports the target's
30
+ * content size on the specified axis back to the provided publish callback.
31
+ */
32
+ export declare function useSizeAnchor(options: UseSizeAnchorOptions): SizeAnchorRef;
13
33
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AA+J3E,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAe1E"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AAEzD,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEhG,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,QAAQ,EAAE,eAAe,EAAE,CAAA;AAE9E,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,2EAA2E;AAC3E,KAAK,SAAS,GAAG,CAAC,EAAE,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,CAAA;AAEhE,yFAAyF;AACzF,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG;IAAE,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AA8K5E,qEAAqE;AACrE,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAwB1E;AAMD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC;IAC7E;;;OAGG;IACH,IAAI,CAAC,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;CAC9B;AAED,2EAA2E;AAC3E,MAAM,MAAM,aAAa,GAAG,SAAS,CAAA;AA0BrC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,aAAa,CAM1E"}
package/dist/react.js CHANGED
@@ -1,5 +1,6 @@
1
- import { useCallback, useEffect, useInsertionEffect, useRef, version } from 'react';
1
+ import { useCallback, useEffect, useInsertionEffect, useMemo, useRef, version } from 'react';
2
2
  import { createViewAnchor } from './view-anchor.js';
3
+ import { createSizeAnchor } from './size-anchor.js';
3
4
  // Callback refs own the imperative anchor because React invokes them during commit.
4
5
  // React 19 may call the cleanup returned from a ref and immediately reattach the
5
6
  // same element in development mode. Collapse is deferred by one microtask so that
@@ -86,9 +87,20 @@ function useAnchorRef(options, applied, adapter) {
86
87
  return;
87
88
  appliedRef.current = applied;
88
89
  const handle = handleRef.current;
89
- if (handle) {
90
- adapterRef.current.update(handle, optionsRef.current);
91
- lastAppliedOptionsRef.current = optionsRef.current;
90
+ const element = elementRef.current;
91
+ if (handle && element) {
92
+ const adapter = adapterRef.current;
93
+ const prevOpts = lastAppliedOptionsRef.current;
94
+ const nextOpts = optionsRef.current;
95
+ // Check if we need to recreate the handle (e.g. axis change in size anchor)
96
+ if (adapter.shouldRecreate?.(prevOpts, nextOpts)) {
97
+ handle.dispose();
98
+ handleRef.current = adapter.create(element, nextOpts);
99
+ }
100
+ else {
101
+ adapter.update(handle, nextOpts);
102
+ }
103
+ lastAppliedOptionsRef.current = nextOpts;
92
104
  }
93
105
  // oxlint-disable-next-line react/exhaustive-deps
94
106
  }, applied);
@@ -100,7 +112,7 @@ function useAnchorRef(options, applied, adapter) {
100
112
  deferDetach(element);
101
113
  };
102
114
  }, []);
103
- return ref;
115
+ return { ref, handleRef };
104
116
  }
105
117
  const hideStates = new WeakMap();
106
118
  const trackHide = (options, state) => ({
@@ -135,14 +147,48 @@ const viewAdapter = {
135
147
  };
136
148
  /** Bind the explicit Placement API to a DOM element callback ref. */
137
149
  export function useViewAnchor(options) {
138
- return useAnchorRef(options, [
150
+ const { ref: attach, handleRef } = useAnchorRef(options, [
139
151
  options.visible,
140
152
  options.publish,
141
153
  options.treatZeroAreaAsHidden,
142
154
  options.followScroll,
143
155
  options.followGeometry,
144
- options.holdSelector,
145
156
  options.dedupe,
146
157
  ...(options.deps ?? []),
147
158
  ], viewAdapter);
159
+ const pulse = useCallback((durationMs) => handleRef.current?.pulse(durationMs), [handleRef]);
160
+ // The new callback captures the handle, but only reads it when pulse() is called.
161
+ return useMemo(
162
+ // oxlint-disable-next-line react/refs
163
+ () => Object.assign((element) => attach(element), { pulse }), [attach, pulse]);
164
+ }
165
+ const sizeAdapter = {
166
+ create(target, options) {
167
+ return createSizeAnchor(target, options);
168
+ },
169
+ update(handle, options) {
170
+ // Options are forwarded as-is; the hook applies the same "omitted resets
171
+ // to default" rule as the core's update().
172
+ handle.update(options);
173
+ },
174
+ collapse(_handle, _options) {
175
+ // Size anchors do not have a "hidden" state to collapse to; they simply
176
+ // stop publishing when disposed. No action needed here.
177
+ },
178
+ isCollapsed(_handle, _options) {
179
+ // Size anchors are never in a "collapsed" state that needs to be sent
180
+ // before unmount; always return true to skip the collapse call.
181
+ return true;
182
+ },
183
+ shouldRecreate(prevOptions, nextOptions) {
184
+ // axis is frozen at creation; changing it requires a new handle
185
+ return prevOptions.axis !== nextOptions.axis;
186
+ },
187
+ };
188
+ /**
189
+ * Bind a size anchor to a DOM element callback ref. Reports the target's
190
+ * content size on the specified axis back to the provided publish callback.
191
+ */
192
+ export function useSizeAnchor(options) {
193
+ return useAnchorRef(options, [options.axis, options.publish, options.dedupe, ...(options.deps ?? [])], sizeAdapter).ref;
148
194
  }
@@ -1,5 +1,4 @@
1
1
  import type { Placement, Publisher } from './types.js';
2
- export declare const DEFAULT_HOLD_SELECTOR = "[role=\"separator\"]";
3
2
  export interface ViewAnchorOptions {
4
3
  /**
5
4
  * Whether the native view should be visible. When true, publishes
@@ -23,27 +22,23 @@ export interface ViewAnchorOptions {
23
22
  */
24
23
  treatZeroAreaAsHidden?: boolean;
25
24
  /**
26
- * When true, listens for capture-phase scroll events on window to re-measure
27
- * when an ancestor container scrolls. Default is false.
28
- * update() applies the same default: omitting it resets to false.
25
+ * When true, re-measures on scroll events. Uses a hybrid approach:
26
+ * - A capture-phase window scroll listener provides a reliable fallback that
27
+ * catches all scrolls (including overflow:hidden + programmatic scrollLeft/Top,
28
+ * and works even before the element is connected or after reparenting).
29
+ * - Scrollable ancestor listeners catch scrolls in detached subtrees or shadow
30
+ * roots that do not reach window. Events reaching window are handled there
31
+ * only once, even when dedupe is disabled.
32
+ * Ancestors are re-collected on update().
33
+ * Default is false. update() applies the same default: omitting it resets to false.
29
34
  */
30
35
  followScroll?: boolean;
31
36
  /**
32
- * When true, polls geometry per animation frame during active motion (scrolls,
33
- * splitter dragging, or pulse()) and auto-closes when steady.
37
+ * When true, polls geometry per animation frame during active motion (scrolls
38
+ * or pulse()) and auto-closes when steady.
34
39
  * update() applies the same default: omitting it resets to false.
35
40
  */
36
41
  followGeometry?: boolean;
37
- /**
38
- * CSS selector for the press-and-hold target that keeps followGeometry open
39
- * for the duration of a pointer press. Only meaningful when followGeometry
40
- * is true; the pointer listeners are mounted only while both are set. A capture-phase pointerdown whose
41
- * target matches `closest(holdSelector)` opens frame following until release.
42
- * Default is `[role="separator"]`. Pass null to disable. update() applies
43
- * the same default when omitted. An invalid selector throws synchronously
44
- * (SyntaxError) before any option is applied.
45
- */
46
- holdSelector?: string | null;
47
42
  /**
48
43
  * When true (the default), a measurement identical to the last accepted
49
44
  * Placement is not published again. When false, every usable frame publishes
@@ -58,7 +53,7 @@ export interface ViewAnchorHandle {
58
53
  /**
59
54
  * Apply a full set of options and re-publish immediately. Omitted options
60
55
  * reset to defaults: treatZeroAreaAsHidden/followScroll/followGeometry → false,
61
- * holdSelector → `[role="separator"]` (pass null to disable), dedupe → true.
56
+ * dedupe → true.
62
57
  * `signal` is not accepted here — it cannot be changed after creation.
63
58
  */
64
59
  update(opts: Omit<ViewAnchorOptions, 'signal'>): void;
@@ -68,8 +63,8 @@ export interface ViewAnchorHandle {
68
63
  * Open a frame-following window. Auto-closes once stable
69
64
  * or after durationMs. No-op if followGeometry is false.
70
65
  * durationMs is an upper bound, not a minimum: the window closes as soon as
71
- * two frames measure the same rect, so a transition that starts slowly
72
- * (sub-pixel movement in its first frames) may not be followed to the end.
66
+ * two consecutive frames measure the same rect, so a transition that starts
67
+ * slowly (sub-pixel movement in its first frames) may not be followed to the end.
73
68
  */
74
69
  pulse(durationMs?: number): void;
75
70
  }
@@ -1 +1 @@
1
- {"version":3,"file":"view-anchor.d.ts","sourceRoot":"","sources":["../src/view-anchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAQ9D,eAAO,MAAM,qBAAqB,yBAAuB,CAAA;AAWzD,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;;OAQG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAA;IACrD,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAM/D;AAeD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,CA+V/F"}
1
+ {"version":3,"file":"view-anchor.d.ts","sourceRoot":"","sources":["../src/view-anchor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAU,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAgB9D,MAAM,WAAW,iBAAiB;IAChC;;;;;OAKG;IACH,OAAO,EAAE,OAAO,CAAA;IAChB,wCAAwC;IACxC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,CAAA;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;IAC/B;;;;;;;;;;OAUG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,iBAAiB,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAA;IACrD,2CAA2C;IAC3C,OAAO,IAAI,IAAI,CAAA;IACf;;;;;;OAMG;IACH,KAAK,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACjC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,CAM/D;AAyDD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,iBAAiB,GAAG,gBAAgB,CAuT/F"}