@matterfact/embed 0.6.0 → 0.8.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 (52) hide show
  1. package/README.md +99 -1
  2. package/dist/chunk-PNSYFXXU.js +791 -0
  3. package/dist/chunk-PNSYFXXU.js.map +1 -0
  4. package/dist/chunk-R2ZEJARX.js +776 -0
  5. package/dist/chunk-R2ZEJARX.js.map +1 -0
  6. package/dist/{chunk-4Q2ROXLR.js → chunk-UD7CAQXV.js} +2 -2
  7. package/dist/{chunk-7I37ZFAJ.js → chunk-UQCETVRF.js} +2 -2
  8. package/dist/chunk-W52Q7G4J.js +3 -0
  9. package/dist/chunk-W52Q7G4J.js.map +7 -0
  10. package/dist/context-ARBB2XD6.js +3 -0
  11. package/dist/{context-YX2KXFLI.js.map → context-ARBB2XD6.js.map} +1 -1
  12. package/dist/{context-WU2F5CBC.js → context-MVGSYIMB.js} +2 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +4 -4
  15. package/dist/index.cjs +986 -159
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +140 -31
  18. package/dist/index.d.ts +140 -31
  19. package/dist/index.js +542 -130
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +1012 -165
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +33 -1
  24. package/dist/react.d.ts +33 -1
  25. package/dist/react.js +561 -132
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-MUXE7KXX.js → snapshot-GL4YBMXD.js} +3 -3
  28. package/dist/{snapshot-MUXE7KXX.js.map → snapshot-GL4YBMXD.js.map} +1 -1
  29. package/dist/{snapshot-JOGZWESK.js → snapshot-UGTXZVB6.js} +2 -2
  30. package/examples/embed-demo/.env.example +5 -1
  31. package/examples/embed-demo/README.md +37 -0
  32. package/examples/embed-demo/package.json +1 -1
  33. package/examples/embed-demo/src/App.tsx +82 -44
  34. package/examples/embed-demo/src/HoistDemo.tsx +390 -0
  35. package/examples/embed-demo/src/config.ts +34 -4
  36. package/examples/embed-demo/src/main.tsx +7 -0
  37. package/examples/embed-demo/src/mockXH.ts +492 -0
  38. package/examples/embed-demo/src/placement.tsx +43 -0
  39. package/examples/embed-demo/src/styles.css +147 -2
  40. package/examples/embed-demo/vite.config.ts +2 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-AXKXNYRT.js +0 -381
  43. package/dist/chunk-AXKXNYRT.js.map +0 -1
  44. package/dist/chunk-D7R6WVHG.js +0 -2
  45. package/dist/chunk-D7R6WVHG.js.map +0 -7
  46. package/dist/chunk-RS6RMZ77.js +0 -396
  47. package/dist/chunk-RS6RMZ77.js.map +0 -1
  48. package/dist/context-YX2KXFLI.js +0 -3
  49. /package/dist/{chunk-4Q2ROXLR.js.map → chunk-UD7CAQXV.js.map} +0 -0
  50. /package/dist/{chunk-7I37ZFAJ.js.map → chunk-UQCETVRF.js.map} +0 -0
  51. /package/dist/{context-WU2F5CBC.js.map → context-MVGSYIMB.js.map} +0 -0
  52. /package/dist/{snapshot-JOGZWESK.js.map → snapshot-UGTXZVB6.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -18,7 +18,7 @@
18
18
  * shapes are right, "deeper integration" is a transport swap (postMessage ->
19
19
  * direct call), not a rewrite. So model the DOMAIN here, not the plumbing.
20
20
  */
21
- declare const PROTOCOL_VERSION = 1;
21
+ declare const PROTOCOL_VERSION = 2;
22
22
  /** Every message is namespaced so we never collide with the host page's own postMessage traffic. */
23
23
  declare const CHANNEL = "mf-embed";
24
24
  /**
@@ -143,6 +143,13 @@ interface HostTool {
143
143
  * requires the user to confirm, every time — see the note on ToolCall.
144
144
  */
145
145
  readOnly?: boolean;
146
+ /**
147
+ * The tiered confirm policy, set by the host (e.g. `window.matterfact.hoist.actions`):
148
+ * `'required'` ⇒ the widget must show a confirm card and get the user's approval
149
+ * before every call; `'auto'` ⇒ it may call without asking. Absent for a tool that
150
+ * predates this policy (treat as `'required'` — the safer default).
151
+ */
152
+ confirm?: 'auto' | 'required';
146
153
  }
147
154
  type HostToWidget = {
148
155
  type: 'host.ready';
@@ -183,6 +190,23 @@ type HostToWidget = {
183
190
  } | {
184
191
  type: 'host.theme';
185
192
  mode: 'light' | 'dark';
193
+ }
194
+ /**
195
+ * Where the widget currently IS. Sent on mount and after anything that moves it
196
+ * (drag, snap, mode change, viewport resize).
197
+ *
198
+ * The widget cannot work this out for itself — it can't see the viewport, and the
199
+ * loader owns the box. Without it the widget renders its launcher against a stale
200
+ * guess: a launcher dragged to the left edge kept drawing its circle bottom-RIGHT
201
+ * inside a left-anchored box, so the circle jumped as soon as the box grew for the
202
+ * hover pill. `growth` is the direction the pill/menu/panel must open (toward the
203
+ * viewport centre); `mode` decides whether the launcher is a circle or an edge tab.
204
+ */
205
+ | {
206
+ type: 'host.geometry';
207
+ mode: 'float' | 'dock';
208
+ dockSide: 'left' | 'right';
209
+ growth: 'tl' | 'tr' | 'bl' | 'br';
186
210
  };
187
211
  /**
188
212
  * The agent asking the host to do something.
@@ -245,14 +269,57 @@ type WidgetToHost = {
245
269
  height: number;
246
270
  }
247
271
  /**
248
- * Maximize / restore. Expanded makes the iframe fill the viewport (with a small
249
- * margin) so the composer's `/` picker, mention previews, and the transcript have
250
- * room the corner popup can't give them. The loader owns the iframe size, so only
251
- * it can do this `widget.resize` (height-only) can't widen the popup.
272
+ * Dock / undock the open panel.
273
+ *
274
+ * Docked is a FULL-HEIGHT SIDE PANEL on the launcher's own side left corners dock
275
+ * to the left edge, right corners to the right, unless `setDockSide` overrides it.
276
+ * This replaces the old `setExpanded`, which was hardcoded to the right edge and so
277
+ * threw the panel across the screen whenever the launcher sat on the left.
278
+ *
279
+ * The loader owns the box, so only it can do this — `widget.resize` (height-only)
280
+ * can neither widen the panel nor move it to an edge.
281
+ */
282
+ | {
283
+ type: 'widget.setMode';
284
+ mode: 'float' | 'dock';
285
+ }
286
+ /** Which edge the tab and its panel live on. */
287
+ | {
288
+ type: 'widget.setDockSide';
289
+ side: 'left' | 'right';
290
+ }
291
+ /** Place the FLOATING launcher at a specific corner (the menu's snap picker). */
292
+ | {
293
+ type: 'widget.snapCorner';
294
+ corner: 'tl' | 'tr' | 'bl' | 'br';
295
+ }
296
+ /**
297
+ * Resize the COLLAPSED launcher's host box so the widget can paint outside the 56px
298
+ * circle — the hover pill (`{w:180,h:56}`) or the context menu (its own box). The
299
+ * iframe clips to the host box, so anything beside the circle is invisible until the
300
+ * host makes room. `{w:56,h:56}` returns to the bare circle. Ignored while open.
301
+ */
302
+ | {
303
+ type: 'widget.setLauncherRegion';
304
+ w: number;
305
+ h: number;
306
+ }
307
+ /**
308
+ * User resize of the floating panel, via the grip on its inner corner.
309
+ *
310
+ * Same contract as the launcher drag below, and for the same reason: the press is
311
+ * captured by the iframe, so the host cannot see the moves. The widget tracks the
312
+ * gesture and sends SCREEN-coordinate deltas; the loader owns the sizing, the
313
+ * per-corner sign of the delta, and the clamp.
252
314
  */
253
315
  | {
254
- type: 'widget.setExpanded';
255
- expanded: boolean;
316
+ type: 'widget.resizeStart';
317
+ } | {
318
+ type: 'widget.resizeMove';
319
+ dx: number;
320
+ dy: number;
321
+ } | {
322
+ type: 'widget.resizeEnd';
256
323
  }
257
324
  /**
258
325
  * Launcher drag. The widget tracks the gesture; the host only moves the element.
@@ -278,18 +345,7 @@ type WidgetToHost = {
278
345
  } | {
279
346
  type: 'widget.dragEnd';
280
347
  }
281
- /**
282
- * Grow/shrink the iframe to fit the launcher's context menu. The collapsed launcher is
283
- * only ~64px tall, so a menu rendered inside it would be clipped by the iframe's own
284
- * bounds — the host has to make room before it can be seen. `widget.resize` can't do
285
- * this: it's height-only AND ignored while closed.
286
- */
287
- | {
288
- type: 'widget.setMenu';
289
- open: boolean;
290
- height: number;
291
- }
292
- /** Put the launcher back in the default corner and forget the dragged position. */
348
+ /** Put the launcher back in the default corner and forget the saved size/dock. */
293
349
  | {
294
350
  type: 'widget.resetPos';
295
351
  }
@@ -386,7 +442,7 @@ interface LoaderConfig {
386
442
  * own side panel, tool drawer or tab and wants the agent to live there.
387
443
  *
388
444
  * This inverts who owns the chrome. In corner mode the widget owns everything: it draws
389
- * its own launcher and it tells us how big to be (setOpen/setExpanded/resize). Inside
445
+ * its own launcher and it tells us how big to be (setOpen/setDocked/resize). Inside
390
446
  * someone else's panel, both are theirs — a widget that resized its host's drawer, or
391
447
  * drew a second floating bubble inside it, is just broken. So here we fill the
392
448
  * container, ignore the size messages, and the widget skips the launcher and renders
@@ -422,17 +478,44 @@ declare class EmbedHost {
422
478
  private queue;
423
479
  private ready;
424
480
  private open;
425
- private expanded;
426
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
427
- private pos;
428
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
429
- private dragBase;
481
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
482
+ private geo;
483
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
484
+ private resizeBase;
485
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
486
+ private dockBase;
487
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
488
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
489
+ * read the box back, which stops being the thing we're moving the moment it docks. */
490
+ private dragFrom;
491
+ private dragAt;
492
+ /** The painted box when the drag began — the edge test measures what you can SEE
493
+ * touching the screen edge, which is not the anchor once the panel is open. */
494
+ private dragBox;
495
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
496
+ private dragGrowth;
497
+ /**
498
+ * Has this drag been clear of the dock band yet?
499
+ *
500
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
501
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
502
+ * up docked it instantly, before it had moved anywhere. Docking now requires
503
+ * ENTERING the band rather than merely starting in it.
504
+ */
505
+ private dragLeftBand;
430
506
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
431
507
  private context;
432
508
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
433
509
  private hostEl;
434
510
  /** Rendering into the host's own element: they own the box, the chrome and visibility. */
435
511
  private readonly inline;
512
+ /** Host-auth storm guard (see provideAuth). Counts getAuthToken calls; past a cap we stop
513
+ * calling the provider, rather than joining the storm. This host instance OUTLIVES iframe
514
+ * reloads, so — unlike anything in the widget — the counter survives the very reload loop
515
+ * that drives the storm. (Kept tiny on purpose: this file has a hard per-page size budget;
516
+ * the widget side carries the windowed/retry-after-settle half.) Terse name = fewer bytes
517
+ * in the stub; it is the host-auth request count. */
518
+ private ac;
436
519
  constructor(config: LoaderConfig);
437
520
  mount(): void;
438
521
  /**
@@ -443,6 +526,13 @@ declare class EmbedHost {
443
526
  * on it has plenty of frames.
444
527
  */
445
528
  private onMessage;
529
+ /**
530
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
531
+ * source-valid MessageEvent — this routes a message through the identical logic
532
+ * without weakening the origin/source checks above, which stay the only real door.
533
+ */
534
+ __testHandle(msg: WidgetToHost): void;
535
+ private handle;
446
536
  /**
447
537
  * Own a drag for its lifetime.
448
538
  *
@@ -455,12 +545,31 @@ declare class EmbedHost {
455
545
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
456
546
  * widget has to own the gesture. All we do is take a delta and place the element.
457
547
  */
458
- private cornerOffset;
459
- private dragTo;
460
- /** Reset to the default corner (a menu action the drag is otherwise sticky). */
461
- private resetPos;
462
- private readPos;
463
- private writePos;
548
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
549
+ private applyBox;
550
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
551
+ /**
552
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
553
+ *
554
+ * Every consumer has to agree on this, which is the bug that made the widget
555
+ * flip-flop under small movements near the viewport centre: the box was frozen but
556
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
557
+ * swapping which corner it drew against (and it animates that), twitching back and
558
+ * forth while the loader held perfectly still.
559
+ */
560
+ private growthNow;
561
+ private place;
562
+ /**
563
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
564
+ * without this it draws its launcher against a stale guess — which is what made the
565
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
566
+ */
567
+ private publishGeometry;
568
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
569
+ private themeMode;
570
+ private onViewportResize;
571
+ private readGeometry;
572
+ private writeGeometry;
464
573
  /**
465
574
  * Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
466
575
  * call it and hand the token straight to the widget via `host.auth` — no popup. The
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@
18
18
  * shapes are right, "deeper integration" is a transport swap (postMessage ->
19
19
  * direct call), not a rewrite. So model the DOMAIN here, not the plumbing.
20
20
  */
21
- declare const PROTOCOL_VERSION = 1;
21
+ declare const PROTOCOL_VERSION = 2;
22
22
  /** Every message is namespaced so we never collide with the host page's own postMessage traffic. */
23
23
  declare const CHANNEL = "mf-embed";
24
24
  /**
@@ -143,6 +143,13 @@ interface HostTool {
143
143
  * requires the user to confirm, every time — see the note on ToolCall.
144
144
  */
145
145
  readOnly?: boolean;
146
+ /**
147
+ * The tiered confirm policy, set by the host (e.g. `window.matterfact.hoist.actions`):
148
+ * `'required'` ⇒ the widget must show a confirm card and get the user's approval
149
+ * before every call; `'auto'` ⇒ it may call without asking. Absent for a tool that
150
+ * predates this policy (treat as `'required'` — the safer default).
151
+ */
152
+ confirm?: 'auto' | 'required';
146
153
  }
147
154
  type HostToWidget = {
148
155
  type: 'host.ready';
@@ -183,6 +190,23 @@ type HostToWidget = {
183
190
  } | {
184
191
  type: 'host.theme';
185
192
  mode: 'light' | 'dark';
193
+ }
194
+ /**
195
+ * Where the widget currently IS. Sent on mount and after anything that moves it
196
+ * (drag, snap, mode change, viewport resize).
197
+ *
198
+ * The widget cannot work this out for itself — it can't see the viewport, and the
199
+ * loader owns the box. Without it the widget renders its launcher against a stale
200
+ * guess: a launcher dragged to the left edge kept drawing its circle bottom-RIGHT
201
+ * inside a left-anchored box, so the circle jumped as soon as the box grew for the
202
+ * hover pill. `growth` is the direction the pill/menu/panel must open (toward the
203
+ * viewport centre); `mode` decides whether the launcher is a circle or an edge tab.
204
+ */
205
+ | {
206
+ type: 'host.geometry';
207
+ mode: 'float' | 'dock';
208
+ dockSide: 'left' | 'right';
209
+ growth: 'tl' | 'tr' | 'bl' | 'br';
186
210
  };
187
211
  /**
188
212
  * The agent asking the host to do something.
@@ -245,14 +269,57 @@ type WidgetToHost = {
245
269
  height: number;
246
270
  }
247
271
  /**
248
- * Maximize / restore. Expanded makes the iframe fill the viewport (with a small
249
- * margin) so the composer's `/` picker, mention previews, and the transcript have
250
- * room the corner popup can't give them. The loader owns the iframe size, so only
251
- * it can do this `widget.resize` (height-only) can't widen the popup.
272
+ * Dock / undock the open panel.
273
+ *
274
+ * Docked is a FULL-HEIGHT SIDE PANEL on the launcher's own side left corners dock
275
+ * to the left edge, right corners to the right, unless `setDockSide` overrides it.
276
+ * This replaces the old `setExpanded`, which was hardcoded to the right edge and so
277
+ * threw the panel across the screen whenever the launcher sat on the left.
278
+ *
279
+ * The loader owns the box, so only it can do this — `widget.resize` (height-only)
280
+ * can neither widen the panel nor move it to an edge.
281
+ */
282
+ | {
283
+ type: 'widget.setMode';
284
+ mode: 'float' | 'dock';
285
+ }
286
+ /** Which edge the tab and its panel live on. */
287
+ | {
288
+ type: 'widget.setDockSide';
289
+ side: 'left' | 'right';
290
+ }
291
+ /** Place the FLOATING launcher at a specific corner (the menu's snap picker). */
292
+ | {
293
+ type: 'widget.snapCorner';
294
+ corner: 'tl' | 'tr' | 'bl' | 'br';
295
+ }
296
+ /**
297
+ * Resize the COLLAPSED launcher's host box so the widget can paint outside the 56px
298
+ * circle — the hover pill (`{w:180,h:56}`) or the context menu (its own box). The
299
+ * iframe clips to the host box, so anything beside the circle is invisible until the
300
+ * host makes room. `{w:56,h:56}` returns to the bare circle. Ignored while open.
301
+ */
302
+ | {
303
+ type: 'widget.setLauncherRegion';
304
+ w: number;
305
+ h: number;
306
+ }
307
+ /**
308
+ * User resize of the floating panel, via the grip on its inner corner.
309
+ *
310
+ * Same contract as the launcher drag below, and for the same reason: the press is
311
+ * captured by the iframe, so the host cannot see the moves. The widget tracks the
312
+ * gesture and sends SCREEN-coordinate deltas; the loader owns the sizing, the
313
+ * per-corner sign of the delta, and the clamp.
252
314
  */
253
315
  | {
254
- type: 'widget.setExpanded';
255
- expanded: boolean;
316
+ type: 'widget.resizeStart';
317
+ } | {
318
+ type: 'widget.resizeMove';
319
+ dx: number;
320
+ dy: number;
321
+ } | {
322
+ type: 'widget.resizeEnd';
256
323
  }
257
324
  /**
258
325
  * Launcher drag. The widget tracks the gesture; the host only moves the element.
@@ -278,18 +345,7 @@ type WidgetToHost = {
278
345
  } | {
279
346
  type: 'widget.dragEnd';
280
347
  }
281
- /**
282
- * Grow/shrink the iframe to fit the launcher's context menu. The collapsed launcher is
283
- * only ~64px tall, so a menu rendered inside it would be clipped by the iframe's own
284
- * bounds — the host has to make room before it can be seen. `widget.resize` can't do
285
- * this: it's height-only AND ignored while closed.
286
- */
287
- | {
288
- type: 'widget.setMenu';
289
- open: boolean;
290
- height: number;
291
- }
292
- /** Put the launcher back in the default corner and forget the dragged position. */
348
+ /** Put the launcher back in the default corner and forget the saved size/dock. */
293
349
  | {
294
350
  type: 'widget.resetPos';
295
351
  }
@@ -386,7 +442,7 @@ interface LoaderConfig {
386
442
  * own side panel, tool drawer or tab and wants the agent to live there.
387
443
  *
388
444
  * This inverts who owns the chrome. In corner mode the widget owns everything: it draws
389
- * its own launcher and it tells us how big to be (setOpen/setExpanded/resize). Inside
445
+ * its own launcher and it tells us how big to be (setOpen/setDocked/resize). Inside
390
446
  * someone else's panel, both are theirs — a widget that resized its host's drawer, or
391
447
  * drew a second floating bubble inside it, is just broken. So here we fill the
392
448
  * container, ignore the size messages, and the widget skips the launcher and renders
@@ -422,17 +478,44 @@ declare class EmbedHost {
422
478
  private queue;
423
479
  private ready;
424
480
  private open;
425
- private expanded;
426
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
427
- private pos;
428
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
429
- private dragBase;
481
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
482
+ private geo;
483
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
484
+ private resizeBase;
485
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
486
+ private dockBase;
487
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
488
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
489
+ * read the box back, which stops being the thing we're moving the moment it docks. */
490
+ private dragFrom;
491
+ private dragAt;
492
+ /** The painted box when the drag began — the edge test measures what you can SEE
493
+ * touching the screen edge, which is not the anchor once the panel is open. */
494
+ private dragBox;
495
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
496
+ private dragGrowth;
497
+ /**
498
+ * Has this drag been clear of the dock band yet?
499
+ *
500
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
501
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
502
+ * up docked it instantly, before it had moved anywhere. Docking now requires
503
+ * ENTERING the band rather than merely starting in it.
504
+ */
505
+ private dragLeftBand;
430
506
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
431
507
  private context;
432
508
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
433
509
  private hostEl;
434
510
  /** Rendering into the host's own element: they own the box, the chrome and visibility. */
435
511
  private readonly inline;
512
+ /** Host-auth storm guard (see provideAuth). Counts getAuthToken calls; past a cap we stop
513
+ * calling the provider, rather than joining the storm. This host instance OUTLIVES iframe
514
+ * reloads, so — unlike anything in the widget — the counter survives the very reload loop
515
+ * that drives the storm. (Kept tiny on purpose: this file has a hard per-page size budget;
516
+ * the widget side carries the windowed/retry-after-settle half.) Terse name = fewer bytes
517
+ * in the stub; it is the host-auth request count. */
518
+ private ac;
436
519
  constructor(config: LoaderConfig);
437
520
  mount(): void;
438
521
  /**
@@ -443,6 +526,13 @@ declare class EmbedHost {
443
526
  * on it has plenty of frames.
444
527
  */
445
528
  private onMessage;
529
+ /**
530
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
531
+ * source-valid MessageEvent — this routes a message through the identical logic
532
+ * without weakening the origin/source checks above, which stay the only real door.
533
+ */
534
+ __testHandle(msg: WidgetToHost): void;
535
+ private handle;
446
536
  /**
447
537
  * Own a drag for its lifetime.
448
538
  *
@@ -455,12 +545,31 @@ declare class EmbedHost {
455
545
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
456
546
  * widget has to own the gesture. All we do is take a delta and place the element.
457
547
  */
458
- private cornerOffset;
459
- private dragTo;
460
- /** Reset to the default corner (a menu action the drag is otherwise sticky). */
461
- private resetPos;
462
- private readPos;
463
- private writePos;
548
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
549
+ private applyBox;
550
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
551
+ /**
552
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
553
+ *
554
+ * Every consumer has to agree on this, which is the bug that made the widget
555
+ * flip-flop under small movements near the viewport centre: the box was frozen but
556
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
557
+ * swapping which corner it drew against (and it animates that), twitching back and
558
+ * forth while the loader held perfectly still.
559
+ */
560
+ private growthNow;
561
+ private place;
562
+ /**
563
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
564
+ * without this it draws its launcher against a stale guess — which is what made the
565
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
566
+ */
567
+ private publishGeometry;
568
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
569
+ private themeMode;
570
+ private onViewportResize;
571
+ private readGeometry;
572
+ private writeGeometry;
464
573
  /**
465
574
  * Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
466
575
  * call it and hand the token straight to the widget via `host.auth` — no popup. The