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