@matterfact/embed 0.7.0 → 0.9.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 +171 -1
  2. package/dist/chunk-6EM7T2JV.js +816 -0
  3. package/dist/chunk-6EM7T2JV.js.map +1 -0
  4. package/dist/{chunk-4Q2ROXLR.js → chunk-BKKXHSYU.js} +2 -2
  5. package/dist/chunk-FEXG4LQJ.js +3 -0
  6. package/dist/chunk-FEXG4LQJ.js.map +7 -0
  7. package/dist/chunk-NWNMS34P.js +2 -0
  8. package/dist/chunk-URGQBG4I.js +800 -0
  9. package/dist/chunk-URGQBG4I.js.map +1 -0
  10. package/dist/context-ACFBWIFH.js +3 -0
  11. package/dist/{context-YX2KXFLI.js.map → context-ACFBWIFH.js.map} +1 -1
  12. package/dist/{context-WU2F5CBC.js → context-U2HJJN2S.js} +4 -2
  13. package/dist/embed.js +1 -1
  14. package/dist/embed.js.map +4 -4
  15. package/dist/index.cjs +1006 -159
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +192 -32
  18. package/dist/index.d.ts +192 -32
  19. package/dist/index.js +537 -130
  20. package/dist/index.js.map +1 -1
  21. package/dist/react.cjs +1042 -165
  22. package/dist/react.cjs.map +1 -1
  23. package/dist/react.d.cts +81 -1
  24. package/dist/react.d.ts +81 -1
  25. package/dist/react.js +566 -132
  26. package/dist/react.js.map +1 -1
  27. package/dist/{snapshot-JOGZWESK.js → snapshot-4GXT6PKZ.js} +2 -2
  28. package/dist/{snapshot-MUXE7KXX.js → snapshot-Y75SCGCM.js} +3 -3
  29. package/dist/{snapshot-MUXE7KXX.js.map → snapshot-Y75SCGCM.js.map} +1 -1
  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-7I37ZFAJ.js +0 -2
  40. package/dist/chunk-AXKXNYRT.js +0 -381
  41. package/dist/chunk-AXKXNYRT.js.map +0 -1
  42. package/dist/chunk-D7R6WVHG.js +0 -2
  43. package/dist/chunk-D7R6WVHG.js.map +0 -7
  44. package/dist/chunk-RS6RMZ77.js +0 -396
  45. package/dist/chunk-RS6RMZ77.js.map +0 -1
  46. package/dist/context-YX2KXFLI.js +0 -3
  47. /package/dist/{chunk-4Q2ROXLR.js.map → chunk-BKKXHSYU.js.map} +0 -0
  48. /package/dist/{chunk-7I37ZFAJ.js.map → chunk-NWNMS34P.js.map} +0 -0
  49. /package/dist/{context-WU2F5CBC.js.map → context-U2HJJN2S.js.map} +0 -0
  50. /package/dist/{snapshot-JOGZWESK.js.map → snapshot-4GXT6PKZ.js.map} +0 -0
package/dist/react.cjs CHANGED
@@ -22,6 +22,401 @@ var __copyProps = (to, from, except, desc) => {
22
22
  };
23
23
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
24
 
25
+ // src/redact.ts
26
+ function redact(text) {
27
+ let out = text;
28
+ for (const re of PII) out = out.replace(re, "[redacted]");
29
+ return out;
30
+ }
31
+ var PII;
32
+ var init_redact = __esm({
33
+ "src/redact.ts"() {
34
+ "use strict";
35
+ PII = [
36
+ /\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
37
+ // email
38
+ /\b(?:\d[ -]?){13,19}\b/g,
39
+ // card-ish
40
+ /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
41
+ // ssn — dashed, spaced, OR bare 9 digits
42
+ /\b\d{9,}\b/g,
43
+ // long bare digit runs: account / MRN / routing numbers
44
+ /(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
45
+ // us phone
46
+ /\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
47
+ // stripe-style api keys
48
+ /\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
49
+ // jwt
50
+ ];
51
+ }
52
+ });
53
+
54
+ // src/adapters/hoist.ts
55
+ function detectHoist(read) {
56
+ let rt;
57
+ try {
58
+ rt = read();
59
+ } catch {
60
+ return null;
61
+ }
62
+ if (!rt) return null;
63
+ const worthwhile = !!rt.route.name || rt.grids.length > 0 || rt.charts.length > 0 || rt.nav.length > 0;
64
+ return worthwhile ? rt : null;
65
+ }
66
+ function quote(s, max = 120) {
67
+ const clean = redact(s.replace(/\s+/g, " ").trim());
68
+ const capped = clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
69
+ return `"${capped.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
70
+ }
71
+ function cell(value) {
72
+ const s = value == null ? "" : String(value);
73
+ return `- cell ${quote(s)}`;
74
+ }
75
+ function pushRow(row, cols, out, pad) {
76
+ out.push(`${pad}- row:`);
77
+ for (const c of cols) out.push(`${pad} ${cell(row[c.field])}`);
78
+ }
79
+ function renderGrid(grid, rowCap, out, depth) {
80
+ const pad = " ".repeat(depth);
81
+ out.push(`${pad}- table ${quote(grid.title || "grid")}:`);
82
+ out.push(`${pad} - rowgroup "columns":`);
83
+ for (const c of grid.columns)
84
+ out.push(`${pad} - columnheader ${quote(c.header || c.field)}`);
85
+ if (grid.selected.length) {
86
+ out.push(`${pad} - rowgroup "selected":`);
87
+ for (const r of grid.selected) pushRow(r, grid.columns, out, `${pad} `);
88
+ }
89
+ const shown = grid.rows.slice(0, Math.max(0, rowCap));
90
+ out.push(`${pad} - rowgroup "rows":`);
91
+ for (const r of shown) pushRow(r, grid.columns, out, `${pad} `);
92
+ out.push(
93
+ `${pad} - text: ${quote(`showing ${shown.length} of ${grid.totalCount} rows`)}`
94
+ );
95
+ }
96
+ function seriesSummary(name, data) {
97
+ if (!data.length) return `series ${name}: 0 points`;
98
+ let min = data[0];
99
+ let max = data[0];
100
+ for (const n of data) {
101
+ if (n < min) min = n;
102
+ if (n > max) max = n;
103
+ }
104
+ const last = data[data.length - 1];
105
+ return `series ${name}: ${data.length} points, min ${min}, max ${max}, last ${last}`;
106
+ }
107
+ function renderChart(chart, out, depth) {
108
+ const pad = " ".repeat(depth);
109
+ out.push(`${pad}- figure ${quote(chart.title || "chart")}:`);
110
+ for (const s of chart.series) {
111
+ out.push(`${pad} - text: ${quote(seriesSummary(s.name, s.data))}`);
112
+ }
113
+ }
114
+ function renderNav(groups, out, depth) {
115
+ const pad = " ".repeat(depth);
116
+ for (const g of groups) {
117
+ if (!g.items.length) continue;
118
+ out.push(`${pad}- navigation ${quote(g.label)}:`);
119
+ for (const d of g.items) {
120
+ const mark = d.current ? " [current]" : "";
121
+ out.push(`${pad} - link ${quote(d.label)}${mark} \u2192 ${redact(d.path)}`);
122
+ }
123
+ }
124
+ }
125
+ function flattenDestinations(rt) {
126
+ return rt.nav.flatMap((g) => g.items);
127
+ }
128
+ function buildNavigateTool(rt, policy) {
129
+ const destinations = flattenDestinations(rt);
130
+ const bySection = rt.nav.filter((g) => g.items.length).map((g) => `${g.label} (${g.items.map((d) => d.label).join(", ")})`).join("; ");
131
+ return {
132
+ name: "hoist.navigate",
133
+ description: redact(
134
+ `Navigate the user to a destination in the app. "destination" must be one of the destinations currently available, grouped by section: ${bySection || "(none available)"}.`
135
+ ),
136
+ inputSchema: {
137
+ type: "object",
138
+ properties: {
139
+ destination: {
140
+ type: "string",
141
+ enum: destinations.map((d) => d.label)
142
+ },
143
+ params: { type: "object" }
144
+ },
145
+ required: ["destination"]
146
+ },
147
+ readOnly: false,
148
+ // 'confirm' policy ⇒ the widget must show a confirm card every call; 'auto' ⇒ it
149
+ // may call without asking. See the confirm field's doc comment on HostTool.
150
+ confirm: policy === "auto" ? "auto" : "required"
151
+ };
152
+ }
153
+ function buildHoistContext(rt) {
154
+ const selected = rt.grids.reduce((n, g) => n + g.selected.length, 0);
155
+ const view = rt.route.name || rt.route.path || "this view";
156
+ const bits = [`Viewing ${view}`];
157
+ if (selected) bits.push(`${selected} selected`);
158
+ if (rt.grids.length) bits.push(`${rt.grids.length} grid(s)`);
159
+ if (rt.charts.length) bits.push(`${rt.charts.length} chart(s)`);
160
+ return {
161
+ route: rt.route.name ? redact(rt.route.name) : void 0,
162
+ description: redact(`${bits.join(", ")}.`)
163
+ };
164
+ }
165
+ function renderHoistSnapshot(rt, rowCap) {
166
+ const out = [];
167
+ renderNav(rt.nav, out, 0);
168
+ let truncated = false;
169
+ for (const g of rt.grids) {
170
+ if (g.rows.length > rowCap) truncated = true;
171
+ renderGrid(g, rowCap, out, 0);
172
+ }
173
+ for (const c of rt.charts) renderChart(c, out, 0);
174
+ return { yaml: out.join("\n"), truncated };
175
+ }
176
+ var init_hoist = __esm({
177
+ "src/adapters/hoist.ts"() {
178
+ "use strict";
179
+ init_redact();
180
+ }
181
+ });
182
+
183
+ // src/adapters/hoist-runtime.ts
184
+ function readHoistConfig(win) {
185
+ const cfg = win?.matterfact?.hoist;
186
+ const ex = cfg?.excludeModels;
187
+ return {
188
+ rows: typeof cfg?.rows === "number" && cfg.rows >= 0 ? cfg.rows : 50,
189
+ excludeModels: Array.isArray(ex) ? ex : []
190
+ };
191
+ }
192
+ function getModelsByClass(XH, name) {
193
+ const models = typeof XH?.getModels === "function" ? XH.getModels(name) : [];
194
+ return Array.isArray(models) ? models : [];
195
+ }
196
+ function className(m) {
197
+ return m?.constructor?.name ?? "";
198
+ }
199
+ function modelId(m) {
200
+ return String(m?.xhId ?? m?.id ?? m?.modelId ?? "");
201
+ }
202
+ function headerOf(c) {
203
+ return String(c?.headerName ?? c?.displayName ?? c?.field ?? c?.colId ?? "");
204
+ }
205
+ function collectHeaders(cols, prefix, out) {
206
+ for (const c of Array.isArray(cols) ? cols : []) {
207
+ const children = c?.children;
208
+ if (Array.isArray(children) && children.length) {
209
+ collectHeaders(children, [...prefix, headerOf(c)], out);
210
+ } else {
211
+ const id = String(c?.colId ?? c?.field ?? "");
212
+ if (id) out.set(id, [...prefix, headerOf(c)].filter(Boolean).join(" "));
213
+ }
214
+ }
215
+ }
216
+ function readColumns(g) {
217
+ const leaves = typeof g?.getVisibleLeafColumns === "function" ? g.getVisibleLeafColumns() : Array.isArray(g?.columns) ? g.columns : [];
218
+ const headers = /* @__PURE__ */ new Map();
219
+ collectHeaders(Array.isArray(g?.columns) ? g.columns : [], [], headers);
220
+ return (Array.isArray(leaves) ? leaves : []).map((c) => {
221
+ const field = String(c?.colId ?? c?.field ?? "");
222
+ return { field, header: headers.get(field) || headerOf(c) };
223
+ }).filter((c) => c.field);
224
+ }
225
+ function readRow(rec, cols) {
226
+ const row = {};
227
+ for (const c of cols)
228
+ row[c.field] = typeof rec?.get === "function" ? rec.get(c.field) : rec?.data?.[c.field];
229
+ return row;
230
+ }
231
+ function readGrid(g) {
232
+ const columns = readColumns(g);
233
+ const records = Array.isArray(g?.store?.records) ? g.store.records : [];
234
+ return {
235
+ id: modelId(g),
236
+ title: String(g?.title ?? ""),
237
+ columns,
238
+ selected: (Array.isArray(g?.selectedRecords) ? g.selectedRecords : []).map(
239
+ (r) => readRow(r, columns)
240
+ ),
241
+ rows: records.map((r) => readRow(r, columns)),
242
+ totalCount: typeof g?.store?.allCount === "number" ? g.store.allCount : records.length
243
+ };
244
+ }
245
+ function hasContent(g) {
246
+ return g.totalCount > 0 || g.selected.length > 0;
247
+ }
248
+ function readChart(c) {
249
+ const series = c?.series ?? c?.highchartsConfig?.series ?? [];
250
+ return {
251
+ id: modelId(c),
252
+ title: String(c?.title ?? c?.highchartsConfig?.title?.text ?? ""),
253
+ series: (Array.isArray(series) ? series : []).map((s) => ({
254
+ name: String(s?.name ?? ""),
255
+ data: (Array.isArray(s?.data) ? s.data : []).map((d) => typeof d === "number" ? d : Number(d?.y ?? d?.[1])).filter((n) => Number.isFinite(n))
256
+ }))
257
+ };
258
+ }
259
+ function humanize(seg) {
260
+ return seg.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/\b\w/g, (c) => c.toUpperCase()).trim();
261
+ }
262
+ function childrenOf(node) {
263
+ const k = node?.children;
264
+ return k ? Array.isArray(k) ? k : Array.from(k) : [];
265
+ }
266
+ function routeIndex(XH, params) {
267
+ const router = XH?.router;
268
+ const root = router?.rootNode ?? router?.config?.rootNode;
269
+ const out = /* @__PURE__ */ new Map();
270
+ if (!router || typeof router.buildPath !== "function" || !root) return out;
271
+ const walk2 = (node, prefix) => {
272
+ for (const c of childrenOf(node)) {
273
+ const seg = String(c?.name ?? "");
274
+ if (!seg) continue;
275
+ const name = prefix ? `${prefix}.${seg}` : seg;
276
+ let path = "";
277
+ try {
278
+ path = router.buildPath(name, params);
279
+ } catch {
280
+ path = "";
281
+ }
282
+ if (path && !out.has(seg)) out.set(seg, { path, name });
283
+ walk2(c, name);
284
+ }
285
+ };
286
+ walk2(root, "");
287
+ return out;
288
+ }
289
+ function readSitemap(XH, excluded, origin) {
290
+ const params = XH?.routerState?.params ?? {};
291
+ const currentName = String(XH?.routerState?.name ?? "");
292
+ const routes = routeIndex(XH, params);
293
+ if (!routes.size) return [];
294
+ const groups = [];
295
+ for (const tc of getModelsByClass(XH, "TabContainerModel")) {
296
+ if (excluded(tc)) continue;
297
+ const tabs = Array.isArray(tc?.tabs) ? tc.tabs : [];
298
+ const activeTabId = String(tc?.activeTabId ?? "");
299
+ const items = [];
300
+ let companyScoped = false;
301
+ for (const t of tabs) {
302
+ const id = String(t?.id ?? "");
303
+ const r = routes.get(id);
304
+ if (!id || !r) continue;
305
+ if (r.name.includes(".company.ticker.")) companyScoped = true;
306
+ items.push({
307
+ // Redact HERE, at the source — not in a downstream renderer. This is the
308
+ // one place a Hoist tab TITLE (host-page text, so potentially PII-shaped)
309
+ // becomes a `HoistDestination.label`, and that label is what EVERY
310
+ // consumer uses verbatim: renderNav's YAML, buildNavigateTool's
311
+ // description AND its inputSchema.enum, and executeHostTool's
312
+ // `.find(d => d.label === destination)` lookup. Redacting once here keeps
313
+ // all of them consistent by construction — an enum built from an
314
+ // ALREADY-redacted label can never leak PII a sibling renderer scrubbed
315
+ // (see Important #1), and a destination arg matching the redacted string
316
+ // (the only thing the agent ever saw) still resolves.
317
+ label: redact(String(t?.title ?? "") || humanize(id)),
318
+ // ABSOLUTE, against the HOST page's origin — not the router's bare path.
319
+ // This path's only job is to be a link the agent can hand the user, and
320
+ // the agent's answer renders inside our widget iframe, which is on OUR
321
+ // origin: a bare "/research" there resolves to app.matterfact.com/research
322
+ // (a dead link to the wrong app), which is exactly what shipped before.
323
+ // The adapter runs in the host page, so its own origin is the right one.
324
+ path: origin && r.path.startsWith("/") ? origin + r.path : r.path,
325
+ // The fully-qualified route name — what hoist.navigate hands XH.navigate.
326
+ // Always present: `r` only exists here because routes.get(id) resolved.
327
+ name: r.name,
328
+ current: id === activeTabId || r.name === currentName
329
+ });
330
+ }
331
+ if (!items.length) continue;
332
+ const ticker = String(params.ticker ?? "");
333
+ const label = companyScoped ? ticker ? `Company \xB7 ${ticker}` : "Company" : "Site";
334
+ groups.push({ label, items });
335
+ }
336
+ return groups;
337
+ }
338
+ function readHoistRuntime(win = typeof window !== "undefined" ? window : void 0) {
339
+ const XH = win?.XH;
340
+ if (!XH) return null;
341
+ const { excludeModels } = readHoistConfig(win);
342
+ const excluded = (m) => excludeModels.includes(className(m)) || excludeModels.includes(modelId(m));
343
+ const rs = XH.routerState ?? {};
344
+ return {
345
+ route: {
346
+ name: String(rs.name ?? ""),
347
+ path: String(rs.path ?? ""),
348
+ params: rs.params ?? {}
349
+ },
350
+ grids: getModelsByClass(XH, "GridModel").filter((m) => !excluded(m)).map(readGrid).filter(hasContent),
351
+ charts: getModelsByClass(XH, "ChartModel").filter((m) => !excluded(m)).map(readChart).filter((c) => c.series.length > 0),
352
+ nav: readSitemap(
353
+ XH,
354
+ excluded,
355
+ String(
356
+ win?.location?.origin ?? ""
357
+ )
358
+ )
359
+ };
360
+ }
361
+ function readActionsConfig(win) {
362
+ const navigate = win?.matterfact?.hoist?.actions?.navigate;
363
+ return {
364
+ navigate: navigate === "confirm" || navigate === "auto" ? navigate : "off"
365
+ };
366
+ }
367
+ function advertiseTools(win = typeof window !== "undefined" ? window : void 0) {
368
+ try {
369
+ const XH = win?.XH;
370
+ if (!XH) return [];
371
+ const { navigate } = readActionsConfig(win);
372
+ if (navigate === "off") return [];
373
+ const rt = readHoistRuntime(win);
374
+ if (!rt) return [];
375
+ return [buildNavigateTool(rt, navigate)];
376
+ } catch {
377
+ return [];
378
+ }
379
+ }
380
+ function executeHostTool(call, win = typeof window !== "undefined" ? window : void 0) {
381
+ try {
382
+ if (call.name !== "hoist.navigate") {
383
+ return { ok: false, error: "unknown tool" };
384
+ }
385
+ if (readActionsConfig(win).navigate === "off") {
386
+ return { ok: false, error: "navigation is disabled" };
387
+ }
388
+ const destination = String(call.args?.destination ?? "");
389
+ const rt = readHoistRuntime(win);
390
+ const item = rt ? flattenDestinations(rt).find((d) => d.label === destination) : void 0;
391
+ if (!item) return { ok: false, error: "unknown destination" };
392
+ const XH = win?.XH;
393
+ const params = {
394
+ ...XH?.routerState?.params ?? {},
395
+ ...call.args?.params ?? {}
396
+ };
397
+ if (typeof XH?.navigate === "function") {
398
+ XH.navigate(item.name, params);
399
+ } else if (typeof XH?.router?.navigate === "function") {
400
+ XH.router.navigate(item.name, params);
401
+ } else {
402
+ return { ok: false, error: "navigation unavailable" };
403
+ }
404
+ return { ok: true, result: redact(`navigated to ${destination}`) };
405
+ } catch (e) {
406
+ return {
407
+ ok: false,
408
+ error: redact(e instanceof Error ? e.message : "navigate failed")
409
+ };
410
+ }
411
+ }
412
+ var init_hoist_runtime = __esm({
413
+ "src/adapters/hoist-runtime.ts"() {
414
+ "use strict";
415
+ init_hoist();
416
+ init_redact();
417
+ }
418
+ });
419
+
25
420
  // src/dom/a11y.ts
26
421
  function createStyleCache() {
27
422
  const cache = /* @__PURE__ */ new WeakMap();
@@ -275,7 +670,7 @@ function isHidden(el, style, layout) {
275
670
  if (el.hasAttribute("hidden") || el.hasAttribute("inert")) return true;
276
671
  return !isVisible(el, style, layout);
277
672
  }
278
- function childrenOf(el) {
673
+ function childrenOf2(el) {
279
674
  if (el.shadowRoot) return el.shadowRoot.childNodes;
280
675
  if (el.tagName === "SLOT") {
281
676
  const assigned = el.assignedNodes({ flatten: true });
@@ -284,7 +679,7 @@ function childrenOf(el) {
284
679
  return el.childNodes;
285
680
  }
286
681
  function visitChildren(el, parent, style, w) {
287
- const children = childrenOf(el);
682
+ const children = childrenOf2(el);
288
683
  for (let i = 0; i < children.length; i++) {
289
684
  const child = children[i];
290
685
  if (child) visitNode(child, parent, style, w);
@@ -405,17 +800,17 @@ function sanitize(text, max) {
405
800
  const clean = redact(collapsed);
406
801
  return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
407
802
  }
408
- function quote(text) {
803
+ function quote2(text) {
409
804
  return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
410
805
  }
411
806
  function render(children, depth, out) {
412
807
  const pad = " ".repeat(depth);
413
808
  for (const child of children) {
414
809
  if (typeof child === "string") {
415
- out.push(`${pad}- text: ${quote(sanitize(child, MAX_TEXT))}`);
810
+ out.push(`${pad}- text: ${quote2(sanitize(child, MAX_TEXT))}`);
416
811
  continue;
417
812
  }
418
- const name = child.name ? ` ${quote(sanitize(child.name, MAX_NAME))}` : "";
813
+ const name = child.name ? ` ${quote2(sanitize(child.name, MAX_NAME))}` : "";
419
814
  const head = `${pad}- ${child.role}${name}${child.props}`;
420
815
  if (child.children.length) {
421
816
  out.push(`${head}:`);
@@ -503,6 +898,7 @@ __export(context_exports, {
503
898
  callTool: () => callTool,
504
899
  grantsFromIframeSrcs: () => grantsFromIframeSrcs,
505
900
  isPrivate: () => isPrivate,
901
+ navigateHost: () => navigateHost,
506
902
  provideContext: () => provideContext,
507
903
  readPageContext: () => readPageContext,
508
904
  redact: () => redact,
@@ -511,11 +907,6 @@ __export(context_exports, {
511
907
  start: () => start,
512
908
  stop: () => stop
513
909
  });
514
- function redact(text) {
515
- let out = text;
516
- for (const re of PII) out = out.replace(re, "[redacted]");
517
- return out;
518
- }
519
910
  function isPrivate(el) {
520
911
  if (el.closest("[data-mf-private]")) return true;
521
912
  const tag = el.tagName;
@@ -597,6 +988,8 @@ async function resolveDeclaredContext() {
597
988
  const c = await mf.getPageContext();
598
989
  if (c) return c;
599
990
  }
991
+ const hoist = detectHoist(() => readHoistRuntime());
992
+ if (hoist) return buildHoistContext(hoist);
600
993
  return mf?.context ?? {};
601
994
  }
602
995
  async function publishContext() {
@@ -606,6 +999,7 @@ async function publishContext() {
606
999
  }
607
1000
  function provideContext() {
608
1001
  void publishContext();
1002
+ publishSitemap();
609
1003
  }
610
1004
  function grantsFromIframeSrcs(srcs, origin) {
611
1005
  const out = [];
@@ -637,6 +1031,27 @@ function readArtifactGrants() {
637
1031
  function publishArtifactGrants() {
638
1032
  send?.({ type: "host.artifactGrants", grants: readArtifactGrants() });
639
1033
  }
1034
+ function readSitemap2() {
1035
+ const s = window.matterfact?.sitemap;
1036
+ return Array.isArray(s) ? s : [];
1037
+ }
1038
+ function publishSitemap() {
1039
+ if (!pageContextOn) return;
1040
+ const sitemap = readSitemap2();
1041
+ if (sitemap.length) send?.({ type: "host.sitemap", sitemap });
1042
+ }
1043
+ function navigateHost(href) {
1044
+ if (typeof href !== "string" || !href) return;
1045
+ let target;
1046
+ try {
1047
+ target = new URL(href, location.href);
1048
+ } catch {
1049
+ return;
1050
+ }
1051
+ if (target.protocol !== "http:" && target.protocol !== "https:") return;
1052
+ if (target.origin !== location.origin) return;
1053
+ location.assign(target.href);
1054
+ }
640
1055
  function pushActivity(e) {
641
1056
  activity.push({ ...e, seq: ++activitySeq, ts: Date.now() });
642
1057
  if (activity.length > MAX_ACTIVITY) activity.shift();
@@ -699,6 +1114,11 @@ function watchNavigation() {
699
1114
  pushActivity({ type: "nav", summary: `navigated to ${url}` });
700
1115
  void publishContext();
701
1116
  publishArtifactGrants();
1117
+ publishSitemap();
1118
+ send?.({
1119
+ type: "host.tools",
1120
+ tools: advertiseTools(typeof window !== "undefined" ? window : void 0)
1121
+ });
702
1122
  };
703
1123
  navFire = fire;
704
1124
  for (const name of ["pushState", "replaceState"]) {
@@ -755,6 +1175,20 @@ function scheduleFocus() {
755
1175
  }
756
1176
  async function sendSnapshot(emit) {
757
1177
  if (!pageContextOn) return;
1178
+ const hoist = detectHoist(() => readHoistRuntime());
1179
+ if (hoist) {
1180
+ const { rows } = readHoistConfig(
1181
+ typeof window !== "undefined" ? window : void 0
1182
+ );
1183
+ const { yaml: yaml2, truncated: truncated2 } = renderHoistSnapshot(hoist, rows);
1184
+ emit({
1185
+ type: "host.snapshot",
1186
+ snapshot: { yaml: redact(yaml2), seq: ++snapshotSeq, truncated: truncated2 }
1187
+ });
1188
+ lastFocus = { ...lastFocus, visibleRefs: [] };
1189
+ emit({ type: "host.focus", focus: lastFocus });
1190
+ return;
1191
+ }
758
1192
  const { snapshot: snapshot2 } = await loadSnapshotModule();
759
1193
  const { yaml, truncated, visibleRefs } = snapshot2();
760
1194
  emit({
@@ -780,11 +1214,25 @@ async function sendRegion(ref, emit) {
780
1214
  emit({ type: "host.region", ref, yaml: redact(yaml) });
781
1215
  }
782
1216
  async function callTool(call, emit) {
1217
+ if (!pageContextOn) {
1218
+ emit({
1219
+ type: "host.toolResult",
1220
+ callId: call.callId,
1221
+ ok: false,
1222
+ error: "host tools are disabled"
1223
+ });
1224
+ return;
1225
+ }
1226
+ const r = executeHostTool(
1227
+ call,
1228
+ typeof window !== "undefined" ? window : void 0
1229
+ );
783
1230
  emit({
784
1231
  type: "host.toolResult",
785
1232
  callId: call.callId,
786
- ok: false,
787
- error: "Host tools are read-only in this version; no action was taken."
1233
+ ok: r.ok,
1234
+ result: r.result,
1235
+ error: r.error
788
1236
  });
789
1237
  }
790
1238
  function stop() {
@@ -827,6 +1275,7 @@ function start(emit, origin, pageContext = true, provider) {
827
1275
  if (pageContextOn) {
828
1276
  void publishContext();
829
1277
  publishArtifactGrants();
1278
+ publishSitemap();
830
1279
  watchNavigation();
831
1280
  document.addEventListener("click", onClick, {
832
1281
  capture: true,
@@ -854,11 +1303,21 @@ function start(emit, origin, pageContext = true, provider) {
854
1303
  }
855
1304
  const theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
856
1305
  emit({ type: "host.theme", mode: theme });
1306
+ if (pageContextOn) {
1307
+ const tools = advertiseTools(
1308
+ typeof window !== "undefined" ? window : void 0
1309
+ );
1310
+ if (tools.length) emit({ type: "host.tools", tools });
1311
+ }
857
1312
  }
858
- var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, pageContextOn, contextProvider, navFire, origPushState, origReplaceState, snapshotModule, PII, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
1313
+ var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, pageContextOn, contextProvider, navFire, origPushState, origReplaceState, snapshotModule, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
859
1314
  var init_context = __esm({
860
1315
  "src/context.ts"() {
861
1316
  "use strict";
1317
+ init_redact();
1318
+ init_hoist();
1319
+ init_hoist_runtime();
1320
+ init_redact();
862
1321
  widgetOrigin = "";
863
1322
  MAX_ACTIVITY = 40;
864
1323
  send = null;
@@ -870,22 +1329,6 @@ var init_context = __esm({
870
1329
  origPushState = null;
871
1330
  origReplaceState = null;
872
1331
  snapshotModule = null;
873
- PII = [
874
- /\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
875
- // email
876
- /\b(?:\d[ -]?){13,19}\b/g,
877
- // card-ish
878
- /\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
879
- // ssn — dashed, spaced, OR bare 9 digits
880
- /\b\d{9,}\b/g,
881
- // long bare digit runs: account / MRN / routing numbers
882
- /(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
883
- // us phone
884
- /\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
885
- // stripe-style api keys
886
- /\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
887
- // jwt
888
- ];
889
1332
  FORM_CONTROLS = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
890
1333
  ACTIONABLE = 'a[href],button,input,select,textarea,summary,[role="button"],[role="link"],[role="menuitem"],[role="tab"],[role="option"],[role="checkbox"],[role="switch"],[role="radio"],[onclick],[tabindex]';
891
1334
  lastFocus = {};
@@ -904,7 +1347,7 @@ module.exports = __toCommonJS(react_exports);
904
1347
  var import_react = require("react");
905
1348
 
906
1349
  // src/protocol.ts
907
- var PROTOCOL_VERSION = 1;
1350
+ var PROTOCOL_VERSION = 3;
908
1351
  var CHANNEL = "mf-embed";
909
1352
  function envelope(payload, id) {
910
1353
  return {
@@ -918,6 +1361,213 @@ function isEnvelope(data) {
918
1361
  return typeof data === "object" && data !== null && data.channel === CHANNEL;
919
1362
  }
920
1363
 
1364
+ // src/geometry.ts
1365
+ var MARGIN = 20;
1366
+ var LAUNCHER = 56;
1367
+ var SNAP_RADIUS = 96;
1368
+ var TAB_W = 44;
1369
+ var TAB_H = 132;
1370
+ var EDGE_DOCK = 64;
1371
+ var DEFAULT_FLOAT_W = 420;
1372
+ var DEFAULT_FLOAT_H = 640;
1373
+ var MIN_FLOAT_W = 320;
1374
+ var MIN_FLOAT_H = 420;
1375
+ var DEFAULT_DOCK_W = 600;
1376
+ var MIN_DOCK_W = 320;
1377
+ var CORNERS = ["tl", "tr", "bl", "br"];
1378
+ function defaultGeometry() {
1379
+ return {
1380
+ // Dock is the default: a side panel reads as part of the host app.
1381
+ mode: "dock",
1382
+ corner: "br",
1383
+ x: 0,
1384
+ y: 0,
1385
+ floatW: DEFAULT_FLOAT_W,
1386
+ floatH: DEFAULT_FLOAT_H,
1387
+ dockSide: "right",
1388
+ tabY: 0,
1389
+ // 0 => centre it (see tabTop)
1390
+ dockW: DEFAULT_DOCK_W
1391
+ };
1392
+ }
1393
+ function clamp(n, lo, hi) {
1394
+ return Math.min(Math.max(n, lo), hi);
1395
+ }
1396
+ function cornerOrigin(corner, w, h, vw, vh) {
1397
+ return {
1398
+ x: corner[1] === "l" ? MARGIN : vw - MARGIN - w,
1399
+ y: corner[0] === "t" ? MARGIN : vh - MARGIN - h
1400
+ };
1401
+ }
1402
+ function launcherOrigin(g, vw, vh) {
1403
+ if (g.corner) return cornerOrigin(g.corner, LAUNCHER, LAUNCHER, vw, vh);
1404
+ return {
1405
+ x: clamp(g.x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER)),
1406
+ y: clamp(g.y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER))
1407
+ };
1408
+ }
1409
+ function growthCorner(g, vw, vh) {
1410
+ if (g.corner) return g.corner;
1411
+ const o = launcherOrigin(g, vw, vh);
1412
+ return nearestCorner(o.x + LAUNCHER / 2, o.y + LAUNCHER / 2, vw, vh);
1413
+ }
1414
+ function nearestCorner(cx, cy, vw, vh) {
1415
+ return `${cy < vh / 2 ? "t" : "b"}${cx < vw / 2 ? "l" : "r"}`;
1416
+ }
1417
+ function snapOrFree(x, y, vw, vh) {
1418
+ const cx = clamp(x, MARGIN, Math.max(MARGIN, vw - MARGIN - LAUNCHER));
1419
+ const cy = clamp(y, MARGIN, Math.max(MARGIN, vh - MARGIN - LAUNCHER));
1420
+ for (const corner of CORNERS) {
1421
+ const o = cornerOrigin(corner, LAUNCHER, LAUNCHER, vw, vh);
1422
+ if (Math.hypot(cx - o.x, cy - o.y) <= SNAP_RADIUS) {
1423
+ return { corner, x: cx, y: cy };
1424
+ }
1425
+ }
1426
+ return { corner: null, x: cx, y: cy };
1427
+ }
1428
+ function edgeDock(x, w, vw) {
1429
+ if (x <= EDGE_DOCK) return "left";
1430
+ if (x + w >= vw - EDGE_DOCK) return "right";
1431
+ return null;
1432
+ }
1433
+ function dragAnchor(g, vw, vh) {
1434
+ if (g.mode === "dock") {
1435
+ return { x: g.dockSide === "left" ? 0 : vw - TAB_W, y: tabTop(g, vh) };
1436
+ }
1437
+ return launcherOrigin(g, vw, vh);
1438
+ }
1439
+ function visibleBox(g, open, vw, vh) {
1440
+ if (g.mode === "dock") {
1441
+ const w2 = open ? Math.min(Math.max(g.dockW, MIN_DOCK_W), Math.max(0, vw - 2 * MARGIN)) : TAB_W;
1442
+ return { x: g.dockSide === "left" ? 0 : vw - w2, w: w2 };
1443
+ }
1444
+ const o = launcherOrigin(g, vw, vh);
1445
+ if (!open) return { x: o.x, w: LAUNCHER };
1446
+ const { w } = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
1447
+ return {
1448
+ x: growthCorner(g, vw, vh)[1] === "r" ? o.x + LAUNCHER - w : o.x,
1449
+ w
1450
+ };
1451
+ }
1452
+ function placeDrop(anchorX, anchorY, boxLeft, boxW, vw, vh) {
1453
+ const snapped = snapOrFree(anchorX, anchorY, vw, vh);
1454
+ if (snapped.corner) return { mode: "float", ...snapped };
1455
+ const side = edgeDock(boxLeft, boxW, vw);
1456
+ if (side) {
1457
+ return {
1458
+ mode: "dock",
1459
+ dockSide: side,
1460
+ tabY: clamp(anchorY, 0, Math.max(0, vh - TAB_H))
1461
+ };
1462
+ }
1463
+ return { mode: "float", ...snapped };
1464
+ }
1465
+ function clampSize(w, h, vw, vh, minW, minH) {
1466
+ return {
1467
+ w: Math.min(Math.max(w, minW), Math.max(0, vw - 2 * MARGIN)),
1468
+ h: Math.min(Math.max(h, minH), Math.max(0, vh - 2 * MARGIN))
1469
+ };
1470
+ }
1471
+ function applyResizeDelta(corner, startW, startH, dx, dy) {
1472
+ const wSign = corner[1] === "r" ? -1 : 1;
1473
+ const hSign = corner[0] === "b" ? -1 : 1;
1474
+ return { w: startW + wSign * dx, h: startH + hSign * dy };
1475
+ }
1476
+ function applyDockResizeDelta(side, startW, dx) {
1477
+ return side === "right" ? startW - dx : startW + dx;
1478
+ }
1479
+ function parseGeometry(raw) {
1480
+ if (!raw) return null;
1481
+ try {
1482
+ const p = JSON.parse(raw);
1483
+ if (!p || p.mode !== "float" && p.mode !== "dock") return null;
1484
+ const d = defaultGeometry();
1485
+ const num = (v, dv) => typeof v === "number" ? v : dv;
1486
+ return {
1487
+ mode: p.mode,
1488
+ corner: CORNERS.includes(p.corner) ? p.corner : null,
1489
+ x: num(p.x, d.x),
1490
+ y: num(p.y, d.y),
1491
+ floatW: num(p.floatW, d.floatW),
1492
+ floatH: num(p.floatH, d.floatH),
1493
+ dockSide: p.dockSide === "left" ? "left" : "right",
1494
+ tabY: num(p.tabY, d.tabY),
1495
+ dockW: num(p.dockW, d.dockW)
1496
+ };
1497
+ } catch {
1498
+ return null;
1499
+ }
1500
+ }
1501
+ function serializeGeometry(g) {
1502
+ return JSON.stringify(g);
1503
+ }
1504
+ var px = (n) => `${n}px`;
1505
+ function boxAt(ox, oy, w, h, corner, vw, vh) {
1506
+ const right = corner[1] === "r";
1507
+ const bottom = corner[0] === "b";
1508
+ const rawX = right ? vw - (ox + LAUNCHER) : ox;
1509
+ const rawY = bottom ? vh - (oy + LAUNCHER) : oy;
1510
+ const x = clamp(rawX, MARGIN, Math.max(MARGIN, vw - MARGIN - w));
1511
+ const y = clamp(rawY, MARGIN, Math.max(MARGIN, vh - MARGIN - h));
1512
+ return {
1513
+ top: bottom ? "auto" : px(y),
1514
+ bottom: bottom ? px(y) : "auto",
1515
+ left: right ? "auto" : px(x),
1516
+ right: right ? px(x) : "auto",
1517
+ width: px(w),
1518
+ height: px(h)
1519
+ };
1520
+ }
1521
+ function tabTop(g, vh) {
1522
+ const max = Math.max(0, vh - TAB_H);
1523
+ return clamp(g.tabY || Math.round((vh - TAB_H) / 2), 0, max);
1524
+ }
1525
+ function tabBox(g, vw, vh) {
1526
+ const left = g.dockSide === "left";
1527
+ return {
1528
+ top: px(tabTop(g, vh)),
1529
+ bottom: "auto",
1530
+ left: left ? "0px" : "auto",
1531
+ right: left ? "auto" : "0px",
1532
+ width: px(TAB_W),
1533
+ height: px(TAB_H)
1534
+ };
1535
+ }
1536
+ function floatChrome(g, w, h, vw, vh, growth) {
1537
+ const o = launcherOrigin(g, vw, vh);
1538
+ return boxAt(o.x, o.y, w, h, growth ?? growthCorner(g, vw, vh), vw, vh);
1539
+ }
1540
+ function dockChrome(g, w, h, vw, vh) {
1541
+ const left = g.dockSide === "left";
1542
+ return {
1543
+ top: px(tabTop(g, vh)),
1544
+ bottom: "auto",
1545
+ left: left ? "0px" : "auto",
1546
+ right: left ? "auto" : "0px",
1547
+ width: px(w),
1548
+ height: px(h)
1549
+ };
1550
+ }
1551
+ function floatBox(g, vw, vh, growth) {
1552
+ const f = clampSize(g.floatW, g.floatH, vw, vh, MIN_FLOAT_W, MIN_FLOAT_H);
1553
+ return floatChrome(g, f.w, f.h, vw, vh, growth);
1554
+ }
1555
+ function dockBox(g, vw, _vh) {
1556
+ const w = Math.min(
1557
+ Math.max(g.dockW, MIN_DOCK_W),
1558
+ Math.max(0, vw - 2 * MARGIN)
1559
+ );
1560
+ const left = g.dockSide === "left";
1561
+ return {
1562
+ top: "0px",
1563
+ bottom: "0px",
1564
+ height: "auto",
1565
+ left: left ? "0px" : "auto",
1566
+ right: left ? "auto" : "0px",
1567
+ width: px(w)
1568
+ };
1569
+ }
1570
+
921
1571
  // src/loader.ts
922
1572
  function devRequested() {
923
1573
  try {
@@ -936,11 +1586,31 @@ var EmbedHost = class {
936
1586
  this.queue = [];
937
1587
  this.ready = false;
938
1588
  this.open = false;
939
- this.expanded = false;
940
- /** Null until the user drags; then it pins the corner offset and survives reloads. */
941
- this.pos = null;
942
- /** Where the launcher sat when the current drag began; deltas are applied to this. */
943
- this.dragBase = null;
1589
+ /** The persisted box: launcher corner, float size, dock state/side/width. */
1590
+ this.geo = defaultGeometry();
1591
+ /** Float size at resizeStart; resizeMove deltas apply against it (they're cumulative). */
1592
+ this.resizeBase = null;
1593
+ /** Docked width at resizeStart — the docked panel resizes in one axis only. */
1594
+ this.dockBase = DEFAULT_DOCK_W;
1595
+ /** The widget's logical anchor when the drag began, and where it is now. Deltas arrive
1596
+ * cumulative from the press, so the current spot is simply origin + delta — no need to
1597
+ * read the box back, which stops being the thing we're moving the moment it docks. */
1598
+ this.dragFrom = null;
1599
+ this.dragAt = { x: 0, y: 0 };
1600
+ /** The painted box when the drag began — the edge test measures what you can SEE
1601
+ * touching the screen edge, which is not the anchor once the panel is open. */
1602
+ this.dragBox = { x: 0, w: 0 };
1603
+ /** Growth direction frozen for the drag — see floatChrome's `growth` param. */
1604
+ this.dragGrowth = null;
1605
+ /**
1606
+ * Has this drag been clear of the dock band yet?
1607
+ *
1608
+ * The resting position is INSIDE the band — a corner-anchored widget sits MARGIN
1609
+ * (20px) from the edge, well within EDGE_DOCK (64px) — so without this, picking one
1610
+ * up docked it instantly, before it had moved anywhere. Docking now requires
1611
+ * ENTERING the band rather than merely starting in it.
1612
+ */
1613
+ this.dragLeftBand = false;
944
1614
  /** Loaded on first open. Holds everything that touches the customer's DOM. */
945
1615
  this.context = null;
946
1616
  /** The host element; kept so `destroy()` can remove it (React lifecycle). */
@@ -963,87 +1633,10 @@ var EmbedHost = class {
963
1633
  if (event.origin !== this.config.origin) return;
964
1634
  if (event.source !== this.iframe?.contentWindow) return;
965
1635
  if (!isEnvelope(event.data)) return;
966
- const msg = event.data.payload;
967
- switch (msg.type) {
968
- case "widget.ready":
969
- this.ready = true;
970
- this.send({
971
- type: "host.ready",
972
- protocol: PROTOCOL_VERSION,
973
- origin: location.origin
974
- });
975
- this.flush();
976
- if (this.inline) void this.loadContext();
977
- break;
978
- case "widget.setOpen":
979
- if (this.inline) break;
980
- this.open = msg.open;
981
- this.iframe?.setAttribute("data-open", String(msg.open));
982
- if (!msg.open) {
983
- this.expanded = false;
984
- this.iframe?.setAttribute("data-expanded", "false");
985
- }
986
- if (msg.open) void this.loadContext();
987
- break;
988
- case "widget.setExpanded":
989
- if (this.inline) break;
990
- this.expanded = msg.expanded;
991
- this.iframe?.setAttribute("data-expanded", String(msg.expanded));
992
- if (this.iframe) this.iframe.style.height = "";
993
- break;
994
- case "widget.resize":
995
- if (this.inline) break;
996
- if (this.iframe && this.open && !this.expanded) {
997
- this.iframe.style.height = `${Math.min(msg.height, window.innerHeight - 40)}px`;
998
- }
999
- break;
1000
- case "widget.requestContext":
1001
- void this.loadContext().then((m) => m.provideContext());
1002
- break;
1003
- case "widget.requestSnapshot":
1004
- void this.loadContext().then((m) => m.sendSnapshot(this.send));
1005
- break;
1006
- case "widget.readRegion":
1007
- void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
1008
- break;
1009
- case "widget.callTool":
1010
- void this.loadContext().then((m) => m.callTool(msg.call, this.send));
1011
- break;
1012
- // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
1013
- // doesn't draw one), and moving or hiding the host's own panel from inside it
1014
- // would be us redecorating their app. An older cached loader could still be told
1015
- // any of these by a newer widget, so they're guarded rather than assumed absent.
1016
- case "widget.dragStart":
1017
- if (this.inline) break;
1018
- this.dragBase = this.cornerOffset();
1019
- break;
1020
- case "widget.dragMove":
1021
- if (this.inline) break;
1022
- this.dragTo(msg.dx, msg.dy);
1023
- break;
1024
- case "widget.dragEnd":
1025
- if (this.inline) break;
1026
- this.dragBase = null;
1027
- this.writePos(this.pos);
1028
- break;
1029
- case "widget.setMenu":
1030
- if (this.inline) break;
1031
- if (this.iframe && !this.open) {
1032
- this.iframe.style.height = msg.open ? `${64 + msg.height}px` : "";
1033
- }
1034
- break;
1035
- case "widget.resetPos":
1036
- if (this.inline) break;
1037
- this.resetPos();
1038
- break;
1039
- case "widget.hide":
1040
- if (this.inline) break;
1041
- if (this.hostEl) this.hostEl.style.display = "none";
1042
- break;
1043
- case "widget.needsAuth":
1044
- void this.provideAuth();
1045
- break;
1046
- }
1636
+ this.handle(event.data.payload);
1637
+ };
1638
+ this.onViewportResize = () => {
1639
+ this.place();
1047
1640
  };
1048
1641
  this.send = (msg) => {
1049
1642
  if (!this.ready) {
@@ -1069,34 +1662,36 @@ var EmbedHost = class {
1069
1662
  ].join(";");
1070
1663
  this.config.container.appendChild(host);
1071
1664
  } else {
1072
- this.pos = this.readPos();
1665
+ this.geo = this.readGeometry();
1073
1666
  host.style.cssText = [
1074
1667
  "all: initial",
1075
1668
  "position: fixed",
1076
- `right: ${this.pos ? this.pos.right : 20}px`,
1077
- `bottom: ${this.pos ? this.pos.bottom : 20}px`,
1078
1669
  // Below the max so a host that genuinely needs to cover us (a modal, a cookie
1079
1670
  // banner they are legally obliged to show) still can.
1080
1671
  "z-index: 2147483000",
1081
- "contain: layout style"
1672
+ "contain: layout style",
1673
+ // The host element owns the BOX now (position AND size); the iframe fills it.
1674
+ // Transitioning the box is what makes the corner snap and the open/dock read as
1675
+ // motion rather than a jump. Suppressed during drag/resize (see those handlers).
1676
+ "transition: width .18s ease, height .18s ease, top .18s ease, right .18s ease, bottom .18s ease, left .18s ease"
1082
1677
  ].join(";");
1083
1678
  document.body.appendChild(host);
1084
1679
  }
1085
1680
  this.shadow = host.attachShadow({ mode: "closed" });
1086
1681
  const style = document.createElement("style");
1087
- style.textContent = ":host{all:initial}iframe{border:0;display:block;color-scheme:light dark;}" + (this.inline ? "iframe{width:100%;height:100%}" : (
1088
- // Hairline ring + soft ambient: the widget must read as a crisp card on
1089
- // ANY host background, including pure white where a shadow alone bleeds.
1090
- '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)}'
1682
+ style.textContent = ":host{all:initial}iframe{border:0;display:block;width:100%;height:100%;background:transparent}" + (this.inline ? "" : (
1683
+ // color-scheme only on the OPEN panel on a collapsed launcher it makes the
1684
+ // UA paint an opaque canvas base behind our shapes.
1685
+ ':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}'
1091
1686
  ));
1092
1687
  this.shadow.appendChild(style);
1093
1688
  const iframe = document.createElement("iframe");
1094
1689
  iframe.title = "matterfact assistant";
1095
1690
  iframe.setAttribute(
1096
1691
  "sandbox",
1097
- "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
1692
+ "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
1098
1693
  );
1099
- iframe.setAttribute("allow", "microphone");
1694
+ iframe.setAttribute("allow", "microphone; clipboard-write");
1100
1695
  iframe.src = `${this.config.origin}/embed/chat?k=${encodeURIComponent(
1101
1696
  this.config.publishableKey
1102
1697
  )}&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
@@ -1105,6 +1700,227 @@ var EmbedHost = class {
1105
1700
  this.iframe = iframe;
1106
1701
  this.shadow.appendChild(iframe);
1107
1702
  window.addEventListener("message", this.onMessage);
1703
+ if (!this.inline) {
1704
+ window.addEventListener("resize", this.onViewportResize);
1705
+ this.place();
1706
+ }
1707
+ }
1708
+ /**
1709
+ * Test seam. The shadow root is CLOSED, so a test cannot reach the iframe to forge a
1710
+ * source-valid MessageEvent — this routes a message through the identical logic
1711
+ * without weakening the origin/source checks above, which stay the only real door.
1712
+ */
1713
+ __testHandle(msg) {
1714
+ this.handle(msg);
1715
+ }
1716
+ handle(msg) {
1717
+ switch (msg.type) {
1718
+ case "widget.ready":
1719
+ this.ready = true;
1720
+ this.send({
1721
+ type: "host.ready",
1722
+ protocol: PROTOCOL_VERSION,
1723
+ origin: location.origin
1724
+ });
1725
+ this.send({ type: "host.theme", mode: this.themeMode() });
1726
+ this.flush();
1727
+ if (this.inline) void this.loadContext();
1728
+ break;
1729
+ case "widget.setOpen":
1730
+ if (this.inline) break;
1731
+ this.open = msg.open;
1732
+ this.place();
1733
+ if (msg.open) void this.loadContext();
1734
+ break;
1735
+ case "widget.setMode":
1736
+ if (this.inline) break;
1737
+ this.geo.mode = msg.mode;
1738
+ this.writeGeometry(this.geo);
1739
+ this.place();
1740
+ break;
1741
+ case "widget.setDockSide":
1742
+ if (this.inline) break;
1743
+ this.geo.dockSide = msg.side;
1744
+ this.writeGeometry(this.geo);
1745
+ this.place();
1746
+ break;
1747
+ case "widget.snapCorner":
1748
+ if (this.inline) break;
1749
+ this.geo.corner = msg.corner;
1750
+ this.writeGeometry(this.geo);
1751
+ this.place();
1752
+ break;
1753
+ case "widget.setLauncherRegion": {
1754
+ if (this.inline || this.open) break;
1755
+ const vw = window.innerWidth;
1756
+ const vh = window.innerHeight;
1757
+ if (msg.w <= LAUNCHER && msg.h <= LAUNCHER) {
1758
+ this.place();
1759
+ } else {
1760
+ const region = this.geo.mode === "dock" ? dockChrome(this.geo, msg.w, msg.h, vw, vh) : floatChrome(this.geo, msg.w, msg.h, vw, vh);
1761
+ this.applyBox(region, msg.h > LAUNCHER ? "menu" : "pill");
1762
+ }
1763
+ break;
1764
+ }
1765
+ case "widget.resize":
1766
+ if (this.inline) break;
1767
+ if (this.hostEl && this.open && this.geo.mode === "float") {
1768
+ this.hostEl.style.height = `${Math.min(
1769
+ msg.height,
1770
+ Math.max(0, window.innerHeight - 2 * MARGIN)
1771
+ )}px`;
1772
+ }
1773
+ break;
1774
+ case "widget.resizeStart":
1775
+ if (this.inline) break;
1776
+ this.resizeBase = { w: this.geo.floatW, h: this.geo.floatH };
1777
+ this.dockBase = this.geo.dockW;
1778
+ if (this.hostEl) this.hostEl.style.transition = "none";
1779
+ break;
1780
+ case "widget.resizeMove": {
1781
+ if (this.inline || !this.resizeBase) break;
1782
+ if (this.geo.mode === "dock") {
1783
+ const w = applyDockResizeDelta(
1784
+ this.geo.dockSide,
1785
+ this.dockBase,
1786
+ msg.dx
1787
+ );
1788
+ this.geo.dockW = Math.min(
1789
+ Math.max(w, MIN_DOCK_W),
1790
+ Math.max(0, window.innerWidth - 2 * MARGIN)
1791
+ );
1792
+ } else {
1793
+ const next = applyResizeDelta(
1794
+ growthCorner(this.geo, window.innerWidth, window.innerHeight),
1795
+ this.resizeBase.w,
1796
+ this.resizeBase.h,
1797
+ msg.dx,
1798
+ msg.dy
1799
+ );
1800
+ const fit = clampSize(
1801
+ next.w,
1802
+ next.h,
1803
+ window.innerWidth,
1804
+ window.innerHeight,
1805
+ MIN_FLOAT_W,
1806
+ MIN_FLOAT_H
1807
+ );
1808
+ this.geo.floatW = fit.w;
1809
+ this.geo.floatH = fit.h;
1810
+ }
1811
+ this.place();
1812
+ break;
1813
+ }
1814
+ case "widget.resizeEnd":
1815
+ if (this.inline) break;
1816
+ this.resizeBase = null;
1817
+ if (this.hostEl) this.hostEl.style.transition = "";
1818
+ this.writeGeometry(this.geo);
1819
+ break;
1820
+ case "widget.requestContext":
1821
+ void this.loadContext().then((m) => m.provideContext());
1822
+ break;
1823
+ case "widget.requestSnapshot":
1824
+ void this.loadContext().then((m) => m.sendSnapshot(this.send));
1825
+ break;
1826
+ case "widget.readRegion":
1827
+ void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
1828
+ break;
1829
+ case "widget.callTool":
1830
+ void this.loadContext().then((m) => m.callTool(msg.call, this.send));
1831
+ break;
1832
+ // Everything below is LAUNCHER chrome: there is no launcher inline (the widget
1833
+ // doesn't draw one), and moving or hiding the host's own panel from inside it
1834
+ // would be us redecorating their app. An older cached loader could still be told
1835
+ // any of these by a newer widget, so they're guarded rather than assumed absent.
1836
+ case "widget.dragStart":
1837
+ if (this.inline) break;
1838
+ this.dragFrom = dragAnchor(
1839
+ this.geo,
1840
+ window.innerWidth,
1841
+ window.innerHeight
1842
+ );
1843
+ this.dragAt = { ...this.dragFrom };
1844
+ this.dragLeftBand = false;
1845
+ this.dragGrowth = growthCorner(
1846
+ this.geo,
1847
+ window.innerWidth,
1848
+ window.innerHeight
1849
+ );
1850
+ this.dragBox = visibleBox(
1851
+ this.geo,
1852
+ this.open,
1853
+ window.innerWidth,
1854
+ window.innerHeight
1855
+ );
1856
+ if (this.hostEl) this.hostEl.style.transition = "none";
1857
+ break;
1858
+ case "widget.dragMove": {
1859
+ if (this.inline || !this.dragFrom) break;
1860
+ const vw = window.innerWidth;
1861
+ const vh = window.innerHeight;
1862
+ this.dragAt = {
1863
+ x: this.dragFrom.x + msg.dx,
1864
+ y: this.dragFrom.y + msg.dy
1865
+ };
1866
+ const side = edgeDock(this.dragBox.x + msg.dx, this.dragBox.w, vw);
1867
+ if (!side) this.dragLeftBand = true;
1868
+ if (side && (this.dragLeftBand || this.geo.mode === "dock")) {
1869
+ this.geo.mode = "dock";
1870
+ this.geo.dockSide = side;
1871
+ this.geo.tabY = Math.max(0, this.dragAt.y);
1872
+ } else {
1873
+ this.geo.mode = "float";
1874
+ this.geo.corner = null;
1875
+ this.geo.x = this.dragAt.x;
1876
+ this.geo.y = this.dragAt.y;
1877
+ }
1878
+ this.place();
1879
+ break;
1880
+ }
1881
+ case "widget.dragEnd": {
1882
+ if (this.inline || !this.dragFrom) break;
1883
+ const at = placeDrop(
1884
+ this.dragAt.x,
1885
+ this.dragAt.y,
1886
+ this.dragBox.x + (this.dragAt.x - this.dragFrom.x),
1887
+ this.dragBox.w,
1888
+ window.innerWidth,
1889
+ window.innerHeight
1890
+ );
1891
+ this.geo.mode = at.mode;
1892
+ if (at.mode === "dock") {
1893
+ this.geo.dockSide = at.dockSide;
1894
+ this.geo.tabY = at.tabY;
1895
+ } else {
1896
+ this.geo.corner = at.corner;
1897
+ this.geo.x = at.x;
1898
+ this.geo.y = at.y;
1899
+ }
1900
+ this.dragFrom = null;
1901
+ this.dragGrowth = null;
1902
+ if (this.hostEl) this.hostEl.style.transition = "";
1903
+ this.place();
1904
+ this.writeGeometry(this.geo);
1905
+ break;
1906
+ }
1907
+ case "widget.resetPos":
1908
+ if (this.inline) break;
1909
+ this.geo = defaultGeometry();
1910
+ this.writeGeometry(this.geo);
1911
+ this.place();
1912
+ break;
1913
+ case "widget.hide":
1914
+ if (this.inline) break;
1915
+ if (this.hostEl) this.hostEl.style.display = "none";
1916
+ break;
1917
+ case "widget.needsAuth":
1918
+ void this.provideAuth();
1919
+ break;
1920
+ case "widget.navigate":
1921
+ void this.loadContext().then((m) => m.navigateHost(msg.href));
1922
+ break;
1923
+ }
1108
1924
  }
1109
1925
  /**
1110
1926
  * Own a drag for its lifetime.
@@ -1118,47 +1934,77 @@ var EmbedHost = class {
1118
1934
  * We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
1119
1935
  * widget has to own the gesture. All we do is take a delta and place the element.
1120
1936
  */
1121
- cornerOffset() {
1122
- const rect = this.hostEl.getBoundingClientRect();
1123
- return {
1124
- right: window.innerWidth - rect.right,
1125
- bottom: window.innerHeight - rect.bottom
1126
- };
1937
+ /** Write a box onto the host element and stamp the mode the shadow CSS keys off. */
1938
+ applyBox(box, mode) {
1939
+ if (!this.hostEl || this.inline) return;
1940
+ const s = this.hostEl.style;
1941
+ s.top = box.top;
1942
+ s.right = box.right;
1943
+ s.bottom = box.bottom;
1944
+ s.left = box.left;
1945
+ s.width = box.width;
1946
+ s.height = box.height;
1947
+ this.hostEl.dataset.mode = mode;
1948
+ if (mode === "tab" || mode === "dock")
1949
+ this.hostEl.dataset.flush = this.geo.dockSide;
1950
+ else delete this.hostEl.dataset.flush;
1127
1951
  }
1128
- dragTo(dx, dy) {
1129
- if (!this.dragBase || !this.hostEl) return;
1130
- const rect = this.hostEl.getBoundingClientRect();
1131
- const maxRight = Math.max(0, window.innerWidth - rect.width);
1132
- const maxBottom = Math.max(0, window.innerHeight - rect.height);
1133
- const right = Math.min(Math.max(0, this.dragBase.right - dx), maxRight);
1134
- const bottom = Math.min(Math.max(0, this.dragBase.bottom - dy), maxBottom);
1135
- this.pos = { right, bottom };
1136
- this.hostEl.style.right = `${right}px`;
1137
- this.hostEl.style.bottom = `${bottom}px`;
1138
- }
1139
- /** Reset to the default corner (a menu action — the drag is otherwise sticky). */
1140
- resetPos() {
1141
- this.pos = null;
1142
- this.writePos(null);
1143
- if (this.hostEl) {
1144
- this.hostEl.style.right = "20px";
1145
- this.hostEl.style.bottom = "20px";
1952
+ /** Re-apply the box for whatever state we're in: collapsed launcher, float, or dock. */
1953
+ /**
1954
+ * The growth direction to use RIGHT NOW — frozen while a drag is in flight.
1955
+ *
1956
+ * Every consumer has to agree on this, which is the bug that made the widget
1957
+ * flip-flop under small movements near the viewport centre: the box was frozen but
1958
+ * `host.geometry` still published a freshly-derived growth, so the WIDGET kept
1959
+ * swapping which corner it drew against (and it animates that), twitching back and
1960
+ * forth while the loader held perfectly still.
1961
+ */
1962
+ growthNow() {
1963
+ return this.dragGrowth ?? growthCorner(this.geo, window.innerWidth, window.innerHeight);
1964
+ }
1965
+ place() {
1966
+ const vw = window.innerWidth;
1967
+ const vh = window.innerHeight;
1968
+ if (!this.open) {
1969
+ const dock = this.geo.mode === "dock";
1970
+ this.applyBox(
1971
+ dock ? tabBox(this.geo, vw, vh) : floatChrome(this.geo, LAUNCHER, LAUNCHER, vw, vh, this.growthNow()),
1972
+ dock ? "tab" : "launcher"
1973
+ );
1974
+ } else if (this.geo.mode === "dock") {
1975
+ this.applyBox(dockBox(this.geo, vw, vh), "dock");
1976
+ } else {
1977
+ this.applyBox(floatBox(this.geo, vw, vh, this.growthNow()), "float");
1146
1978
  }
1979
+ this.publishGeometry();
1980
+ }
1981
+ /**
1982
+ * Tell the widget where it is. It can't see the viewport and doesn't own the box, so
1983
+ * without this it draws its launcher against a stale guess — which is what made the
1984
+ * circle jump when the launcher sat on the left (see `host.geometry` in the protocol).
1985
+ */
1986
+ publishGeometry() {
1987
+ this.send({
1988
+ type: "host.geometry",
1989
+ mode: this.geo.mode,
1990
+ dockSide: this.geo.dockSide,
1991
+ growth: this.growthNow()
1992
+ });
1993
+ }
1994
+ /** The theme to publish: an explicit data-theme wins, else the OS preference. */
1995
+ themeMode() {
1996
+ return this.config.theme === "auto" ? matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light" : this.config.theme;
1147
1997
  }
1148
- readPos() {
1998
+ readGeometry() {
1149
1999
  try {
1150
- const raw = localStorage.getItem(POS_KEY);
1151
- if (!raw) return null;
1152
- const p = JSON.parse(raw);
1153
- return typeof p?.right === "number" && typeof p?.bottom === "number" ? { right: p.right, bottom: p.bottom } : null;
2000
+ return parseGeometry(localStorage.getItem(POS_KEY)) ?? defaultGeometry();
1154
2001
  } catch {
1155
- return null;
2002
+ return defaultGeometry();
1156
2003
  }
1157
2004
  }
1158
- writePos(pos) {
2005
+ writeGeometry(g) {
1159
2006
  try {
1160
- if (pos) localStorage.setItem(POS_KEY, JSON.stringify(pos));
1161
- else localStorage.removeItem(POS_KEY);
2007
+ localStorage.setItem(POS_KEY, serializeGeometry(g));
1162
2008
  } catch {
1163
2009
  }
1164
2010
  }
@@ -1214,6 +2060,7 @@ var EmbedHost = class {
1214
2060
  * calls this. */
1215
2061
  destroy() {
1216
2062
  window.removeEventListener("message", this.onMessage);
2063
+ window.removeEventListener("resize", this.onViewportResize);
1217
2064
  this.hostEl?.remove();
1218
2065
  this.hostEl = null;
1219
2066
  this.iframe = null;
@@ -1231,6 +2078,17 @@ function mount(config) {
1231
2078
  // src/react.tsx
1232
2079
  var import_jsx_runtime = require("react/jsx-runtime");
1233
2080
  var DEFAULT_ORIGIN = "https://app.matterfact.com";
2081
+ function writeActionsGlobal(actions) {
2082
+ if (typeof window === "undefined") return;
2083
+ const w = window;
2084
+ const mf = w.matterfact ?? (w.matterfact = {});
2085
+ mf.hoist = { ...mf.hoist, actions: actions ?? { navigate: "off" } };
2086
+ }
2087
+ function writeSitemapGlobal(sitemap) {
2088
+ if (typeof window === "undefined" || sitemap === void 0) return;
2089
+ const w = window;
2090
+ (w.matterfact ?? (w.matterfact = {})).sitemap = sitemap;
2091
+ }
1234
2092
  function MatterfactAgent({
1235
2093
  publishableKey,
1236
2094
  widgetOrigin: widgetOrigin2,
@@ -1239,19 +2097,29 @@ function MatterfactAgent({
1239
2097
  getAuthToken,
1240
2098
  getPageContext,
1241
2099
  inline = false,
1242
- className,
2100
+ className: className2,
1243
2101
  style,
1244
2102
  pageContext,
1245
- dev
2103
+ dev,
2104
+ actions,
2105
+ sitemap
1246
2106
  }) {
1247
2107
  const authRef = (0, import_react.useRef)(getAuthToken);
1248
2108
  authRef.current = getAuthToken;
1249
2109
  const pageContextRef = (0, import_react.useRef)(getPageContext);
1250
2110
  pageContextRef.current = getPageContext;
2111
+ const actionsRef = (0, import_react.useRef)(actions);
2112
+ actionsRef.current = actions;
2113
+ const actionsKey = JSON.stringify(actions ?? null);
1251
2114
  const slot = (0, import_react.useRef)(null);
2115
+ const sitemapKey = JSON.stringify(sitemap ?? null);
2116
+ (0, import_react.useEffect)(() => {
2117
+ writeSitemapGlobal(sitemap);
2118
+ }, [sitemapKey]);
1252
2119
  (0, import_react.useEffect)(() => {
1253
2120
  if (typeof window === "undefined") return;
1254
2121
  if (inline && !slot.current) return;
2122
+ writeActionsGlobal(actionsRef.current);
1255
2123
  const config = {
1256
2124
  publishableKey,
1257
2125
  origin: widgetOrigin2 || DEFAULT_ORIGIN,
@@ -1274,13 +2142,22 @@ function MatterfactAgent({
1274
2142
  console.error("[matterfact] failed to mount the embed widget", e);
1275
2143
  }
1276
2144
  return () => host?.destroy();
1277
- }, [publishableKey, widgetOrigin2, surface, theme, inline, pageContext, dev]);
2145
+ }, [
2146
+ publishableKey,
2147
+ widgetOrigin2,
2148
+ surface,
2149
+ theme,
2150
+ inline,
2151
+ pageContext,
2152
+ dev,
2153
+ actionsKey
2154
+ ]);
1278
2155
  if (!inline) return null;
1279
2156
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1280
2157
  "div",
1281
2158
  {
1282
2159
  ref: slot,
1283
- className,
2160
+ className: className2,
1284
2161
  style: { width: "100%", height: "100%", ...style }
1285
2162
  }
1286
2163
  );
@@ -1292,7 +2169,7 @@ function MatterfactArtifact({
1292
2169
  token,
1293
2170
  widgetOrigin: widgetOrigin2,
1294
2171
  theme = "auto",
1295
- className,
2172
+ className: className2,
1296
2173
  style
1297
2174
  }) {
1298
2175
  const origin = widgetOrigin2 || DEFAULT_ARTIFACT_ORIGIN;
@@ -1302,7 +2179,7 @@ function MatterfactArtifact({
1302
2179
  {
1303
2180
  src,
1304
2181
  title: `matterfact artifact ${slug}`,
1305
- className,
2182
+ className: className2,
1306
2183
  style: { width: "100%", height: 600, border: 0, ...style },
1307
2184
  sandbox: "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
1308
2185
  }