view-anchor 0.2.2-alpha.0 → 1.0.0-beta.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 (50) hide show
  1. package/README.md +93 -79
  2. package/README.zh-CN.md +100 -92
  3. package/dist/abort.d.ts +3 -0
  4. package/dist/abort.d.ts.map +1 -0
  5. package/dist/abort.js +9 -0
  6. package/dist/index.d.ts +5 -7
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -3
  9. package/dist/protocol-publisher.d.ts +9 -9
  10. package/dist/protocol-publisher.d.ts.map +1 -1
  11. package/dist/protocol-publisher.js +20 -15
  12. package/dist/protocol-types.d.ts +5 -8
  13. package/dist/protocol-types.d.ts.map +1 -1
  14. package/dist/protocol-types.js +3 -6
  15. package/dist/protocol.d.ts +2 -2
  16. package/dist/protocol.d.ts.map +1 -1
  17. package/dist/protocol.js +10 -2
  18. package/dist/react.d.ts +2 -15
  19. package/dist/react.d.ts.map +1 -1
  20. package/dist/react.js +43 -38
  21. package/dist/{size-advertiser.d.ts → size-anchor.d.ts} +6 -5
  22. package/dist/size-anchor.d.ts.map +1 -0
  23. package/dist/size-anchor.js +131 -0
  24. package/dist/types.d.ts +27 -29
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/view-anchor.d.ts +53 -32
  27. package/dist/view-anchor.d.ts.map +1 -1
  28. package/dist/view-anchor.js +205 -265
  29. package/docs/bidirectional-design.md +39 -55
  30. package/docs/index.html +682 -366
  31. package/docs/mechanism.md +41 -63
  32. package/docs/performance-report.md +12 -54
  33. package/docs/protocol.md +19 -10
  34. package/package.json +6 -16
  35. package/src/abort.ts +9 -0
  36. package/src/index.ts +5 -19
  37. package/src/protocol-publisher.ts +29 -23
  38. package/src/protocol-types.ts +5 -8
  39. package/src/protocol.ts +13 -7
  40. package/src/react.ts +51 -64
  41. package/src/size-anchor.ts +131 -0
  42. package/src/types.ts +30 -34
  43. package/src/view-anchor.ts +241 -284
  44. package/dist/measure-loop.d.ts +0 -24
  45. package/dist/measure-loop.d.ts.map +0 -1
  46. package/dist/measure-loop.js +0 -89
  47. package/dist/size-advertiser.d.ts.map +0 -1
  48. package/dist/size-advertiser.js +0 -84
  49. package/src/measure-loop.ts +0 -101
  50. package/src/size-advertiser.ts +0 -97
package/README.md CHANGED
@@ -2,38 +2,46 @@
2
2
  <img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — keep anything outside the DOM aligned to a DOM element" width="820">
3
3
  </p>
4
4
 
5
- > A high-performance geometry bridge that keeps anything living outside the DOM aligned to a DOM element: an Electron `WebContentsView`, a native webview in another desktop shell, a cross-origin iframe, or any surface you position from a rectangle. Every move and resize is published synchronously with no duplicate frames.
5
+ > Keep an external surface aligned to a DOM element. `view-anchor` measures the element and synchronously gives your code the current geometry whenever it changes.
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
9
9
  [![License](https://img.shields.io/npm/l/view-anchor)](./LICENSE)
10
- [![Node](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
11
10
 
12
11
  [English](./README.md) · [简体中文](./README.zh-CN.md)
13
12
 
14
- > 🎮 **Live demo**: the [3D interactive demo](https://lbb00.github.io/view-anchor/) runs the real core in your browser. Drag the splitter, toggle the panel, and watch the native view follow.
13
+ > 🎮 **Live demo**: [3D interactive demo](https://lbb00.github.io/view-anchor/) runs the real core in your browser. Drag the splitters, scroll the page, and click its buttons directly in the tilted scene: surface A follows its placeholder, and surface B reports its content height back so the page resizes its placeholder.
15
14
 
16
15
  ## The problem
17
16
 
18
- Some things you want to place inside your layout are not DOM nodes. An Electron `WebContentsView` is positioned by the main process. A native webview in another desktop shell is positioned by host code. The document inside a cross-origin iframe only knows what you tell it over `postMessage`. Your layout, whether it is flexbox, dockview, or react-resizable-panels, only moves DOM nodes and has no idea that something else is supposed to sit exactly on top of one of them.
17
+ Some surfaces are positioned by application code rather than by CSS a canvas, a video overlay, an embedded document, or anything you can position from a rectangle. DOM layout moves placeholder elements, but cannot move those external surfaces.
19
18
 
20
- `view-anchor` closes that gap. You point it at a placeholder element. It measures the element and, every time the element moves or resizes, hands the new rectangle to your `publish` callback. What happens next is up to you: `ipcRenderer.send` plus `view.setBounds` in Electron, `postMessage` to an iframe, or a direct call into whatever positions the surface.
19
+ Point `view-anchor` at a placeholder element. It measures it on creation, on `ResizeObserver` notifications, and on window `resize`, then calls your `publish` function. Your function applies, stores, or sends that value however your application already does.
21
20
 
22
- The core has no dependency on Electron, a browser shell, React, or any layout library. It only uses `ResizeObserver`, `requestAnimationFrame`, and `getBoundingClientRect`. React support lives in a separate `view-anchor/react` entry.
21
+ The core has no dependency on a host runtime, React, or a layout library, and uses only `ResizeObserver`, `requestAnimationFrame`, and `getBoundingClientRect`. React support lives in a separate `view-anchor/react` entry.
23
22
 
24
- ## Built for the hot path
23
+ ## What `publish` receives
25
24
 
26
- Geometry updates fire on every resize and, when following a drag, on every animation frame. The library keeps that work predictable:
25
+ `publish` is a synchronous function that the library calls with one of these plain values:
26
+
27
+ - `createViewAnchor` calls `publish({ visible: true, bounds })` while shown, where `bounds` is `{ x, y, width, height }` in CSS pixels from `getBoundingClientRect()`, or `publish({ visible: false })` while hidden. The explicit `visible` flag distinguishes a visible-but-zero-sized element from one that is hidden or unmounted.
28
+ - `createSizeAnchor` calls `publish({ axis, extent })`, where `axis` is `block` (height) or `inline` (width), and `extent` is the target's rounded, non-negative border-box size on that axis in CSS pixels (padding and border included; content-box only where the browser does not report a border box).
29
+
30
+ Return `false` only when the value was not accepted; a later measurement may retry it. Return `true` or nothing after accepting or queueing it. A hidden anchor measures nothing, so a rejected `{ visible: false }` is not retried on its own: call `update()` again to resend it. `useViewAnchor` does this for you on unmount.
31
+
32
+ ## Performance
33
+
34
+ Geometry updates fire on every resize and, when following a drag, on every animation frame.
27
35
 
28
36
  - **Synchronous delivery.** Measurement and publish happen inside the same `ResizeObserver` callback. No timers, no extra frame of lag.
29
- - **Dedupe before allocate.** A rectangle identical to the last accepted one is rejected by comparing four numbers, before any object is created.
30
- - **Frame following only when needed.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then closes itself once the rectangle settles. Idle cost is zero, and hidden or invalid targets are capped at 30 frames.
37
+ - **Dedupe by default.** A `Placement` identical to the last accepted one is not passed to `publish` again; set `dedupe: false` to receive every measurement.
38
+ - **Frame following on demand.** `followGeometry` polls `requestAnimationFrame` during a scroll burst, a splitter drag, or an explicit `pulse()`, then stops once the rectangle settles. Idle cost is zero; hidden or invalid targets are capped at 30 frames.
31
39
  - **O(1) generation changes.** In the protocol layer, moving an anchor to a new generation or clearing it does not touch other anchors.
32
- - **Latest-wins batching.** Messages queued in the same task are merged in a microtask. The newest placement and size for each anchor are sent separately.
40
+ - **Latest-wins batching.** Messages queued in the same task are merged in a microtask; the newest placement and size for each anchor are sent separately.
33
41
  - **Release on disposal.** Disposed handles stop observing and release their target and callback references, even when the caller keeps the handle.
34
- - **Compact, tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
42
+ - **Tree-shakeable core.** Functions are separate exports with `sideEffects: false`; the complete core export is under 3 KB gzipped.
35
43
 
36
- The [performance report](./docs/performance-report.md) contains the reproducible measurements and their environment. They are useful for comparing changes on the same machine, not for predicting DOM layout, Electron IPC, structured clone, or your app's workload.
44
+ See the [performance report](./docs/performance-report.md) for a comparison with 0.2.2 and export sizes.
37
45
 
38
46
  ## Installation
39
47
 
@@ -43,7 +51,7 @@ pnpm add view-anchor
43
51
  npm install view-anchor
44
52
  ```
45
53
 
46
- React is an optional peer dependency. Import the hooks from `view-anchor/react`. The root entry also re-exports `useViewAnchor` for compatibility with `v0.1.2`, so any app that imports `view-anchor` needs React installed. `view-anchor/protocol` does not.
54
+ React is an optional peer dependency, needed only if you import from `view-anchor/react`. The root entry (`view-anchor`) and `view-anchor/protocol` have no React dependency and load without it installed.
47
55
 
48
56
  ## Usage
49
57
 
@@ -52,16 +60,36 @@ React is an optional peer dependency. Import the hooks from `view-anchor/react`.
52
60
  ```ts
53
61
  import { createViewAnchor } from 'view-anchor'
54
62
 
63
+ const publish = (placement) => {
64
+ if (placement.visible) applyBounds(placement.bounds)
65
+ else hideSurface()
66
+ }
67
+
55
68
  const handle = createViewAnchor(target, {
56
- present: true, // mount the native view
57
- publish: (bounds) => { ... }, // receive live rectangles; wire IPC → setBounds
69
+ visible: true,
70
+ publish,
71
+ followScroll: true, // re-measure when any ancestor scrolls
72
+ followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
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
+ dedupe: true, // default; set false to receive every measurement, even unchanged ones
58
76
  })
59
77
 
60
- handle.update({ present, publish }) // apply new options and publish right away
61
- handle.dispose() // stop observing; never publishes again
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
80
+ handle.dispose() // stop observing; never publishes again
62
81
  ```
63
82
 
64
- Set `present: false` to collapse the view. The core publishes a zero rectangle and stops observing. The host can detach the subview while keeping the `WebContents` alive, so re-showing it later is instant.
83
+ Set `visible: false` to collapse the surface. The core publishes `{ visible: false }` and stops observing. Your `publish` function decides whether that removes, hides, or retains the external surface.
84
+
85
+ `createViewAnchor`, `createSizeAnchor`, and `createGeometryBatcher` accept `signal`. Aborting it is equivalent to `dispose()`; an already-aborted signal does not measure, publish, or install listeners.
86
+
87
+ ```ts
88
+ const controller = new AbortController()
89
+ const handle = createViewAnchor(target, { visible: true, publish, signal: controller.signal })
90
+
91
+ controller.abort() // same cleanup as handle.dispose()
92
+ ```
65
93
 
66
94
  ### React
67
95
 
@@ -70,57 +98,44 @@ import { useViewAnchor } from 'view-anchor/react'
70
98
 
71
99
  function DebugPanel({ visible }: { visible: boolean }) {
72
100
  const ref = useViewAnchor({
73
- present: visible,
74
- publish: publishPanelBounds,
101
+ visible,
102
+ publish: publishPanelPlacement,
103
+ followScroll: true,
104
+ followGeometry: true,
75
105
  })
76
- // The native view follows this placeholder div. Hiding the panel
77
- // (visible=false or unmount) collapses it without destroying it.
106
+ // The external surface follows this placeholder. Hiding or unmounting it
107
+ // sends { visible: false }, without deciding how the surface is stored.
78
108
  return <div ref={ref} className="h-full w-full" />
79
109
  }
80
110
  ```
81
111
 
82
- The hook survives React 18 and 19 StrictMode double-mounting without publishing stale frames.
83
-
84
- ### Explicit visibility
85
-
86
- A zero rectangle cannot tell a hidden view from one that is visible but currently 0×0. When that distinction matters, use the `Placement` API. It publishes `{ visible: true, bounds }` or `{ visible: false }` and adds opt-in scroll and geometry following:
87
-
88
- ```ts
89
- import { createPlacementAnchor } from 'view-anchor'
90
-
91
- const handle = createPlacementAnchor(target, {
92
- publish: (placement) => { ... },
93
- followScroll: true, // re-measure when any ancestor scrolls
94
- followGeometry: true, // poll animation frames during scrolls / drags, stop when steady
95
- guardDisplayNone: true, // zero-area or display:none target → { visible: false }
96
- })
97
-
98
- handle.pulse() // open a short frame-following window, e.g. during a CSS transition
99
- ```
100
-
101
- The React version is `usePlacementAnchor` from `view-anchor/react`.
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.
102
113
 
103
114
  ### Let content drive the size
104
115
 
105
- Sometimes the hosted surface's size should come from its own content, for example a toolbar rendered by downstream code. Run `createSizeAdvertiser` inside the hosted document. It reports the content size back so a DOM placeholder in the host can grow to match:
116
+ Sometimes the hosted surface's size should come from its own content, for example a toolbar rendered by downstream code. Run `createSizeAnchor` inside the hosted document. It reports the content size back so a DOM placeholder in the host can grow to match:
106
117
 
107
118
  ```ts
108
- import { createSizeAdvertiser } from 'view-anchor'
119
+ import { createSizeAnchor } from 'view-anchor'
109
120
 
110
- const handle = createSizeAdvertiser(contentWrapper, {
111
- axis: 'block', // one axis per advertiser: block = height, inline = width
112
- publish: (size) => { ... }, // receives { axis, extent }; wire IPC → host
121
+ const publish = (size) => {
122
+ updatePlaceholderSize(size)
123
+ }
124
+
125
+ const handle = createSizeAnchor(contentWrapper, {
126
+ axis: 'block', // one axis per size anchor: block = height, inline = width
127
+ publish,
113
128
  })
114
129
 
115
- handle.update(publish) // swap the publish channel and report the current size again
116
- handle.dispose() // stop observing; never reports again
130
+ handle.update({ publish }) // apply new options and report the current size again; an omitted dedupe resets to true
131
+ handle.dispose() // stop observing; never reports again
117
132
  ```
118
133
 
119
- > **Warning:** the target must shrink to fit its content on the owned axis. If the host sets that size instead, the two sides keep reacting to each other and never settle. See [docs/bidirectional-design.md](./docs/bidirectional-design.md).
134
+ > The target must shrink to fit its content on the owned axis. If the host sets that size instead, the two sides keep reacting to each other and never settle. See [docs/bidirectional-design.md](./docs/bidirectional-design.md).
120
135
 
121
136
  ### Versioned transport across a boundary
122
137
 
123
- The core hands you plain `Bounds`, `Placement`, and `AdvertisedSize` values. Once those values cross a process or origin boundary, over IPC or `postMessage`, you usually want validation and ordering. The optional `view-anchor/protocol` entry adds versioned message envelopes, bounded decoding of untrusted input, a per-anchor sequence guard that drops stale messages, and a microtask batcher:
138
+ The core hands you plain `Bounds`, `Placement`, and `SizeMeasurement` values. If an application passes them through an asynchronous or untrusted channel, it usually needs validation and ordering. The optional `view-anchor/protocol` entry adds versioned message envelopes, bounded decoding, a per-anchor sequence guard that drops stale messages, and a microtask batcher:
124
139
 
125
140
  ```ts
126
141
  import {
@@ -129,59 +144,58 @@ import {
129
144
  decodeGeometryWireValue,
130
145
  } from 'view-anchor/protocol'
131
146
 
132
- // sending side (renderer, iframe, ...)
133
- const batcher = createGeometryBatcher((batch) => ipc.send('geometry', batch))
147
+ // `sendGeometryBatch` is supplied by your application.
148
+ const batcher = createGeometryBatcher(sendGeometryBatch)
134
149
  const publish = createPlacementMessagePublisher(
135
150
  { anchorId: 'editor', generation: 3 },
136
151
  batcher.publish,
137
152
  )
138
153
 
139
- // receiving side (main process, host page, ...)
154
+ // receiving side
140
155
  const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
141
156
  if (decoded.ok) {
142
157
  /* check the sender, then apply only newer messages */
143
158
  }
144
159
  ```
145
160
 
146
- Two rules keep the ordering correct:
147
-
148
161
  - **Keep one publisher per `{ anchorId, generation }`.** The batcher and `createGeometrySequenceGuard` remember the highest sequence number for each message kind per anchor. A publisher rebuilt for the same address restarts at sequence 1 and its messages are dropped as stale. In React, hold it in `useMemo` or `useRef`. Bump `generation` when you really want a fresh start.
149
- - **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger. A batching publisher returns `true` once queued and owns any later retries.
162
+ - **A synchronous publisher returns `false` to say "not accepted".** The core then retries the same geometry on the next trigger (a rejected `{ visible: false }` waits for the next `update()`). A batching publisher returns `true` once queued and owns any later retries.
150
163
 
151
164
  The full contract is in [docs/protocol.md](./docs/protocol.md).
152
165
 
153
166
  ## API
154
167
 
155
- | Export | Kind | Purpose |
156
- | --------------------------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- |
157
- | `createViewAnchor(target, opts)` | function | Measure a DOM element and publish live bounds. A zero rect means collapsed. |
158
- | `createPlacementAnchor(target, opts)` | function | Same core with explicit `Placement` visibility, opt-in `followScroll` / `followGeometry` / `guardDisplayNone`, and `pulse()`. |
159
- | `measurePlacement(target)` | function | Pure measurement: wraps the target rect as `{ visible: true, bounds }`. |
160
- | `createSizeAdvertiser(target, opts)` | function | Reverse direction: report the view's own content size to the host. |
161
- | `useViewAnchor(opts)` from `view-anchor/react` | hook | Returns a ref callback for a placeholder element. |
162
- | `usePlacementAnchor(opts)` from `view-anchor/react` | hook | React adapter for the `Placement` API, including `followScroll` and `followGeometry`. |
163
- | `Bounds` | type | `{ x, y, width, height }` in CSS pixels. |
164
- | `Placement` | type | `{ visible: true; bounds } \| { visible: false }`. |
165
- | `ViewAnchorOptions` / `ViewAnchorHandle` | type | Options and handle for `createViewAnchor`. |
166
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | type | Options and handle for `createPlacementAnchor`. |
167
- | `UseViewAnchorOptions` / `ViewAnchorRef` from `view-anchor/react` | type | Options and ref shape for `useViewAnchor`. |
168
- | `UsePlacementAnchorOptions` / `PlacementAnchorRef` from `view-anchor/react` | type | Options and ref shape for `usePlacementAnchor`. |
169
- | `AdvertisedAxis` / `AdvertisedSize` | type | Axis and payload types for the reverse direction. |
170
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | type | Options and handle for `createSizeAdvertiser`. |
171
- | `view-anchor/protocol` | functions + types | Versioned messages, strict decoding, sequence guards, message publishers, and microtask batching. |
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. |
182
+
183
+ ## Versioning
184
+
185
+ Within 1.x, `view-anchor` does not remove or rename any public export from `.`, `view-anchor/react`, or `view-anchor/protocol`, and does not change the default behavior for input that is already valid. Minor releases add functionality; patch releases fix bugs. An interface slated for removal is deprecated first and only removed in 2.0.
172
186
 
173
187
  ## Documentation
174
188
 
175
- - [docs/mechanism.md](./docs/mechanism.md): how the forward direction works. Synchronous publishing, stale-frame safety, the `present` / zero-rect / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
189
+ - [docs/mechanism.md](./docs/mechanism.md): how the forward direction works. Synchronous publishing, default dedup and `dedupe: false`, the `Placement` / visibility / unmount contract, StrictMode behaviour. Includes the interactive 3D demo at [docs/index.html](./docs/index.html).
176
190
  - [docs/bidirectional-design.md](./docs/bidirectional-design.md): running both directions at once. Why the forward path is synchronous while the reverse path uses animation frames, single-axis ownership, and where the trust boundary sits.
177
191
  - [docs/protocol.md](./docs/protocol.md): message envelopes, validation, ordering, batching, and what happens on failure.
178
- - [docs/performance-report.md](./docs/performance-report.md): reproducible CPU, heap, RSS, extreme-case, V8, and export-size measurements.
192
+ - [docs/performance-report.md](./docs/performance-report.md): performance summary and export sizes.
179
193
 
180
194
  ## Contributing
181
195
 
182
- Issues and pull requests are welcome. Before submitting, run `pnpm lint`, `pnpm format:check`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` prints the data used to update the performance report.
196
+ Before submitting, run `pnpm lint`, `pnpm format:check`, `pnpm check-types`, `pnpm test`, and `pnpm build`. `pnpm benchmark` prints the data used to update the performance report.
183
197
 
184
- Changes that affect a published version must include a Changeset. Run `pnpm changeset`, select the version bump, and describe the user-visible change. Merging it into `main` opens a version PR; merging that PR publishes the package.
198
+ Changes that affect a published version must include a Changeset. Run `pnpm changeset`, select the version bump, and describe the user-visible change. Merging into `main` opens a version PR; merging that PR publishes the package.
185
199
 
186
200
  ## License
187
201
 
package/README.zh-CN.md CHANGED
@@ -1,39 +1,31 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — 让 DOM 之外的画面贴合 DOM 元素" width="820">
2
+ <img src="https://raw.githubusercontent.com/lbb00/view-anchor/main/assets/banner.svg" alt="view-anchor — 让 DOM 之外的画面实时对齐 DOM 元素" width="820">
3
3
  </p>
4
4
 
5
- > 高性能几何桥接库,让任何"不在 DOM 里的东西"始终贴合某个 DOM 元素:Electron 的 `WebContentsView`、其他桌面壳里的原生 webview、跨域 iframe,或者任何你能用一个矩形来定位的画面。每次移动和缩放都同步发布、不会重复发帧。
5
+ > DOM 之外的画面实时对齐 DOM 元素:测量占位元素,逐帧同步最新矩形,拖拽无延迟。
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/view-anchor)](https://www.npmjs.com/package/view-anchor)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/view-anchor)](https://www.npmjs.com/package/view-anchor)
9
9
  [![License](https://img.shields.io/npm/l/view-anchor)](./LICENSE)
10
- [![Node](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
11
10
 
12
11
  [English](./README.md) · [简体中文](./README.zh-CN.md)
13
12
 
14
- > 🎮 **在线 Demo**:[3D 交互演示](https://lbb00.github.io/view-anchor/) 在浏览器里跑的是真实核心代码。拖动分栏、切换面板显示,看原生视图实时跟随。
13
+ > [在线演示](https://lbb00.github.io/view-anchor/):直接在倾斜的 3D 场景里拖动分隔条、滚动页面、点击按钮。画面 A 跟随占位元素;画面 B 把自己的内容高度报回页面,页面据此调整它的占位。
15
14
 
16
- ## 要解决的问题
15
+ ## 它解决什么问题
17
16
 
18
- 有些你想放进布局里的东西并不是 DOM 节点。Electron 的 `WebContentsView` 由主进程定位;其他桌面壳里的原生 webview 由宿主代码定位;跨域 iframe 里的文档只知道你通过 `postMessage` 告诉它的信息。而你的布局,不管是 flexbox、dockview 还是 react-resizable-panels,只会移动 DOM 节点,并不知道有别的东西需要正好盖在其中某个节点上面。
17
+ 有些画面的位置由应用自己摆放(叠层、嵌入式文档或能根据矩形定位的渲染表面)。DOM 布局会移动占位元素,但不会自动更新这些画面的位置。
19
18
 
20
- `view-anchor` 负责补上这一段。你把它指向一个占位元素,它测量这个元素,元素每次移动或缩放时,把新的矩形交给你的 `publish` 回调。接下来怎么用由你决定:在 Electron 里接 `ipcRenderer.send` 和 `view.setBounds`,对 iframe 就 `postMessage`,或者直接调用任何负责定位那个画面的代码。
19
+ `view-anchor` 负责测量占位元素,在尺寸变化或窗口大小变化时同步调用你提供的 `publish` 函数。函数如何使用这个值由应用决定。
21
20
 
22
- 核心不依赖 Electron、任何浏览器壳、React 或布局库,只用到 `ResizeObserver`、`requestAnimationFrame` 和 `getBoundingClientRect`。React 支持放在单独的 `view-anchor/react` 入口里。
21
+ ## `publish` 收到什么
23
22
 
24
- ## 为热路径而写
23
+ `publish` 是一个同步回调,不同接口会传入不同的普通对象:
25
24
 
26
- 几何更新在每次缩放时都会触发,跟随拖拽时更是每一帧都触发。这个库让这条路径保持可控:
25
+ - `createViewAnchor` 在显示时调用 `publish({ visible: true, bounds })`(`bounds` 是相对视口、取整后的 CSS 像素矩形 `{ x, y, width, height }`),隐藏时调用 `publish({ visible: false })`。显式的 `visible` 用于区分“可见但恰好是 0×0”和“已隐藏或未挂载”。
26
+ - `createSizeAnchor` 调用 `publish({ axis, extent })`。`axis` 为 `block`(高度)或 `inline`(宽度),`extent` 是目标元素在该轴上的 border-box 尺寸(包含 padding 和 border;浏览器不提供 border-box 时退回 content-box),单位 CSS 像素,取整且不小于零。
27
27
 
28
- - **同步发布。** 测量和发布在同一个 `ResizeObserver` 回调里完成。没有定时器,不多等一帧。
29
- - **先去重,再分配。** 和上一次已接受的矩形完全相同的结果,只比较四个数字就会被丢弃,不会创建任何对象。
30
- - **只在需要时逐帧跟随。** `followGeometry` 只在滚动、拖动分栏或显式调用 `pulse()` 时才开始按 `requestAnimationFrame` 轮询,矩形稳定后自动停止。空闲时开销为零,隐藏或无效目标最多跟 30 帧。
31
- - **generation 切换是 O(1)。** 协议层里,把某个锚点换到新的 generation 或清除它,不会碰到其他锚点。
32
- - **合并批量,只发最新。** 同一个任务里排队的消息在一个微任务里合并,每个锚点分别发送最新的位置和尺寸。
33
- - **销毁后释放引用。** 句柄 `dispose()` 后会停止观察,并释放目标元素和回调;调用方继续持有句柄也不会保留它们。
34
- - **核心紧凑,可摇树。** 每个函数都是独立导出,并声明了 `sideEffects: false`;完整核心导出 gzip 后小于 3 KB。
35
-
36
- [性能报告](./docs/performance-report.md) 保留可复现的测量数据和环境。它适合比较同一台机器上的改动,不用于预测 DOM layout、Electron IPC、structured clone 或你的应用负载。
28
+ 数据已接收或已经入队时,返回 `true` 或不返回值;当前不能接收时才返回 `false`,下一次测量会重试被拒绝的值。隐藏状态下不再测量,所以被拒绝的 `{ visible: false }` 不会自动重发,需要再调用一次 `update()`;`useViewAnchor` 会在卸载时替你补发。
37
29
 
38
30
  ## 安装
39
31
 
@@ -43,84 +35,94 @@ pnpm add view-anchor
43
35
  npm install view-anchor
44
36
  ```
45
37
 
46
- React 是可选的 peer dependency。Hook `view-anchor/react` 导入。为了兼容 `v0.1.2`,根入口也会重新导出 `useViewAnchor`,所以只要导入了 `view-anchor` 就需要装 React。`view-anchor/protocol` 不需要。
38
+ React 是可选 peer dependency,只有从 `view-anchor/react` 导入时才需要。根入口(`view-anchor`)和 `view-anchor/protocol` 不依赖 React,未安装 React 也能加载。
47
39
 
48
- ## 用法
40
+ ## 使用
49
41
 
50
42
  ### 跟随一个 DOM 元素
51
43
 
52
44
  ```ts
53
45
  import { createViewAnchor } from 'view-anchor'
54
46
 
55
- const handle = createViewAnchor(target, {
56
- present: true, // 挂载原生视图
57
- publish: (bounds) => { ... }, // 收到实时矩形;接 IPC → setBounds
47
+ const publish = (placement) => {
48
+ if (placement.visible) applyBoundsToExternalSurface(placement.bounds)
49
+ else hideExternalSurface()
50
+ }
51
+
52
+ const handle = createViewAnchor(placeholderEl, {
53
+ visible: true,
54
+ publish,
55
+ followScroll: true, // 祖先容器滚动时重新测量
56
+ followGeometry: true, // 拖拽或滚动期间逐帧测量;稳定后停止
57
+ treatZeroAreaAsHidden: true, // 零面积或 display:none 时发布 { visible: false }
58
+ holdSelector: '[role="separator"]', // 默认值;命中该选择器的 pointerdown 按住期间保持 followGeometry 开启,传 null 关闭
59
+ dedupe: true, // 默认值;传 false 让每次测量都发布,即使值没变
58
60
  })
59
61
 
60
- handle.update({ present, publish }) // 应用新选项并立刻发布一次
61
- handle.dispose() // 停止观察;之后不会再发布
62
+ handle.update({ visible: true, publish }) // 更新选项并立即重新发布
63
+ handle.pulse() // 位置变了但没有任何观察器能感知时(如一次类名切换让目标移动了但尺寸没变)主动重测;稳定后自动停止
64
+ handle.dispose() // 停止观察;之后不会再发布
62
65
  ```
63
66
 
64
- 把 `present` 设为 `false` 可以收起视图。核心会发布一个零矩形并停止观察。宿主可以把子视图摘下来但保留 `WebContents`,这样之后再显示时是即时的。
67
+ 把 `visible` 设为 `false` 会发布 `{ visible: false }` 并停止观察。你的 `publish` 函数可以把它解释为隐藏、移除,或保留外部画面。
68
+
69
+ `createViewAnchor`、`createSizeAnchor` 和 `createGeometryBatcher` 都支持 `signal`。调用 `AbortController.abort()` 的清理效果等同于 `dispose()`;创建前已经 abort 的 signal 不会开始测量或安装监听。
70
+
71
+ ```ts
72
+ const controller = new AbortController()
73
+ const handle = createViewAnchor(placeholderEl, {
74
+ visible: true,
75
+ publish,
76
+ signal: controller.signal,
77
+ })
78
+
79
+ controller.abort()
80
+ ```
65
81
 
66
82
  ### React
67
83
 
68
84
  ```tsx
69
85
  import { useViewAnchor } from 'view-anchor/react'
70
86
 
71
- function DebugPanel({ visible }: { visible: boolean }) {
87
+ function ExternalSurfaceContainer({ visible }: { visible: boolean }) {
72
88
  const ref = useViewAnchor({
73
- present: visible,
74
- publish: publishPanelBounds,
89
+ visible,
90
+ publish: applyPlacementToExternalSurface,
91
+ followScroll: true,
92
+ followGeometry: true,
75
93
  })
76
- // 原生视图跟随这个占位 div。隐藏面板(visible=false 或卸载)
77
- // 只会收起视图,不会销毁它。
94
+
78
95
  return <div ref={ref} className="h-full w-full" />
79
96
  }
80
97
  ```
81
98
 
82
- Hook React 18 和 19 的 StrictMode 双重挂载下不会发出过期的帧。
99
+ 元素卸载时,Hook 会先发布 `{ visible: false }`,再释放监听。它兼容 React 18 和 19 的 StrictMode 重挂载,不会因此发布过期帧。
83
100
 
84
- ### 显式可见性
101
+ 它每次调用都会重新应用选项,规则与 `createViewAnchor` 和 `update()` 一致:省略任何一项都会重置为默认值,而不是沿用上次的值。省略 `treatZeroAreaAsHidden`、`followScroll` 或 `followGeometry` 会重置为 `false`;省略 `holdSelector` 会重置为 `[role="separator"]`;省略 `dedupe` 会重置为 `true`。要关闭 `holdSelector` / `dedupe`,传 `null` / `false`。
85
102
 
86
- 零矩形分不清"视图被隐藏了"和"视图可见但此刻正好是 0×0"。需要区分这两种情况时,用 `Placement` API。它发布的是 `{ visible: true, bounds }` 或 `{ visible: false }`,并提供可选的滚动跟随和几何跟随:
103
+ ### 由内容决定占位尺寸
87
104
 
88
- ```ts
89
- import { createPlacementAnchor } from 'view-anchor'
90
-
91
- const handle = createPlacementAnchor(target, {
92
- publish: (placement) => { ... },
93
- followScroll: true, // 任意祖先滚动时重新测量
94
- followGeometry: true, // 滚动 / 拖拽期间逐帧轮询,稳定后停止
95
- guardDisplayNone: true, // 零面积或 display:none 的目标 → { visible: false }
96
- })
97
-
98
- handle.pulse() // 打开一小段逐帧跟随窗口,比如 CSS 过渡期间
99
- ```
100
-
101
- React 版本是 `view-anchor/react` 里的 `usePlacementAnchor`。
102
-
103
- ### 由内容决定尺寸
104
-
105
- 有时被嵌入的那个画面的尺寸应该由它自己的内容决定,比如一条由下游代码渲染的工具栏。这种情况在被嵌入的文档里运行 `createSizeAdvertiser`,它把内容尺寸报告回去,宿主里的 DOM 占位元素就能跟着长到一样大:
105
+ 当外部画面的内容尺寸需要反过来调整占位元素时,在内容所在的文档中使用 `createSizeAnchor`:
106
106
 
107
107
  ```ts
108
- import { createSizeAdvertiser } from 'view-anchor'
108
+ import { createSizeAnchor } from 'view-anchor'
109
+
110
+ const publish = updatePlaceholderSize
109
111
 
110
- const handle = createSizeAdvertiser(contentWrapper, {
111
- axis: 'block', // 一个 advertiser 只管一个轴:block = 高度,inline = 宽度
112
- publish: (size) => { ... }, // 收到 { axis, extent };接 IPC → 宿主
112
+ const handle = createSizeAnchor(contentWrapper, {
113
+ axis: 'block', // 一个 size anchor 只负责一个轴:block = 高度,inline = 宽度
114
+ publish,
113
115
  })
114
116
 
115
- handle.update(publish) // 换一个发布通道,并立刻再报告一次当前尺寸
116
- handle.dispose() // 停止观察;之后不会再报告
117
+ handle.update({ publish }) // 应用新选项并立即重新报告当前尺寸;省略 dedupe 会重置为 true
118
+ handle.dispose()
117
119
  ```
118
120
 
119
- > **注意:** 目标元素在它负责的那个轴上必须是随内容收缩的。如果这个尺寸反过来由宿主设置,两边会互相触发、永远停不下来。见 [docs/bidirectional-design.md](./docs/bidirectional-design.md)。
121
+ 目标元素在它负责的轴上必须随内容伸缩。若应用也反过来在同一轴上强制设置尺寸,双方会互相触发布局,难以稳定。详见 [双向几何设计](./docs/bidirectional-design.md)。
120
122
 
121
- ### 跨边界的带版本传输
123
+ ### 需要传递、校验和排序时
122
124
 
123
- 核心交给你的是普通的 `Bounds`、`Placement` 和 `AdvertisedSize` 值。这些值一旦通过 IPC 或 `postMessage` 跨过进程或源的边界,通常就需要校验和排序。可选的 `view-anchor/protocol` 入口提供带版本的消息信封、对不可信输入的有界解码、按锚点丢弃过期消息的序列守卫,以及一个微任务批处理器:
125
+ 核心只交付 `Bounds`、`Placement` 和 `SizeMeasurement`。如果应用需要把这些值交给别的页面、环境或异步通道,可选的 `view-anchor/protocol` 提供消息版本、输入校验、乱序过滤和微任务合并。
124
126
 
125
127
  ```ts
126
128
  import {
@@ -129,57 +131,63 @@ import {
129
131
  decodeGeometryWireValue,
130
132
  } from 'view-anchor/protocol'
131
133
 
132
- // 发送方(渲染进程、iframe 等)
133
- const batcher = createGeometryBatcher((batch) => ipc.send('geometry', batch))
134
+ // sendGeometryBatch 由应用实现:接收一个 batch,并同步返回是否已接收。
135
+ const batcher = createGeometryBatcher(sendGeometryBatch)
134
136
  const publish = createPlacementMessagePublisher(
135
137
  { anchorId: 'editor', generation: 3 },
136
138
  batcher.publish,
137
139
  )
138
140
 
139
- // 接收方(主进程、宿主页面等)
140
141
  const decoded = decodeGeometryWireValue(received, { maxMessages: 100 })
141
142
  if (decoded.ok) {
142
- /* 先校验发送方,再只应用更新的消息 */
143
+ // 先验证来源,再只应用更新的消息。
143
144
  }
144
145
  ```
145
146
 
146
- 两条规则保证顺序正确:
147
+ 同一个 `{ anchorId, generation }` 应只保留一个 publisher 实例。针对同一地址重新创建 publisher 会让序号回到 1,接收端会把新消息当作过期消息丢弃。需要重新开始时,把 `generation` 加一。
148
+
149
+ 完整约定见 [通信协议](./docs/protocol.md)。
150
+
151
+ ## 性能边界
147
152
 
148
- - **每个 `{ anchorId, generation }` 只保留一个 publisher。** batcher 和 `createGeometrySequenceGuard` 会分别记住每个锚点的位置与尺寸消息的最大序号。针对同一地址重建 publisher 会让序号从 1 重新开始,它发的消息会被当成过期而丢弃。在 React 里用 `useMemo` 或 `useRef` 持有它。确实需要重新开始时,把 `generation` 加一。
149
- - **同步 publisher 返回 `false` 表示"未接受"。** 核心会在下一次触发时重试同一份几何数据。批处理 publisher 入队后就返回 `true`,之后的重试由它自己负责。
153
+ - 默认去重:与上一次已发布的 `Placement`/尺寸完全相同不会再次调用 `publish`;传 `dedupe: false` 可以让每次测量都发布。
154
+ - `followGeometry` 只在需要时启动动画帧轮询,稳定后停止。
155
+ - `createGeometryBatcher` 会合并同一微任务内的更新;每个锚点的位置和尺寸各保留最新值。
156
+ - `dispose()` 或 abort 会停止监听并释放长期持有的元素和回调引用。
150
157
 
151
- 完整约定见 [docs/protocol.md](./docs/protocol.md)。
158
+ 0.2.2 的对比和导出体积见 [性能报告](./docs/performance-report.md)。
152
159
 
153
160
  ## API
154
161
 
155
- | 导出 | 类型 | 用途 |
156
- | ---------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------- |
157
- | `createViewAnchor(target, opts)` | 函数 | 测量 DOM 元素并发布实时 bounds。零矩形表示已收起。 |
158
- | `createPlacementAnchor(target, opts)` | 函数 | 同一个核心,带显式 `Placement` 可见性、可选的 `followScroll` / `followGeometry` / `guardDisplayNone`,以及 `pulse()`。 |
159
- | `measurePlacement(target)` | 函数 | 纯测量:把目标矩形包成 `{ visible: true, bounds }`。 |
160
- | `createSizeAdvertiser(target, opts)` | 函数 | 反向:把视图自身的内容尺寸报告给宿主。 |
161
- | `useViewAnchor(opts)`,来自 `view-anchor/react` | Hook | 返回用于占位元素的 ref 回调。 |
162
- | `usePlacementAnchor(opts)`,来自 `view-anchor/react` | Hook | `Placement` API React 适配,包含 `followScroll` `followGeometry`。 |
163
- | `Bounds` | 类型 | CSS 像素单位的 `{ x, y, width, height }`。 |
164
- | `Placement` | 类型 | `{ visible: true; bounds } \| { visible: false }`。 |
165
- | `ViewAnchorOptions` / `ViewAnchorHandle` | 类型 | `createViewAnchor` 的选项和句柄。 |
166
- | `PlacementAnchorOptions` / `PlacementAnchorHandle` | 类型 | `createPlacementAnchor` 的选项和句柄。 |
167
- | `UseViewAnchorOptions` / `ViewAnchorRef`,来自 `view-anchor/react` | 类型 | `useViewAnchor` 的选项和 ref 形状。 |
168
- | `UsePlacementAnchorOptions` / `PlacementAnchorRef`,来自 `view-anchor/react` | 类型 | `usePlacementAnchor` 的选项和 ref 形状。 |
169
- | `AdvertisedAxis` / `AdvertisedSize` | 类型 | 反向方向的轴和负载类型。 |
170
- | `SizeAdvertiserOptions` / `SizeAdvertiserHandle` | 类型 | `createSizeAdvertiser` 的选项和句柄。 |
171
- | `view-anchor/protocol` | 函数 + 类型 | 带版本的消息、严格解码、序列守卫、消息 publisher 和微任务批处理。 |
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` | 函数 + 类型 | 消息封装、校验、排序和批处理。 |
176
+
177
+ ## 版本承诺
178
+
179
+ 1.x 版本内,`view-anchor` 不会删除或重命名 `.`、`view-anchor/react`、`view-anchor/protocol` 的任何公开导出,也不会改变已有合法输入的默认行为。minor 版本只新增功能,patch 版本只修 bug。要移除的接口会先标记为废弃,等到 2.0 才真正删除。
172
180
 
173
181
  ## 文档
174
182
 
175
- - [docs/mechanism.md](./docs/mechanism.md):正向方向的原理。同步发布、防止过期帧、`present` / 零矩形 / 卸载的约定、StrictMode 下的行为。内含 [docs/index.html](./docs/index.html) 的 3D 交互演示。
176
- - [docs/bidirectional-design.md](./docs/bidirectional-design.md):两个方向同时运行时的设计。为什么正向是同步的而反向走动画帧、单轴归属,以及信任边界在哪。
177
- - [docs/protocol.md](./docs/protocol.md):消息信封、校验、排序、批处理,以及失败时会发生什么。
178
- - [docs/performance-report.md](./docs/performance-report.md):可复现的 CPU、堆、RSS、极端场景、V8 和导出体积测量。
183
+ - [机制说明](./docs/mechanism.md):正向测量的触发条件、默认去重与 `dedupe: false`、可见性与 React 生命周期。
184
+ - [双向几何设计](./docs/bidirectional-design.md):内容尺寸反向调整占位元素时的单轴约束。
185
+ - [通信协议](./docs/protocol.md):消息格式、校验、排序和批处理。
186
+ - [性能报告](./docs/performance-report.md):性能结论和导出体积。
179
187
 
180
188
  ## 参与贡献
181
189
 
182
- 欢迎提 issue 和 pull request。提交前请运行 `pnpm lint`、`pnpm format:check`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 会输出用于更新性能报告的数据。
190
+ 提交前运行 `pnpm lint`、`pnpm format:check`、`pnpm check-types`、`pnpm test` 和 `pnpm build`。`pnpm benchmark` 用于更新性能报告。
183
191
 
184
192
  ## 许可证
185
193
 
@@ -0,0 +1,3 @@
1
+ /** Attach a one-shot abort listener and return a function that detaches it. */
2
+ export declare function watchAbort(signal: AbortSignal | undefined, dispose: () => void): () => void;
3
+ //# sourceMappingURL=abort.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.d.ts","sourceRoot":"","sources":["../src/abort.ts"],"names":[],"mappings":"AAEA,+EAA+E;AAC/E,wBAAgB,UAAU,CAAC,MAAM,EAAE,WAAW,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAK3F"}