@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.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/protocol.ts
2
- var PROTOCOL_VERSION = 1;
2
+ var PROTOCOL_VERSION = 2;
3
3
  var CHANNEL = "mf-embed";
4
4
  function envelope(payload, id) {
5
5
  return {
@@ -13,6 +13,213 @@ function isEnvelope(data) {
13
13
  return typeof data === "object" && data !== null && data.channel === CHANNEL;
14
14
  }
15
15
 
16
+ // src/geometry.ts
17
+ var MARGIN = 20;
18
+ var LAUNCHER = 56;
19
+ var SNAP_RADIUS = 96;
20
+ var TAB_W = 44;
21
+ var TAB_H = 132;
22
+ var EDGE_DOCK = 64;
23
+ var DEFAULT_FLOAT_W = 420;
24
+ var DEFAULT_FLOAT_H = 640;
25
+ var MIN_FLOAT_W = 320;
26
+ var MIN_FLOAT_H = 420;
27
+ var DEFAULT_DOCK_W = 600;
28
+ var MIN_DOCK_W = 320;
29
+ var CORNERS = ["tl", "tr", "bl", "br"];
30
+ function defaultGeometry() {
31
+ return {
32
+ // Dock is the default: a side panel reads as part of the host app.
33
+ mode: "dock",
34
+ corner: "br",
35
+ x: 0,
36
+ y: 0,
37
+ floatW: DEFAULT_FLOAT_W,
38
+ floatH: DEFAULT_FLOAT_H,
39
+ dockSide: "right",
40
+ tabY: 0,
41
+ // 0 => centre it (see tabTop)
42
+ dockW: DEFAULT_DOCK_W
43
+ };
44
+ }
45
+ function clamp(n, lo, hi) {
46
+ return Math.min(Math.max(n, lo), hi);
47
+ }
48
+ function cornerOrigin(corner, w, h, vw, vh) {
49
+ return {
50
+ x: corner[1] === "l" ? MARGIN : vw - MARGIN - w,
51
+ y: corner[0] === "t" ? MARGIN : vh - MARGIN - h
52
+ };
53
+ }
54
+ function launcherOrigin(g, vw, vh) {
55
+ if (g.corner) return cornerOrigin(g.corner, LAUNCHER, LAUNCHER, vw, vh);
56
+ return {
57
+ x: clamp(g.x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER)),
58
+ y: clamp(g.y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER))
59
+ };
60
+ }
61
+ function growthCorner(g, vw, vh) {
62
+ if (g.corner) return g.corner;
63
+ const o = launcherOrigin(g, vw, vh);
64
+ return nearestCorner(o.x + LAUNCHER / 2, o.y + LAUNCHER / 2, vw, vh);
65
+ }
66
+ function nearestCorner(cx, cy, vw, vh) {
67
+ return `${cy < vh / 2 ? "t" : "b"}${cx < vw / 2 ? "l" : "r"}`;
68
+ }
69
+ function snapOrFree(x, y, vw, vh) {
70
+ const cx = clamp(x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER));
71
+ const cy = clamp(y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER));
72
+ for (const corner of CORNERS) {
73
+ const o = cornerOrigin(corner, LAUNCHER, LAUNCHER, vw, vh);
74
+ if (Math.hypot(cx - o.x, cy - o.y) <= SNAP_RADIUS) {
75
+ return { corner, x: cx, y: cy };
76
+ }
77
+ }
78
+ return { corner: null, x: cx, y: cy };
79
+ }
80
+ function edgeDock(x, w, vw) {
81
+ if (x <= EDGE_DOCK) return "left";
82
+ if (x + w >= vw - EDGE_DOCK) return "right";
83
+ return null;
84
+ }
85
+ function dragAnchor(g, vw, vh) {
86
+ if (g.mode === "dock") {
87
+ return { x: g.dockSide === "left" ? 0 : vw - TAB_W, y: tabTop(g, vh) };
88
+ }
89
+ return launcherOrigin(g, vw, vh);
90
+ }
91
+ function visibleBox(g, open, vw, vh) {
92
+ if (g.mode === "dock") {
93
+ const w2 = open ? Math.min(Math.max(g.dockW, MIN_DOCK_W), Math.max(0, vw - 2 * MARGIN)) : TAB_W;
94
+ return { x: g.dockSide === "left" ? 0 : vw - w2, w: w2 };
95
+ }
96
+ const o = launcherOrigin(g, vw, vh);
97
+ if (!open) return { x: o.x, w: LAUNCHER };
98
+ const { w } = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
99
+ return {
100
+ x: growthCorner(g, vw, vh)[1] === "r" ? o.x + LAUNCHER - w : o.x,
101
+ w
102
+ };
103
+ }
104
+ function placeDrop(anchorX, anchorY, boxLeft, boxW, vw, vh) {
105
+ const snapped = snapOrFree(anchorX, anchorY, vw, vh);
106
+ if (snapped.corner) return { mode: "float", ...snapped };
107
+ const side = edgeDock(boxLeft, boxW, vw);
108
+ if (side) {
109
+ return {
110
+ mode: "dock",
111
+ dockSide: side,
112
+ tabY: clamp(anchorY, 0, Math.max(0, vh - TAB_H))
113
+ };
114
+ }
115
+ return { mode: "float", ...snapped };
116
+ }
117
+ function clampSize(w, h, vw, vh, minW, minH) {
118
+ return {
119
+ w: Math.min(Math.max(w, minW), Math.max(0, vw - 2 * MARGIN)),
120
+ h: Math.min(Math.max(h, minH), Math.max(0, vh - 2 * MARGIN))
121
+ };
122
+ }
123
+ function applyResizeDelta(corner, startW, startH, dx, dy) {
124
+ const wSign = corner[1] === "r" ? -1 : 1;
125
+ const hSign = corner[0] === "b" ? -1 : 1;
126
+ return { w: startW + wSign * dx, h: startH + hSign * dy };
127
+ }
128
+ function applyDockResizeDelta(side, startW, dx) {
129
+ return side === "right" ? startW - dx : startW + dx;
130
+ }
131
+ function parseGeometry(raw) {
132
+ if (!raw) return null;
133
+ try {
134
+ const p = JSON.parse(raw);
135
+ if (!p || p.mode !== "float" && p.mode !== "dock") return null;
136
+ const d = defaultGeometry();
137
+ const num = (v, dv) => typeof v === "number" ? v : dv;
138
+ return {
139
+ mode: p.mode,
140
+ corner: CORNERS.includes(p.corner) ? p.corner : null,
141
+ x: num(p.x, d.x),
142
+ y: num(p.y, d.y),
143
+ floatW: num(p.floatW, d.floatW),
144
+ floatH: num(p.floatH, d.floatH),
145
+ dockSide: p.dockSide === "left" ? "left" : "right",
146
+ tabY: num(p.tabY, d.tabY),
147
+ dockW: num(p.dockW, d.dockW)
148
+ };
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+ function serializeGeometry(g) {
154
+ return JSON.stringify(g);
155
+ }
156
+ var px = (n) => `${n}px`;
157
+ function boxAt(ox, oy, w, h, corner, vw, vh) {
158
+ const right = corner[1] === "r";
159
+ const bottom = corner[0] === "b";
160
+ const rawX = right ? vw - (ox + LAUNCHER) : ox;
161
+ const rawY = bottom ? vh - (oy + LAUNCHER) : oy;
162
+ const x = clamp(rawX, MARGIN, Math.max(MARGIN, vw - MARGIN - w));
163
+ const y = clamp(rawY, MARGIN, Math.max(MARGIN, vh - MARGIN - h));
164
+ return {
165
+ top: bottom ? "auto" : px(y),
166
+ bottom: bottom ? px(y) : "auto",
167
+ left: right ? "auto" : px(x),
168
+ right: right ? px(x) : "auto",
169
+ width: px(w),
170
+ height: px(h)
171
+ };
172
+ }
173
+ function tabTop(g, vh) {
174
+ const max = Math.max(0, vh - TAB_H);
175
+ return clamp(g.tabY || Math.round((vh - TAB_H) / 2), 0, max);
176
+ }
177
+ function tabBox(g, vw, vh) {
178
+ const left = g.dockSide === "left";
179
+ return {
180
+ top: px(tabTop(g, vh)),
181
+ bottom: "auto",
182
+ left: left ? "0px" : "auto",
183
+ right: left ? "auto" : "0px",
184
+ width: px(TAB_W),
185
+ height: px(TAB_H)
186
+ };
187
+ }
188
+ function floatChrome(g, w, h, vw, vh, growth) {
189
+ const o = launcherOrigin(g, vw, vh);
190
+ return boxAt(o.x, o.y, w, h, growth ?? growthCorner(g, vw, vh), vw, vh);
191
+ }
192
+ function dockChrome(g, w, h, vw, vh) {
193
+ const left = g.dockSide === "left";
194
+ return {
195
+ top: px(tabTop(g, vh)),
196
+ bottom: "auto",
197
+ left: left ? "0px" : "auto",
198
+ right: left ? "auto" : "0px",
199
+ width: px(w),
200
+ height: px(h)
201
+ };
202
+ }
203
+ function floatBox(g, vw, vh, growth) {
204
+ const f = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
205
+ return floatChrome(g, f.w, f.h, vw, vh, growth);
206
+ }
207
+ function dockBox(g, vw, _vh) {
208
+ const w = Math.min(
209
+ Math.max(g.dockW, MIN_DOCK_W),
210
+ Math.max(0, vw - 2 * MARGIN)
211
+ );
212
+ const left = g.dockSide === "left";
213
+ return {
214
+ top: "0px",
215
+ bottom: "0px",
216
+ height: "auto",
217
+ left: left ? "0px" : "auto",
218
+ right: left ? "auto" : "0px",
219
+ width: px(w)
220
+ };
221
+ }
222
+
16
223
  // src/loader.ts
17
224
  var DEFAULT_ORIGIN = "https://app.matterfact.com";
18
225
  function devRequested() {
@@ -46,6 +253,10 @@ function readConfig() {
46
253
  pageContext: pageContextAttr !== "off" && pageContextAttr !== "false" && pageContextAttr !== "0"
47
254
  // No `data-dev` — see the `dev` field's doc comment: the URL trigger is the
48
255
  // point for the script-tag path, so there is deliberately no script-tag knob here.
256
+ // No `data-actions` either: the action policy lives entirely in the lazy chunk,
257
+ // read straight off `window.matterfact.hoist.actions` by hoist-runtime.ts's
258
+ // readActionsConfig() — see LoaderConfig's class doc and MatterfactAgentProps.actions
259
+ // (react.tsx) for the one remaining way to SET that global programmatically.
49
260
  };
50
261
  }
51
262
  var POS_KEY = "mf.embed.pos";
@@ -58,15 +269,42 @@ var EmbedHost = class {
58
269
  this.queue = [];
59
270
  this.ready = false;
60
271
  this.open = false;
61
- this.expanded = false;
62
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
63
- this.pos = null;
64
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
65
- this.dragBase = null;
272
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
273
+ this.geo = defaultGeometry();
274
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
275
+ this.resizeBase = null;
276
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
277
+ this.dockBase = DEFAULT_DOCK_W;
278
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
279
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
280
+ * read the box back, which stops being the thing we're moving the moment it docks. */
281
+ this.dragFrom = null;
282
+ this.dragAt = { x: 0, y: 0 };
283
+ /** The painted box when the drag began — the edge test measures what you can SEE
284
+ * touching the screen edge, which is not the anchor once the panel is open. */
285
+ this.dragBox = { x: 0, w: 0 };
286
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
287
+ this.dragGrowth = null;
288
+ /**
289
+ * Has this drag been clear of the dock band yet?
290
+ *
291
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
292
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
293
+ * up docked it instantly, before it had moved anywhere. Docking now requires
294
+ * ENTERING the band rather than merely starting in it.
295
+ */
296
+ this.dragLeftBand = false;
66
297
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
67
298
  this.context = null;
68
299
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
69
300
  this.hostEl = null;
301
+ /** Host-auth storm guard (see provideAuth). Counts getAuthToken calls; past a cap we stop
302
+ * calling the provider, rather than joining the storm. This host instance OUTLIVES iframe
303
+ * reloads, so — unlike anything in the widget — the counter survives the very reload loop
304
+ * that drives the storm. (Kept tiny on purpose: this file has a hard per-page size budget;
305
+ * the widget side carries the windowed/retry-after-settle half.) Terse name = fewer bytes
306
+ * in the stub; it is the host-auth request count. */
307
+ this.ac = 0;
70
308
  /**
71
309
  * Every message is checked twice, on every single message — not once at setup.
72
310
  *
@@ -78,87 +316,10 @@ var EmbedHost = class {
78
316
  if (event.origin !== this.config.origin) return;
79
317
  if (event.source !== this.iframe?.contentWindow) return;
80
318
  if (!isEnvelope(event.data)) return;
81
- const msg = event.data.payload;
82
- switch (msg.type) {
83
- case "widget.ready":
84
- this.ready = true;
85
- this.send({
86
- type: "host.ready",
87
- protocol: PROTOCOL_VERSION,
88
- origin: location.origin
89
- });
90
- this.flush();
91
- if (this.inline) void this.loadContext();
92
- break;
93
- case "widget.setOpen":
94
- if (this.inline) break;
95
- this.open = msg.open;
96
- this.iframe?.setAttribute("data-open", String(msg.open));
97
- if (!msg.open) {
98
- this.expanded = false;
99
- this.iframe?.setAttribute("data-expanded", "false");
100
- }
101
- if (msg.open) void this.loadContext();
102
- break;
103
- case "widget.setExpanded":
104
- if (this.inline) break;
105
- this.expanded = msg.expanded;
106
- this.iframe?.setAttribute("data-expanded", String(msg.expanded));
107
- if (this.iframe) this.iframe.style.height = "";
108
- break;
109
- case "widget.resize":
110
- if (this.inline) break;
111
- if (this.iframe && this.open && !this.expanded) {
112
- this.iframe.style.height = `${Math.min(msg.height, window.innerHeight - 40)}px`;
113
- }
114
- break;
115
- case "widget.requestContext":
116
- void this.loadContext().then((m) => m.provideContext());
117
- break;
118
- case "widget.requestSnapshot":
119
- void this.loadContext().then((m) => m.sendSnapshot(this.send));
120
- break;
121
- case "widget.readRegion":
122
- void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
123
- break;
124
- case "widget.callTool":
125
- void this.loadContext().then((m) => m.callTool(msg.call, this.send));
126
- break;
127
- // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
128
- // doesn't draw one), and moving or hiding the host's own panel from inside it
129
- // would be us redecorating their app. An older cached loader could still be told
130
- // any of these by a newer widget, so they're guarded rather than assumed absent.
131
- case "widget.dragStart":
132
- if (this.inline) break;
133
- this.dragBase = this.cornerOffset();
134
- break;
135
- case "widget.dragMove":
136
- if (this.inline) break;
137
- this.dragTo(msg.dx, msg.dy);
138
- break;
139
- case "widget.dragEnd":
140
- if (this.inline) break;
141
- this.dragBase = null;
142
- this.writePos(this.pos);
143
- break;
144
- case "widget.setMenu":
145
- if (this.inline) break;
146
- if (this.iframe && !this.open) {
147
- this.iframe.style.height = msg.open ? `${64 + msg.height}px` : "";
148
- }
149
- break;
150
- case "widget.resetPos":
151
- if (this.inline) break;
152
- this.resetPos();
153
- break;
154
- case "widget.hide":
155
- if (this.inline) break;
156
- if (this.hostEl) this.hostEl.style.display = "none";
157
- break;
158
- case "widget.needsAuth":
159
- void this.provideAuth();
160
- break;
161
- }
319
+ this.handle(event.data.payload);
320
+ };
321
+ this.onViewportResize = () => {
322
+ this.place();
162
323
  };
163
324
  this.send = (msg) => {
164
325
  if (!this.ready) {
@@ -184,34 +345,36 @@ var EmbedHost = class {
184
345
  ].join(";");
185
346
  this.config.container.appendChild(host);
186
347
  } else {
187
- this.pos = this.readPos();
348
+ this.geo = this.readGeometry();
188
349
  host.style.cssText = [
189
350
  "all: initial",
190
351
  "position: fixed",
191
- `right: ${this.pos ? this.pos.right : 20}px`,
192
- `bottom: ${this.pos ? this.pos.bottom : 20}px`,
193
352
  // Below the max so a host that genuinely needs to cover us (a modal, a cookie
194
353
  // banner they are legally obliged to show) still can.
195
354
  "z-index: 2147483000",
196
- "contain: layout style"
355
+ "contain: layout style",
356
+ // The host element owns the BOX now (position AND size); the iframe fills it.
357
+ // Transitioning the box is what makes the corner snap and the open/dock read as
358
+ // motion rather than a jump. Suppressed during drag/resize (see those handlers).
359
+ "transition: width .18s ease, height .18s ease, top .18s ease, right .18s ease, bottom .18s ease, left .18s ease"
197
360
  ].join(";");
198
361
  document.body.appendChild(host);
199
362
  }
200
363
  this.shadow = host.attachShadow({ mode: "closed" });
201
364
  const style = document.createElement("style");
202
- style.textContent = ":host{all:initial}iframe{border:0;display:block;color-scheme:light dark;}" + (this.inline ? "iframe{width:100%;height:100%}" : (
203
- // Hairline ring + soft ambient: the widget must read as a crisp card on
204
- // ANY host background, including pure white where a shadow alone bleeds.
205
- 'iframe{border-radius:12px;box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029;width:400px;height:64px;transition:height .18s ease,width .18s ease}iframe[data-open="true"]{width:420px;height:640px}iframe[data-open="true"][data-expanded="true"]{width:min(600px,calc(100vw - 40px));height:calc(100vh - 40px)}'
365
+ style.textContent = ":host{all:initial}iframe{border:0;display:block;width:100%;height:100%;background:transparent}" + (this.inline ? "" : (
366
+ // color-scheme only on the OPEN panel on a collapsed launcher it makes the
367
+ // UA paint an opaque canvas base behind our shapes.
368
+ ':host([data-mode="float"]) iframe,:host([data-mode="dock"]) iframe{color-scheme:light dark}:host([data-mode="float"]) iframe{border-radius:12px;box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029}:host([data-mode="dock"]) iframe{box-shadow:0 0 0 1px #00000014,0 8px 40px #00000029}:host([data-mode="dock"][data-flush="right"]) iframe{border-radius:12px 0 0 12px}:host([data-mode="dock"][data-flush="left"]) iframe{border-radius:0 12px 12px 0}'
206
369
  ));
207
370
  this.shadow.appendChild(style);
208
371
  const iframe = document.createElement("iframe");
209
372
  iframe.title = "matterfact assistant";
210
373
  iframe.setAttribute(
211
374
  "sandbox",
212
- "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
375
+ "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
213
376
  );
214
- iframe.setAttribute("allow", "microphone");
377
+ iframe.setAttribute("allow", "microphone; clipboard-write");
215
378
  iframe.src = `${this.config.origin}/embed/chat?k=${encodeURIComponent(
216
379
  this.config.publishableKey
217
380
  )}&o=${encodeURIComponent(location.origin)}` + (this.config.surface ? `&s=${encodeURIComponent(this.config.surface)}` : "") + // Either trigger works: the URL param (no redeploy needed) OR the config's
@@ -220,6 +383,223 @@ var EmbedHost = class {
220
383
  this.iframe = iframe;
221
384
  this.shadow.appendChild(iframe);
222
385
  window.addEventListener("message", this.onMessage);
386
+ if (!this.inline) {
387
+ window.addEventListener("resize", this.onViewportResize);
388
+ this.place();
389
+ }
390
+ }
391
+ /**
392
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
393
+ * source-valid MessageEvent — this routes a message through the identical logic
394
+ * without weakening the origin/source checks above, which stay the only real door.
395
+ */
396
+ __testHandle(msg) {
397
+ this.handle(msg);
398
+ }
399
+ handle(msg) {
400
+ switch (msg.type) {
401
+ case "widget.ready":
402
+ this.ready = true;
403
+ this.send({
404
+ type: "host.ready",
405
+ protocol: PROTOCOL_VERSION,
406
+ origin: location.origin
407
+ });
408
+ this.send({ type: "host.theme", mode: this.themeMode() });
409
+ this.flush();
410
+ if (this.inline) void this.loadContext();
411
+ break;
412
+ case "widget.setOpen":
413
+ if (this.inline) break;
414
+ this.open = msg.open;
415
+ this.place();
416
+ if (msg.open) void this.loadContext();
417
+ break;
418
+ case "widget.setMode":
419
+ if (this.inline) break;
420
+ this.geo.mode = msg.mode;
421
+ this.writeGeometry(this.geo);
422
+ this.place();
423
+ break;
424
+ case "widget.setDockSide":
425
+ if (this.inline) break;
426
+ this.geo.dockSide = msg.side;
427
+ this.writeGeometry(this.geo);
428
+ this.place();
429
+ break;
430
+ case "widget.snapCorner":
431
+ if (this.inline) break;
432
+ this.geo.corner = msg.corner;
433
+ this.writeGeometry(this.geo);
434
+ this.place();
435
+ break;
436
+ case "widget.setLauncherRegion": {
437
+ if (this.inline || this.open) break;
438
+ const vw = window.innerWidth;
439
+ const vh = window.innerHeight;
440
+ if (msg.w <= LAUNCHER && msg.h <= LAUNCHER) {
441
+ this.place();
442
+ } else {
443
+ const region = this.geo.mode === "dock" ? dockChrome(this.geo, msg.w, msg.h, vw, vh) : floatChrome(this.geo, msg.w, msg.h, vw, vh);
444
+ this.applyBox(region, msg.h > LAUNCHER ? "menu" : "pill");
445
+ }
446
+ break;
447
+ }
448
+ case "widget.resize":
449
+ if (this.inline) break;
450
+ if (this.hostEl && this.open && this.geo.mode === "float") {
451
+ this.hostEl.style.height = `${Math.min(
452
+ msg.height,
453
+ Math.max(0, window.innerHeight - 2 * MARGIN)
454
+ )}px`;
455
+ }
456
+ break;
457
+ case "widget.resizeStart":
458
+ if (this.inline) break;
459
+ this.resizeBase = { w: this.geo.floatW, h: this.geo.floatH };
460
+ this.dockBase = this.geo.dockW;
461
+ if (this.hostEl) this.hostEl.style.transition = "none";
462
+ break;
463
+ case "widget.resizeMove": {
464
+ if (this.inline || !this.resizeBase) break;
465
+ if (this.geo.mode === "dock") {
466
+ const w = applyDockResizeDelta(
467
+ this.geo.dockSide,
468
+ this.dockBase,
469
+ msg.dx
470
+ );
471
+ this.geo.dockW = Math.min(
472
+ Math.max(w, MIN_DOCK_W),
473
+ Math.max(0, window.innerWidth - 2 * MARGIN)
474
+ );
475
+ } else {
476
+ const next = applyResizeDelta(
477
+ growthCorner(this.geo, window.innerWidth, window.innerHeight),
478
+ this.resizeBase.w,
479
+ this.resizeBase.h,
480
+ msg.dx,
481
+ msg.dy
482
+ );
483
+ const fit = clampSize(
484
+ next.w,
485
+ next.h,
486
+ window.innerWidth,
487
+ window.innerHeight,
488
+ MIN_FLOAT_W,
489
+ MIN_FLOAT_H
490
+ );
491
+ this.geo.floatW = fit.w;
492
+ this.geo.floatH = fit.h;
493
+ }
494
+ this.place();
495
+ break;
496
+ }
497
+ case "widget.resizeEnd":
498
+ if (this.inline) break;
499
+ this.resizeBase = null;
500
+ if (this.hostEl) this.hostEl.style.transition = "";
501
+ this.writeGeometry(this.geo);
502
+ break;
503
+ case "widget.requestContext":
504
+ void this.loadContext().then((m) => m.provideContext());
505
+ break;
506
+ case "widget.requestSnapshot":
507
+ void this.loadContext().then((m) => m.sendSnapshot(this.send));
508
+ break;
509
+ case "widget.readRegion":
510
+ void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
511
+ break;
512
+ case "widget.callTool":
513
+ void this.loadContext().then((m) => m.callTool(msg.call, this.send));
514
+ break;
515
+ // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
516
+ // doesn't draw one), and moving or hiding the host's own panel from inside it
517
+ // would be us redecorating their app. An older cached loader could still be told
518
+ // any of these by a newer widget, so they're guarded rather than assumed absent.
519
+ case "widget.dragStart":
520
+ if (this.inline) break;
521
+ this.dragFrom = dragAnchor(
522
+ this.geo,
523
+ window.innerWidth,
524
+ window.innerHeight
525
+ );
526
+ this.dragAt = { ...this.dragFrom };
527
+ this.dragLeftBand = false;
528
+ this.dragGrowth = growthCorner(
529
+ this.geo,
530
+ window.innerWidth,
531
+ window.innerHeight
532
+ );
533
+ this.dragBox = visibleBox(
534
+ this.geo,
535
+ this.open,
536
+ window.innerWidth,
537
+ window.innerHeight
538
+ );
539
+ if (this.hostEl) this.hostEl.style.transition = "none";
540
+ break;
541
+ case "widget.dragMove": {
542
+ if (this.inline || !this.dragFrom) break;
543
+ const vw = window.innerWidth;
544
+ this.dragAt = {
545
+ x: this.dragFrom.x + msg.dx,
546
+ y: this.dragFrom.y + msg.dy
547
+ };
548
+ const side = edgeDock(this.dragBox.x + msg.dx, this.dragBox.w, vw);
549
+ if (!side) this.dragLeftBand = true;
550
+ if (side && (this.dragLeftBand || this.geo.mode === "dock")) {
551
+ this.geo.mode = "dock";
552
+ this.geo.dockSide = side;
553
+ this.geo.tabY = Math.max(0, this.dragAt.y);
554
+ } else {
555
+ this.geo.mode = "float";
556
+ this.geo.corner = null;
557
+ this.geo.x = this.dragAt.x;
558
+ this.geo.y = this.dragAt.y;
559
+ }
560
+ this.place();
561
+ break;
562
+ }
563
+ case "widget.dragEnd": {
564
+ if (this.inline || !this.dragFrom) break;
565
+ const at = placeDrop(
566
+ this.dragAt.x,
567
+ this.dragAt.y,
568
+ this.dragBox.x + (this.dragAt.x - this.dragFrom.x),
569
+ this.dragBox.w,
570
+ window.innerWidth,
571
+ window.innerHeight
572
+ );
573
+ this.geo.mode = at.mode;
574
+ if (at.mode === "dock") {
575
+ this.geo.dockSide = at.dockSide;
576
+ this.geo.tabY = at.tabY;
577
+ } else {
578
+ this.geo.corner = at.corner;
579
+ this.geo.x = at.x;
580
+ this.geo.y = at.y;
581
+ }
582
+ this.dragFrom = null;
583
+ this.dragGrowth = null;
584
+ if (this.hostEl) this.hostEl.style.transition = "";
585
+ this.place();
586
+ this.writeGeometry(this.geo);
587
+ break;
588
+ }
589
+ case "widget.resetPos":
590
+ if (this.inline) break;
591
+ this.geo = defaultGeometry();
592
+ this.writeGeometry(this.geo);
593
+ this.place();
594
+ break;
595
+ case "widget.hide":
596
+ if (this.inline) break;
597
+ if (this.hostEl) this.hostEl.style.display = "none";
598
+ break;
599
+ case "widget.needsAuth":
600
+ void this.provideAuth();
601
+ break;
602
+ }
223
603
  }
224
604
  /**
225
605
  * Own a drag for its lifetime.
@@ -233,47 +613,77 @@ var EmbedHost = class {
233
613
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
234
614
  * widget has to own the gesture. All we do is take a delta and place the element.
235
615
  */
236
- cornerOffset() {
237
- const rect = this.hostEl.getBoundingClientRect();
238
- return {
239
- right: window.innerWidth - rect.right,
240
- bottom: window.innerHeight - rect.bottom
241
- };
616
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
617
+ applyBox(box, mode) {
618
+ if (!this.hostEl || this.inline) return;
619
+ const s = this.hostEl.style;
620
+ s.top = box.top;
621
+ s.right = box.right;
622
+ s.bottom = box.bottom;
623
+ s.left = box.left;
624
+ s.width = box.width;
625
+ s.height = box.height;
626
+ this.hostEl.dataset.mode = mode;
627
+ if (mode === "tab" || mode === "dock")
628
+ this.hostEl.dataset.flush = this.geo.dockSide;
629
+ else delete this.hostEl.dataset.flush;
242
630
  }
243
- dragTo(dx, dy) {
244
- if (!this.dragBase || !this.hostEl) return;
245
- const rect = this.hostEl.getBoundingClientRect();
246
- const maxRight = Math.max(0, window.innerWidth - rect.width);
247
- const maxBottom = Math.max(0, window.innerHeight - rect.height);
248
- const right = Math.min(Math.max(0, this.dragBase.right - dx), maxRight);
249
- const bottom = Math.min(Math.max(0, this.dragBase.bottom - dy), maxBottom);
250
- this.pos = { right, bottom };
251
- this.hostEl.style.right = `${right}px`;
252
- this.hostEl.style.bottom = `${bottom}px`;
631
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
632
+ /**
633
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
634
+ *
635
+ * Every consumer has to agree on this, which is the bug that made the widget
636
+ * flip-flop under small movements near the viewport centre: the box was frozen but
637
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
638
+ * swapping which corner it drew against (and it animates that), twitching back and
639
+ * forth while the loader held perfectly still.
640
+ */
641
+ growthNow() {
642
+ return this.dragGrowth ?? growthCorner(this.geo, window.innerWidth, window.innerHeight);
253
643
  }
254
- /** Reset to the default corner (a menu action — the drag is otherwise sticky). */
255
- resetPos() {
256
- this.pos = null;
257
- this.writePos(null);
258
- if (this.hostEl) {
259
- this.hostEl.style.right = "20px";
260
- this.hostEl.style.bottom = "20px";
644
+ place() {
645
+ const vw = window.innerWidth;
646
+ const vh = window.innerHeight;
647
+ if (!this.open) {
648
+ const dock = this.geo.mode === "dock";
649
+ this.applyBox(
650
+ dock ? tabBox(this.geo, vw, vh) : floatChrome(this.geo, LAUNCHER, LAUNCHER, vw, vh, this.growthNow()),
651
+ dock ? "tab" : "launcher"
652
+ );
653
+ } else if (this.geo.mode === "dock") {
654
+ this.applyBox(dockBox(this.geo, vw), "dock");
655
+ } else {
656
+ this.applyBox(floatBox(this.geo, vw, vh, this.growthNow()), "float");
261
657
  }
658
+ this.publishGeometry();
659
+ }
660
+ /**
661
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
662
+ * without this it draws its launcher against a stale guess — which is what made the
663
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
664
+ */
665
+ publishGeometry() {
666
+ this.send({
667
+ type: "host.geometry",
668
+ mode: this.geo.mode,
669
+ dockSide: this.geo.dockSide,
670
+ growth: this.growthNow()
671
+ });
672
+ }
673
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
674
+ themeMode() {
675
+ return this.config.theme === "auto" ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : this.config.theme;
262
676
  }
263
- readPos() {
677
+ readGeometry() {
264
678
  try {
265
- const raw = localStorage.getItem(POS_KEY);
266
- if (!raw) return null;
267
- const p = JSON.parse(raw);
268
- return typeof p?.right === "number" && typeof p?.bottom === "number" ? { right: p.right, bottom: p.bottom } : null;
679
+ return parseGeometry(localStorage.getItem(POS_KEY)) ?? defaultGeometry();
269
680
  } catch {
270
- return null;
681
+ return defaultGeometry();
271
682
  }
272
683
  }
273
- writePos(pos) {
684
+ writeGeometry(g) {
274
685
  try {
275
- if (pos) localStorage.setItem(POS_KEY, JSON.stringify(pos));
276
- else localStorage.removeItem(POS_KEY);
686
+ localStorage.setItem(POS_KEY, serializeGeometry(g));
277
687
  } catch {
278
688
  }
279
689
  }
@@ -297,6 +707,7 @@ var EmbedHost = class {
297
707
  async provideAuth() {
298
708
  const provider = this.config.authTokenProvider ?? globalThis.matterfact?.getEmbedAuthToken;
299
709
  if (!provider) return;
710
+ if (++this.ac > 5) return;
300
711
  try {
301
712
  const token = await provider();
302
713
  if (token) this.send({ type: "host.auth", token, expiresAt: 0 });
@@ -304,7 +715,7 @@ var EmbedHost = class {
304
715
  }
305
716
  }
306
717
  loadContext() {
307
- this.context ?? (this.context = import('./context-YX2KXFLI.js').then((m) => {
718
+ this.context ?? (this.context = import('./context-ARBB2XD6.js').then((m) => {
308
719
  m.start(
309
720
  this.send,
310
721
  this.config.origin,
@@ -328,6 +739,7 @@ var EmbedHost = class {
328
739
  * calls this. */
329
740
  destroy() {
330
741
  window.removeEventListener("message", this.onMessage);
742
+ window.removeEventListener("resize", this.onViewportResize);
331
743
  this.hostEl?.remove();
332
744
  this.hostEl = null;
333
745
  this.iframe = null;