@matterfact/embed 0.2.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.
- package/LICENSE +190 -0
- package/README.md +18 -0
- package/dist/chunk-6N5OYQLF.js +300 -0
- package/dist/chunk-6N5OYQLF.js.map +1 -0
- package/dist/chunk-6VW4CCNF.js +3 -0
- package/dist/chunk-6VW4CCNF.js.map +7 -0
- package/dist/chunk-MWRVWYWX.js +2 -0
- package/dist/chunk-MWRVWYWX.js.map +7 -0
- package/dist/chunk-U4IRZU6E.js +2 -0
- package/dist/chunk-U4IRZU6E.js.map +7 -0
- package/dist/chunk-VMYCQB4O.js +289 -0
- package/dist/chunk-VMYCQB4O.js.map +1 -0
- package/dist/context-23EUTK33.js +3 -0
- package/dist/context-23EUTK33.js.map +1 -0
- package/dist/context-CWMQYYFI.js +22 -0
- package/dist/context-CWMQYYFI.js.map +1 -0
- package/dist/embed.js +2 -0
- package/dist/embed.js.map +7 -0
- package/dist/index.cjs +1110 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +437 -0
- package/dist/index.d.ts +437 -0
- package/dist/index.js +326 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1157 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +35 -0
- package/dist/react.d.ts +35 -0
- package/dist/react.js +357 -0
- package/dist/react.js.map +1 -0
- package/dist/snapshot-KN2KJ2PM.js +444 -0
- package/dist/snapshot-KN2KJ2PM.js.map +1 -0
- package/dist/snapshot-Z7YUSB27.js +458 -0
- package/dist/snapshot-Z7YUSB27.js.map +1 -0
- package/package.json +56 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var domAccessibilityApi = require('dom-accessibility-api');
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __esm = (fn, res) => function __init() {
|
|
8
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
9
|
+
};
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
function createStyleCache() {
|
|
15
|
+
const cache = /* @__PURE__ */ new WeakMap();
|
|
16
|
+
return ((el, pseudo) => {
|
|
17
|
+
if (pseudo) return window.getComputedStyle(el, pseudo);
|
|
18
|
+
let style = cache.get(el);
|
|
19
|
+
if (!style) {
|
|
20
|
+
style = window.getComputedStyle(el);
|
|
21
|
+
cache.set(el, style);
|
|
22
|
+
}
|
|
23
|
+
return style;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function hasLayout() {
|
|
27
|
+
return document.documentElement.getClientRects().length > 0;
|
|
28
|
+
}
|
|
29
|
+
function isVisible(el, style, layout) {
|
|
30
|
+
const display = style.display;
|
|
31
|
+
if (display === "none") return false;
|
|
32
|
+
const visibility = style.visibility;
|
|
33
|
+
if (visibility === "hidden" || visibility === "collapse") return false;
|
|
34
|
+
if (Number(style.opacity) === 0) return false;
|
|
35
|
+
if (layout && display !== "contents") {
|
|
36
|
+
const box = el.getBoundingClientRect();
|
|
37
|
+
if (box.width <= 0 || box.height <= 0) return false;
|
|
38
|
+
if (el.getClientRects().length === 0) return false;
|
|
39
|
+
if (box.right < -2e3 || box.bottom < -2e3) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function receivesPointerEvents(style) {
|
|
44
|
+
return style.pointerEvents !== "none";
|
|
45
|
+
}
|
|
46
|
+
function isEditable(el) {
|
|
47
|
+
const v = el.getAttribute("contenteditable");
|
|
48
|
+
return v !== null && v !== "false";
|
|
49
|
+
}
|
|
50
|
+
function isInteractive(el, role, style, parentStyle) {
|
|
51
|
+
const tag = el.tagName;
|
|
52
|
+
if (tag === "A" && el.hasAttribute("href")) return true;
|
|
53
|
+
if (INTERACTIVE_TAGS.has(tag)) return true;
|
|
54
|
+
if (role !== null && INTERACTIVE_ROLES.has(role)) return true;
|
|
55
|
+
const tabindex = el.getAttribute("tabindex");
|
|
56
|
+
if (tabindex !== null && Number(tabindex) >= 0) return true;
|
|
57
|
+
if (isEditable(el)) return true;
|
|
58
|
+
if (style.cursor === "pointer" && parentStyle?.cursor !== "pointer")
|
|
59
|
+
return true;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function isOpaque(el) {
|
|
63
|
+
return OPAQUE_TAGS.has(el.tagName) || isEditable(el);
|
|
64
|
+
}
|
|
65
|
+
function isFilled(el, role) {
|
|
66
|
+
const tag = el.tagName;
|
|
67
|
+
if (tag === "INPUT" || tag === "TEXTAREA") {
|
|
68
|
+
if (!VALUE_ROLES.has(role)) return false;
|
|
69
|
+
return (el.value ?? "").length > 0;
|
|
70
|
+
}
|
|
71
|
+
if (isEditable(el)) return (el.textContent ?? "").trim().length > 0;
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function ariaFlag(el, attr) {
|
|
75
|
+
const v = el.getAttribute(attr);
|
|
76
|
+
if (v === "true") return "true";
|
|
77
|
+
if (v === "mixed") return "mixed";
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function states(el, role) {
|
|
81
|
+
const out = [];
|
|
82
|
+
const tag = el.tagName;
|
|
83
|
+
if (tag === "INPUT" && (role === "checkbox" || role === "radio")) {
|
|
84
|
+
if (el.checked) out.push("[checked]");
|
|
85
|
+
} else {
|
|
86
|
+
const checked = ariaFlag(el, "aria-checked");
|
|
87
|
+
if (checked === "mixed") out.push("[checked=mixed]");
|
|
88
|
+
else if (checked === "true") out.push("[checked]");
|
|
89
|
+
}
|
|
90
|
+
if (domAccessibilityApi.isDisabled(el)) out.push("[disabled]");
|
|
91
|
+
const expanded = el.getAttribute("aria-expanded");
|
|
92
|
+
if (expanded === "true" || expanded === "false") {
|
|
93
|
+
if (expanded === "true") out.push("[expanded]");
|
|
94
|
+
} else if (tag === "SUMMARY") {
|
|
95
|
+
const details = el.parentElement;
|
|
96
|
+
if (details?.tagName === "DETAILS" && details.open) {
|
|
97
|
+
out.push("[expanded]");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (role === "heading") {
|
|
101
|
+
const aria = el.getAttribute("aria-level");
|
|
102
|
+
const level = aria ? Number(aria) : Number(tag[1]);
|
|
103
|
+
if (Number.isFinite(level) && level > 0) out.push(`[level=${level}]`);
|
|
104
|
+
}
|
|
105
|
+
const pressed = ariaFlag(el, "aria-pressed");
|
|
106
|
+
if (pressed === "mixed") out.push("[pressed=mixed]");
|
|
107
|
+
else if (pressed === "true") out.push("[pressed]");
|
|
108
|
+
if (el.getAttribute("aria-selected") === "true") out.push("[selected]");
|
|
109
|
+
if (isFilled(el, role)) out.push("[filled]");
|
|
110
|
+
return out.join(" ");
|
|
111
|
+
}
|
|
112
|
+
function roleOf(el) {
|
|
113
|
+
if (isEditable(el)) return "textbox";
|
|
114
|
+
return domAccessibilityApi.getRole(el);
|
|
115
|
+
}
|
|
116
|
+
function nameIsUntrusted(el) {
|
|
117
|
+
const labelledby = el.getAttribute("aria-labelledby");
|
|
118
|
+
if (labelledby) {
|
|
119
|
+
for (const id of labelledby.split(/\s+/)) {
|
|
120
|
+
if (!id) continue;
|
|
121
|
+
const ref = el.ownerDocument.getElementById(id);
|
|
122
|
+
if (!ref) continue;
|
|
123
|
+
if (ref.matches(UNTRUSTED_CONTENT) || ref.querySelector(UNTRUSTED_CONTENT)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
if (el.hasAttribute("aria-label")) return false;
|
|
130
|
+
return el.querySelector(UNTRUSTED_CONTENT) !== null;
|
|
131
|
+
}
|
|
132
|
+
function nameOf(el, styleOf) {
|
|
133
|
+
const name = domAccessibilityApi.computeAccessibleName(el, {
|
|
134
|
+
getComputedStyle: styleOf,
|
|
135
|
+
// jsdom logs a console error if you claim pseudo-element support it doesn't have.
|
|
136
|
+
computedStyleSupportsPseudoElements: false
|
|
137
|
+
});
|
|
138
|
+
if (!name) return "";
|
|
139
|
+
if (nameIsUntrusted(el)) {
|
|
140
|
+
return el.getAttribute("aria-label") || "";
|
|
141
|
+
}
|
|
142
|
+
return name;
|
|
143
|
+
}
|
|
144
|
+
var INTERACTIVE_ROLES, INTERACTIVE_TAGS, NAME_FROM_CONTENT, VALUE_ROLES, OPAQUE_TAGS, UNTRUSTED_CONTENT;
|
|
145
|
+
var init_a11y = __esm({
|
|
146
|
+
"src/dom/a11y.ts"() {
|
|
147
|
+
INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
148
|
+
"button",
|
|
149
|
+
"checkbox",
|
|
150
|
+
"combobox",
|
|
151
|
+
"link",
|
|
152
|
+
"listbox",
|
|
153
|
+
"menuitem",
|
|
154
|
+
"menuitemcheckbox",
|
|
155
|
+
"menuitemradio",
|
|
156
|
+
"option",
|
|
157
|
+
"radio",
|
|
158
|
+
"searchbox",
|
|
159
|
+
"slider",
|
|
160
|
+
"spinbutton",
|
|
161
|
+
"switch",
|
|
162
|
+
"tab",
|
|
163
|
+
"textbox",
|
|
164
|
+
"treeitem"
|
|
165
|
+
]);
|
|
166
|
+
INTERACTIVE_TAGS = /* @__PURE__ */ new Set([
|
|
167
|
+
"BUTTON",
|
|
168
|
+
"INPUT",
|
|
169
|
+
"SELECT",
|
|
170
|
+
"TEXTAREA",
|
|
171
|
+
"SUMMARY"
|
|
172
|
+
]);
|
|
173
|
+
NAME_FROM_CONTENT = /* @__PURE__ */ new Set([
|
|
174
|
+
"button",
|
|
175
|
+
"cell",
|
|
176
|
+
"checkbox",
|
|
177
|
+
"columnheader",
|
|
178
|
+
"gridcell",
|
|
179
|
+
"heading",
|
|
180
|
+
"link",
|
|
181
|
+
"menuitem",
|
|
182
|
+
"menuitemcheckbox",
|
|
183
|
+
"menuitemradio",
|
|
184
|
+
"option",
|
|
185
|
+
"radio",
|
|
186
|
+
"row",
|
|
187
|
+
"rowheader",
|
|
188
|
+
"switch",
|
|
189
|
+
"tab",
|
|
190
|
+
"tooltip",
|
|
191
|
+
"treeitem"
|
|
192
|
+
]);
|
|
193
|
+
VALUE_ROLES = /* @__PURE__ */ new Set(["textbox", "searchbox", "spinbutton"]);
|
|
194
|
+
OPAQUE_TAGS = /* @__PURE__ */ new Set(["INPUT", "TEXTAREA"]);
|
|
195
|
+
UNTRUSTED_CONTENT = 'input,textarea,select,[contenteditable=""],[contenteditable="true"],[aria-valuetext],[data-mf-private]';
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// src/dom/refs.ts
|
|
200
|
+
function beginSnapshot() {
|
|
201
|
+
generation++;
|
|
202
|
+
registry.clear();
|
|
203
|
+
}
|
|
204
|
+
function refFor(el, role, name) {
|
|
205
|
+
const key = `${role} ${name}`;
|
|
206
|
+
let entry = entries.get(el);
|
|
207
|
+
if (!entry || entry.key !== key) {
|
|
208
|
+
entry = { id: `e${++counter}`, key, gen: generation };
|
|
209
|
+
entries.set(el, entry);
|
|
210
|
+
} else {
|
|
211
|
+
entry.gen = generation;
|
|
212
|
+
}
|
|
213
|
+
registry.set(entry.id, el);
|
|
214
|
+
return entry.id;
|
|
215
|
+
}
|
|
216
|
+
function resolveRef(ref) {
|
|
217
|
+
const el = registry.get(ref);
|
|
218
|
+
if (!el) return null;
|
|
219
|
+
if (!el.isConnected) {
|
|
220
|
+
registry.delete(ref);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const entry = entries.get(el);
|
|
224
|
+
if (!entry || entry.id !== ref || entry.gen !== generation) return null;
|
|
225
|
+
return el;
|
|
226
|
+
}
|
|
227
|
+
var counter, generation, entries, registry;
|
|
228
|
+
var init_refs = __esm({
|
|
229
|
+
"src/dom/refs.ts"() {
|
|
230
|
+
counter = 0;
|
|
231
|
+
generation = 0;
|
|
232
|
+
entries = /* @__PURE__ */ new WeakMap();
|
|
233
|
+
registry = /* @__PURE__ */ new Map();
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// src/dom/snapshot.ts
|
|
238
|
+
var snapshot_exports = {};
|
|
239
|
+
__export(snapshot_exports, {
|
|
240
|
+
resolveRef: () => resolveRef,
|
|
241
|
+
snapshot: () => snapshot,
|
|
242
|
+
snapshotRegion: () => snapshotRegion
|
|
243
|
+
});
|
|
244
|
+
function withinBudget(w) {
|
|
245
|
+
if (w.nodes >= MAX_NODES || w.lines >= MAX_LINES) {
|
|
246
|
+
w.truncated = true;
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
function isOurs(el) {
|
|
252
|
+
return el.id === "matterfact-embed" || el.hasAttribute("data-mf-embed");
|
|
253
|
+
}
|
|
254
|
+
function isHidden(el, style, layout) {
|
|
255
|
+
if (el.getAttribute("aria-hidden") === "true") return true;
|
|
256
|
+
if (el.hasAttribute("hidden") || el.hasAttribute("inert")) return true;
|
|
257
|
+
return !isVisible(el, style, layout);
|
|
258
|
+
}
|
|
259
|
+
function childrenOf(el) {
|
|
260
|
+
if (el.shadowRoot) return el.shadowRoot.childNodes;
|
|
261
|
+
if (el.tagName === "SLOT") {
|
|
262
|
+
const assigned = el.assignedNodes({ flatten: true });
|
|
263
|
+
if (assigned.length) return assigned;
|
|
264
|
+
}
|
|
265
|
+
return el.childNodes;
|
|
266
|
+
}
|
|
267
|
+
function visitChildren(el, parent, style, w) {
|
|
268
|
+
const children = childrenOf(el);
|
|
269
|
+
for (let i = 0; i < children.length; i++) {
|
|
270
|
+
const child = children[i];
|
|
271
|
+
if (child) visitNode(child, parent, style, w);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function visitText(node, parent, w) {
|
|
275
|
+
const text = (node.nodeValue ?? "").replace(/\s+/g, " ").trim();
|
|
276
|
+
if (!text) return;
|
|
277
|
+
if (NAME_FROM_CONTENT.has(parent.role)) return;
|
|
278
|
+
if (!withinBudget(w)) return;
|
|
279
|
+
w.lines++;
|
|
280
|
+
parent.children.push(text);
|
|
281
|
+
}
|
|
282
|
+
function optionNode(opt) {
|
|
283
|
+
const disabled = opt.matches(":disabled") || domAccessibilityApi.isDisabled(opt);
|
|
284
|
+
return {
|
|
285
|
+
role: "option",
|
|
286
|
+
name: (opt.label || opt.textContent || "").trim(),
|
|
287
|
+
props: (disabled ? " [disabled]" : "") + (opt.selected ? " [selected]" : ""),
|
|
288
|
+
children: []
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function emitSelectOptions(select, parent, w) {
|
|
292
|
+
const options = select.options;
|
|
293
|
+
const shown = Math.min(options.length, MAX_OPTIONS);
|
|
294
|
+
for (let i = 0; i < shown; i++) {
|
|
295
|
+
if (!withinBudget(w)) return;
|
|
296
|
+
const opt = options[i];
|
|
297
|
+
if (!opt || isPrivate(opt)) continue;
|
|
298
|
+
w.lines++;
|
|
299
|
+
parent.children.push(optionNode(opt));
|
|
300
|
+
}
|
|
301
|
+
let extraSelected = 0;
|
|
302
|
+
const selected = select.selectedOptions;
|
|
303
|
+
for (let i = 0; i < selected.length; i++) {
|
|
304
|
+
const opt = selected[i];
|
|
305
|
+
if (opt && opt.index >= shown && !isPrivate(opt)) {
|
|
306
|
+
if (!withinBudget(w)) return;
|
|
307
|
+
w.lines++;
|
|
308
|
+
parent.children.push(optionNode(opt));
|
|
309
|
+
extraSelected++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const remaining = options.length - shown - extraSelected;
|
|
313
|
+
if (remaining > 0 && withinBudget(w)) {
|
|
314
|
+
w.lines++;
|
|
315
|
+
parent.children.push(`\u2026 (+${remaining} more options)`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function emitDatalistOptions(input, parent, w) {
|
|
319
|
+
const list = input.list;
|
|
320
|
+
if (!list) return;
|
|
321
|
+
const options = list.options;
|
|
322
|
+
const shown = Math.min(options.length, MAX_OPTIONS);
|
|
323
|
+
for (let i = 0; i < shown; i++) {
|
|
324
|
+
if (!withinBudget(w)) return;
|
|
325
|
+
const opt = options[i];
|
|
326
|
+
if (!opt || isPrivate(opt)) continue;
|
|
327
|
+
w.lines++;
|
|
328
|
+
parent.children.push(optionNode(opt));
|
|
329
|
+
}
|
|
330
|
+
const remaining = options.length - shown;
|
|
331
|
+
if (remaining > 0 && withinBudget(w)) {
|
|
332
|
+
w.lines++;
|
|
333
|
+
parent.children.push(`\u2026 (+${remaining} more options)`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function visitElement(el, parent, parentStyle, w) {
|
|
337
|
+
if (!withinBudget(w)) return;
|
|
338
|
+
if (SKIP_TAGS.has(el.tagName) || isOurs(el)) return;
|
|
339
|
+
if (isPrivate(el)) return;
|
|
340
|
+
w.nodes++;
|
|
341
|
+
const style = w.styleOf(el);
|
|
342
|
+
if (isHidden(el, style, w.layout)) return;
|
|
343
|
+
const role = roleOf(el);
|
|
344
|
+
const transparent = role === null || TRANSPARENT.has(role);
|
|
345
|
+
const interactive = isInteractive(el, role, style, parentStyle);
|
|
346
|
+
if (transparent && !interactive) {
|
|
347
|
+
visitChildren(el, parent, style, w);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const emittedRole = role === null || TRANSPARENT.has(role) ? "generic" : role;
|
|
351
|
+
const name = nameOf(el, w.styleOf);
|
|
352
|
+
let ref = "";
|
|
353
|
+
if (interactive && receivesPointerEvents(style)) {
|
|
354
|
+
const r = refFor(el, emittedRole, name);
|
|
355
|
+
ref = ` [ref=${r}]`;
|
|
356
|
+
w.onRef?.(r, el);
|
|
357
|
+
}
|
|
358
|
+
const state = states(el, emittedRole);
|
|
359
|
+
const node = {
|
|
360
|
+
role: emittedRole,
|
|
361
|
+
name,
|
|
362
|
+
props: (state ? ` ${state}` : "") + ref,
|
|
363
|
+
children: []
|
|
364
|
+
};
|
|
365
|
+
w.lines++;
|
|
366
|
+
parent.children.push(node);
|
|
367
|
+
if (el.tagName === "SELECT") {
|
|
368
|
+
emitSelectOptions(el, node, w);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (el.tagName === "INPUT" && el.list) {
|
|
372
|
+
emitDatalistOptions(el, node, w);
|
|
373
|
+
}
|
|
374
|
+
if (isOpaque(el)) return;
|
|
375
|
+
visitChildren(el, node, style, w);
|
|
376
|
+
}
|
|
377
|
+
function visitNode(node, parent, parentStyle, w) {
|
|
378
|
+
if (node.nodeType === 3) {
|
|
379
|
+
visitText(node, parent, w);
|
|
380
|
+
} else if (node.nodeType === 1) {
|
|
381
|
+
visitElement(node, parent, parentStyle, w);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function sanitize(text, max) {
|
|
385
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
386
|
+
const clean = redact(collapsed);
|
|
387
|
+
return clean.length > max ? `${clean.slice(0, max - 1)}\u2026` : clean;
|
|
388
|
+
}
|
|
389
|
+
function quote(text) {
|
|
390
|
+
return `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
391
|
+
}
|
|
392
|
+
function render(children, depth, out) {
|
|
393
|
+
const pad = " ".repeat(depth);
|
|
394
|
+
for (const child of children) {
|
|
395
|
+
if (typeof child === "string") {
|
|
396
|
+
out.push(`${pad}- text: ${quote(sanitize(child, MAX_TEXT))}`);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
const name = child.name ? ` ${quote(sanitize(child.name, MAX_NAME))}` : "";
|
|
400
|
+
const head = `${pad}- ${child.role}${name}${child.props}`;
|
|
401
|
+
if (child.children.length) {
|
|
402
|
+
out.push(`${head}:`);
|
|
403
|
+
render(child.children, depth + 1, out);
|
|
404
|
+
} else {
|
|
405
|
+
out.push(head);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function walk(root, onRef) {
|
|
410
|
+
const styleOf = createStyleCache();
|
|
411
|
+
const w = {
|
|
412
|
+
styleOf,
|
|
413
|
+
layout: hasLayout(),
|
|
414
|
+
nodes: 0,
|
|
415
|
+
lines: 0,
|
|
416
|
+
truncated: false,
|
|
417
|
+
onRef
|
|
418
|
+
};
|
|
419
|
+
if (domAccessibilityApi.isInaccessible(root, { getComputedStyle: styleOf })) {
|
|
420
|
+
return { yaml: "", truncated: false };
|
|
421
|
+
}
|
|
422
|
+
const tree = { role: "", children: [] };
|
|
423
|
+
visitChildren(root, tree, styleOf(root), w);
|
|
424
|
+
const lines = [];
|
|
425
|
+
render(tree.children, 0, lines);
|
|
426
|
+
if (w.truncated && lines.length) {
|
|
427
|
+
lines[lines.length - 1] = '- text: "\u2026 (snapshot truncated \u2014 more of the page is below)"';
|
|
428
|
+
}
|
|
429
|
+
return { yaml: lines.join("\n"), truncated: w.truncated };
|
|
430
|
+
}
|
|
431
|
+
function snapshot() {
|
|
432
|
+
const root = document.body;
|
|
433
|
+
if (!root) return { yaml: "", truncated: false, visibleRefs: [] };
|
|
434
|
+
beginSnapshot();
|
|
435
|
+
const visibleRefs = [];
|
|
436
|
+
const vh = window.innerHeight || 0;
|
|
437
|
+
const vw = window.innerWidth || 0;
|
|
438
|
+
const result = walk(root, (ref, el) => {
|
|
439
|
+
const r = el.getBoundingClientRect();
|
|
440
|
+
if (r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw) {
|
|
441
|
+
visibleRefs.push(ref);
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
return { ...result, visibleRefs };
|
|
445
|
+
}
|
|
446
|
+
function snapshotRegion(root) {
|
|
447
|
+
if (!root.isConnected) return { yaml: "", truncated: false };
|
|
448
|
+
return walk(root);
|
|
449
|
+
}
|
|
450
|
+
var MAX_NODES, MAX_LINES, MAX_NAME, MAX_TEXT, MAX_OPTIONS, TRANSPARENT, SKIP_TAGS;
|
|
451
|
+
var init_snapshot = __esm({
|
|
452
|
+
"src/dom/snapshot.ts"() {
|
|
453
|
+
init_context();
|
|
454
|
+
init_a11y();
|
|
455
|
+
init_refs();
|
|
456
|
+
init_refs();
|
|
457
|
+
MAX_NODES = 4e3;
|
|
458
|
+
MAX_LINES = 800;
|
|
459
|
+
MAX_NAME = 120;
|
|
460
|
+
MAX_TEXT = 120;
|
|
461
|
+
MAX_OPTIONS = 25;
|
|
462
|
+
TRANSPARENT = /* @__PURE__ */ new Set(["generic", "presentation", "none"]);
|
|
463
|
+
SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
464
|
+
"SCRIPT",
|
|
465
|
+
"STYLE",
|
|
466
|
+
"NOSCRIPT",
|
|
467
|
+
"TEMPLATE",
|
|
468
|
+
"SVG",
|
|
469
|
+
"HEAD",
|
|
470
|
+
"LINK",
|
|
471
|
+
"META",
|
|
472
|
+
"BASE"
|
|
473
|
+
]);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
// src/context.ts
|
|
478
|
+
var context_exports = {};
|
|
479
|
+
__export(context_exports, {
|
|
480
|
+
beginAuth: () => beginAuth,
|
|
481
|
+
callTool: () => callTool,
|
|
482
|
+
isPrivate: () => isPrivate,
|
|
483
|
+
readPageContext: () => readPageContext,
|
|
484
|
+
redact: () => redact,
|
|
485
|
+
sendRegion: () => sendRegion,
|
|
486
|
+
sendSnapshot: () => sendSnapshot,
|
|
487
|
+
start: () => start
|
|
488
|
+
});
|
|
489
|
+
function redact(text) {
|
|
490
|
+
let out = text;
|
|
491
|
+
for (const re of PII) out = out.replace(re, "[redacted]");
|
|
492
|
+
return out;
|
|
493
|
+
}
|
|
494
|
+
function isPrivate(el) {
|
|
495
|
+
if (el.closest("[data-mf-private]")) return true;
|
|
496
|
+
const tag = el.tagName;
|
|
497
|
+
if (tag === "INPUT") {
|
|
498
|
+
const t = el.type;
|
|
499
|
+
if (t === "password" || t === "hidden") return true;
|
|
500
|
+
}
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
function embeddedMatterfactEntities() {
|
|
504
|
+
if (!widgetOrigin) return [];
|
|
505
|
+
const out = [];
|
|
506
|
+
for (const frame of Array.from(document.querySelectorAll("iframe"))) {
|
|
507
|
+
let u;
|
|
508
|
+
try {
|
|
509
|
+
u = new URL(frame.getAttribute("src") || "", location.href);
|
|
510
|
+
} catch {
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (u.origin !== widgetOrigin) continue;
|
|
514
|
+
const m = u.pathname.match(/^\/artifacts\/(.+?)\/?$/);
|
|
515
|
+
if (!m) continue;
|
|
516
|
+
const id = decodeURIComponent(m[1]);
|
|
517
|
+
if (!id || out.some((e) => e.id === id)) continue;
|
|
518
|
+
out.push({
|
|
519
|
+
kind: "artifact",
|
|
520
|
+
id,
|
|
521
|
+
label: redact(frame.getAttribute("title") || "") || id
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return out;
|
|
525
|
+
}
|
|
526
|
+
function opaqueFrameCount() {
|
|
527
|
+
return Array.from(document.querySelectorAll("iframe")).filter((f) => {
|
|
528
|
+
try {
|
|
529
|
+
const u = new URL(f.getAttribute("src") || "", location.href);
|
|
530
|
+
return u.origin !== location.origin && u.origin !== widgetOrigin;
|
|
531
|
+
} catch {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}).length;
|
|
535
|
+
}
|
|
536
|
+
function readPageContext() {
|
|
537
|
+
const declared = window.matterfact?.context ?? {};
|
|
538
|
+
const found = embeddedMatterfactEntities();
|
|
539
|
+
const opaque = opaqueFrameCount();
|
|
540
|
+
return {
|
|
541
|
+
// Path only. A query string is where session ids, tokens and email addresses
|
|
542
|
+
// live; it is not ours to take.
|
|
543
|
+
url: location.origin + location.pathname,
|
|
544
|
+
path: location.pathname,
|
|
545
|
+
title: redact(document.title),
|
|
546
|
+
locale: document.documentElement.lang || void 0,
|
|
547
|
+
...declared,
|
|
548
|
+
// After the spread: what we FOUND is additive to what the host DECLARED, never a
|
|
549
|
+
// replacement. A host that declares its own entities still gets the artifacts we
|
|
550
|
+
// spotted, and vice versa.
|
|
551
|
+
entities: [...declared.entities ?? [], ...found],
|
|
552
|
+
data: {
|
|
553
|
+
...declared.data ?? {},
|
|
554
|
+
...opaque ? { opaque_frames: opaque } : {}
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function publishContext() {
|
|
559
|
+
send?.({ type: "host.context", context: readPageContext() });
|
|
560
|
+
}
|
|
561
|
+
function pushActivity(e) {
|
|
562
|
+
activity.push({ ...e, seq: ++activitySeq, ts: Date.now() });
|
|
563
|
+
if (activity.length > MAX_ACTIVITY) activity.shift();
|
|
564
|
+
send?.({ type: "host.activity", events: [activity[activity.length - 1]] });
|
|
565
|
+
}
|
|
566
|
+
function controlLabel(el) {
|
|
567
|
+
const aria = el.getAttribute("aria-label");
|
|
568
|
+
if (aria) return aria;
|
|
569
|
+
const id = el.id;
|
|
570
|
+
if (id) {
|
|
571
|
+
const forLabel = document.querySelector(`label[for="${CSS.escape(id)}"]`);
|
|
572
|
+
const t = forLabel?.textContent?.trim();
|
|
573
|
+
if (t) return t;
|
|
574
|
+
}
|
|
575
|
+
const wrapping = el.closest("label")?.textContent?.trim();
|
|
576
|
+
if (wrapping) return wrapping;
|
|
577
|
+
const placeholder = el.getAttribute("placeholder");
|
|
578
|
+
if (placeholder) return placeholder;
|
|
579
|
+
return "";
|
|
580
|
+
}
|
|
581
|
+
function describe(el) {
|
|
582
|
+
const role = el.getAttribute("role") || el.tagName.toLowerCase();
|
|
583
|
+
const label = el.getAttribute("aria-label") || (FORM_CONTROLS.has(el.tagName) ? controlLabel(el) : el.innerText?.trim().slice(0, 60)) || el.getAttribute("title") || "";
|
|
584
|
+
return label ? `${role} "${redact(label)}"` : role;
|
|
585
|
+
}
|
|
586
|
+
function onClick(ev) {
|
|
587
|
+
const target = ev.target;
|
|
588
|
+
if (!target || !(target instanceof Element)) return;
|
|
589
|
+
const label = target.closest("label");
|
|
590
|
+
const labelled = label ? label.getAttribute("for") && document.getElementById(label.getAttribute("for")) || label.querySelector("input,select,textarea") : null;
|
|
591
|
+
const actionable = labelled ?? target.closest(ACTIONABLE);
|
|
592
|
+
if (!actionable || isPrivate(actionable)) return;
|
|
593
|
+
pushActivity({ type: "click", summary: `clicked ${describe(actionable)}` });
|
|
594
|
+
}
|
|
595
|
+
function onSubmit(ev) {
|
|
596
|
+
const el = ev.target;
|
|
597
|
+
if (!el || isPrivate(el)) return;
|
|
598
|
+
pushActivity({ type: "submit", summary: `submitted ${describe(el)}` });
|
|
599
|
+
}
|
|
600
|
+
function onChange(ev) {
|
|
601
|
+
const el = ev.target;
|
|
602
|
+
if (!el || !(el instanceof Element) || isPrivate(el)) return;
|
|
603
|
+
const tag = el.tagName;
|
|
604
|
+
const type = el.type;
|
|
605
|
+
if (tag === "INPUT" && (type === "password" || type === "hidden")) return;
|
|
606
|
+
let summary = `changed ${describe(el)}`;
|
|
607
|
+
if (tag === "SELECT") {
|
|
608
|
+
const opt = el.selectedOptions[0]?.text;
|
|
609
|
+
if (opt) summary = `${describe(el)} \u2192 "${redact(opt)}"`;
|
|
610
|
+
} else if (type === "checkbox" || type === "radio") {
|
|
611
|
+
summary = `${el.checked ? "checked" : "unchecked"} ${describe(el)}`;
|
|
612
|
+
}
|
|
613
|
+
pushActivity({ type: "input", summary });
|
|
614
|
+
}
|
|
615
|
+
function watchNavigation() {
|
|
616
|
+
const fire = () => {
|
|
617
|
+
const url = location.pathname;
|
|
618
|
+
if (url === lastUrl) return;
|
|
619
|
+
lastUrl = url;
|
|
620
|
+
pushActivity({ type: "nav", summary: `navigated to ${url}` });
|
|
621
|
+
publishContext();
|
|
622
|
+
};
|
|
623
|
+
for (const name of ["pushState", "replaceState"]) {
|
|
624
|
+
const orig = history[name];
|
|
625
|
+
history[name] = function(...args) {
|
|
626
|
+
const r = orig.apply(this, args);
|
|
627
|
+
fire();
|
|
628
|
+
return r;
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
window.addEventListener("popstate", fire);
|
|
632
|
+
}
|
|
633
|
+
async function readFocus() {
|
|
634
|
+
const focus = {};
|
|
635
|
+
const sel = window.getSelection?.();
|
|
636
|
+
if (sel && !sel.isCollapsed) {
|
|
637
|
+
const anchor = sel.anchorNode?.parentElement ?? null;
|
|
638
|
+
if (!anchor || !anchor.closest("[data-mf-private]")) {
|
|
639
|
+
const text = sel.toString().trim().slice(0, 500);
|
|
640
|
+
if (text) focus.selection = redact(text);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const active = document.activeElement;
|
|
644
|
+
if (active && active !== document.body && !isPrivate(active) && !active.closest("[data-mf-private]")) {
|
|
645
|
+
const { snapshot: snapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
646
|
+
focus.focused = {
|
|
647
|
+
label: redact(describeControl(active)),
|
|
648
|
+
role: active.getAttribute("role") || active.tagName.toLowerCase()
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
const doc = document.documentElement;
|
|
652
|
+
const scrollable = doc.scrollHeight - doc.clientHeight;
|
|
653
|
+
focus.scroll = scrollable > 0 ? Math.round(doc.scrollTop / scrollable * 100) / 100 : 0;
|
|
654
|
+
return focus;
|
|
655
|
+
}
|
|
656
|
+
function describeControl(el) {
|
|
657
|
+
return describe(el);
|
|
658
|
+
}
|
|
659
|
+
function scheduleFocus() {
|
|
660
|
+
if (focusTimer) return;
|
|
661
|
+
focusTimer = setTimeout(async () => {
|
|
662
|
+
focusTimer = null;
|
|
663
|
+
lastFocus = await readFocus();
|
|
664
|
+
send?.({ type: "host.focus", focus: lastFocus });
|
|
665
|
+
}, 250);
|
|
666
|
+
}
|
|
667
|
+
async function sendSnapshot(emit) {
|
|
668
|
+
const { snapshot: snapshot2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
669
|
+
const { yaml, truncated, visibleRefs } = snapshot2();
|
|
670
|
+
emit({
|
|
671
|
+
type: "host.snapshot",
|
|
672
|
+
snapshot: { yaml: redact(yaml), seq: ++snapshotSeq, truncated }
|
|
673
|
+
});
|
|
674
|
+
lastFocus = { ...lastFocus, visibleRefs };
|
|
675
|
+
emit({ type: "host.focus", focus: lastFocus });
|
|
676
|
+
}
|
|
677
|
+
async function sendRegion(ref, emit) {
|
|
678
|
+
const { snapshotRegion: snapshotRegion2, resolveRef: resolveRef2 } = await Promise.resolve().then(() => (init_snapshot(), snapshot_exports));
|
|
679
|
+
const el = resolveRef2(ref);
|
|
680
|
+
if (!el) {
|
|
681
|
+
emit({
|
|
682
|
+
type: "host.region",
|
|
683
|
+
ref,
|
|
684
|
+
yaml: "(this element is no longer on the page)"
|
|
685
|
+
});
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const { yaml } = snapshotRegion2(el);
|
|
689
|
+
emit({ type: "host.region", ref, yaml: redact(yaml) });
|
|
690
|
+
}
|
|
691
|
+
async function callTool(call, emit) {
|
|
692
|
+
emit({
|
|
693
|
+
type: "host.toolResult",
|
|
694
|
+
callId: call.callId,
|
|
695
|
+
ok: false,
|
|
696
|
+
error: "Host tools are read-only in this version; no action was taken."
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
function beginAuth(config, emit) {
|
|
700
|
+
const w = window.open(
|
|
701
|
+
`${config.origin}/embed/authorize?k=${encodeURIComponent(
|
|
702
|
+
config.publishableKey
|
|
703
|
+
)}&o=${encodeURIComponent(location.origin)}`,
|
|
704
|
+
"mf_auth",
|
|
705
|
+
"width=460,height=640"
|
|
706
|
+
);
|
|
707
|
+
if (!w) return;
|
|
708
|
+
const onMsg = (event) => {
|
|
709
|
+
if (event.origin !== config.origin) return;
|
|
710
|
+
const d = event.data;
|
|
711
|
+
if (d?.type !== "mf:embed:auth") return;
|
|
712
|
+
window.removeEventListener("message", onMsg);
|
|
713
|
+
emit({ type: "host.auth", token: d.token, expiresAt: d.expiresAt });
|
|
714
|
+
};
|
|
715
|
+
window.addEventListener("message", onMsg);
|
|
716
|
+
}
|
|
717
|
+
function start(emit, origin) {
|
|
718
|
+
send = emit;
|
|
719
|
+
widgetOrigin = origin || "";
|
|
720
|
+
lastUrl = location.pathname;
|
|
721
|
+
publishContext();
|
|
722
|
+
watchNavigation();
|
|
723
|
+
document.addEventListener("click", onClick, { capture: true, passive: true });
|
|
724
|
+
document.addEventListener("submit", onSubmit, {
|
|
725
|
+
capture: true,
|
|
726
|
+
passive: true
|
|
727
|
+
});
|
|
728
|
+
document.addEventListener("change", onChange, {
|
|
729
|
+
capture: true,
|
|
730
|
+
passive: true
|
|
731
|
+
});
|
|
732
|
+
document.addEventListener("selectionchange", scheduleFocus, {
|
|
733
|
+
passive: true
|
|
734
|
+
});
|
|
735
|
+
document.addEventListener("focusin", scheduleFocus, {
|
|
736
|
+
capture: true,
|
|
737
|
+
passive: true
|
|
738
|
+
});
|
|
739
|
+
window.addEventListener("scroll", scheduleFocus, {
|
|
740
|
+
capture: true,
|
|
741
|
+
passive: true
|
|
742
|
+
});
|
|
743
|
+
const theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
744
|
+
emit({ type: "host.theme", mode: theme });
|
|
745
|
+
}
|
|
746
|
+
var widgetOrigin, MAX_ACTIVITY, send, activitySeq, activity, lastUrl, PII, FORM_CONTROLS, ACTIONABLE, lastFocus, focusTimer, snapshotSeq;
|
|
747
|
+
var init_context = __esm({
|
|
748
|
+
"src/context.ts"() {
|
|
749
|
+
widgetOrigin = "";
|
|
750
|
+
MAX_ACTIVITY = 40;
|
|
751
|
+
send = null;
|
|
752
|
+
activitySeq = 0;
|
|
753
|
+
activity = [];
|
|
754
|
+
lastUrl = "";
|
|
755
|
+
PII = [
|
|
756
|
+
/\b[\w.+-]+@[\w-]+\.[\w.]{2,}\b/g,
|
|
757
|
+
// email
|
|
758
|
+
/\b(?:\d[ -]?){13,19}\b/g,
|
|
759
|
+
// card-ish
|
|
760
|
+
/\b\d{3}[- ]?\d{2}[- ]?\d{4}\b/g,
|
|
761
|
+
// ssn — dashed, spaced, OR bare 9 digits
|
|
762
|
+
/\b\d{9,}\b/g,
|
|
763
|
+
// long bare digit runs: account / MRN / routing numbers
|
|
764
|
+
/(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}\b/g,
|
|
765
|
+
// us phone
|
|
766
|
+
/\b(?:sk|pk|rk|ak)_(?:live|test)_[A-Za-z0-9]{8,}\b/g,
|
|
767
|
+
// stripe-style api keys
|
|
768
|
+
/\beyJ[\w-]+\.[\w-]+\.[\w-]+\b/g
|
|
769
|
+
// jwt
|
|
770
|
+
];
|
|
771
|
+
FORM_CONTROLS = /* @__PURE__ */ new Set(["INPUT", "SELECT", "TEXTAREA"]);
|
|
772
|
+
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]';
|
|
773
|
+
lastFocus = {};
|
|
774
|
+
focusTimer = null;
|
|
775
|
+
snapshotSeq = 0;
|
|
776
|
+
}
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// src/protocol.ts
|
|
780
|
+
var PROTOCOL_VERSION = 1;
|
|
781
|
+
var CHANNEL = "mf-embed";
|
|
782
|
+
function envelope(payload, id) {
|
|
783
|
+
return {
|
|
784
|
+
channel: CHANNEL,
|
|
785
|
+
protocol: PROTOCOL_VERSION,
|
|
786
|
+
...id ? { id } : {},
|
|
787
|
+
payload
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function isEnvelope(data) {
|
|
791
|
+
return typeof data === "object" && data !== null && data.channel === CHANNEL;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/loader.ts
|
|
795
|
+
var DEFAULT_ORIGIN = "https://app.matterfact.com";
|
|
796
|
+
function devRequested() {
|
|
797
|
+
try {
|
|
798
|
+
return new URLSearchParams(location.search).get("mfdev") === "1";
|
|
799
|
+
} catch {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function readConfig() {
|
|
804
|
+
const el = document.currentScript ?? document.querySelector('script[data-key][src*="embed"]');
|
|
805
|
+
const publishableKey = el?.dataset.key;
|
|
806
|
+
if (!publishableKey) {
|
|
807
|
+
console.error("[matterfact] missing data-key on the embed script tag");
|
|
808
|
+
return null;
|
|
809
|
+
}
|
|
810
|
+
const sel = el?.dataset.container;
|
|
811
|
+
const container = sel ? document.querySelector(sel) : null;
|
|
812
|
+
if (sel && !container) {
|
|
813
|
+
console.error(
|
|
814
|
+
`[matterfact] data-container="${sel}" matched nothing; falling back to the corner`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
publishableKey,
|
|
819
|
+
origin: el?.dataset.origin || DEFAULT_ORIGIN,
|
|
820
|
+
theme: el?.dataset.theme || "auto",
|
|
821
|
+
surface: el?.dataset.surface || "",
|
|
822
|
+
container
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
var POS_KEY = "mf.embed.pos";
|
|
826
|
+
var EmbedHost = class {
|
|
827
|
+
constructor(config) {
|
|
828
|
+
this.config = config;
|
|
829
|
+
this.iframe = null;
|
|
830
|
+
this.shadow = null;
|
|
831
|
+
/** Buffered until the widget says it's listening — postMessage before load is dropped silently. */
|
|
832
|
+
this.queue = [];
|
|
833
|
+
this.ready = false;
|
|
834
|
+
this.open = false;
|
|
835
|
+
this.expanded = false;
|
|
836
|
+
/** Null until the user drags; then it pins the corner offset and survives reloads. */
|
|
837
|
+
this.pos = null;
|
|
838
|
+
/** Where the launcher sat when the current drag began; deltas are applied to this. */
|
|
839
|
+
this.dragBase = null;
|
|
840
|
+
/** Loaded on first open. Holds everything that touches the customer's DOM. */
|
|
841
|
+
this.context = null;
|
|
842
|
+
/** The host element; kept so `destroy()` can remove it (React lifecycle). */
|
|
843
|
+
this.hostEl = null;
|
|
844
|
+
/**
|
|
845
|
+
* Every message is checked twice, on every single message — not once at setup.
|
|
846
|
+
*
|
|
847
|
+
* `channel` is a namespace, not a boundary. The origin and source checks are the
|
|
848
|
+
* boundary: any frame on the page can postMessage us, and a page with an ad iframe
|
|
849
|
+
* on it has plenty of frames.
|
|
850
|
+
*/
|
|
851
|
+
this.onMessage = (event) => {
|
|
852
|
+
if (event.origin !== this.config.origin) return;
|
|
853
|
+
if (event.source !== this.iframe?.contentWindow) return;
|
|
854
|
+
if (!isEnvelope(event.data)) return;
|
|
855
|
+
const msg = event.data.payload;
|
|
856
|
+
switch (msg.type) {
|
|
857
|
+
case "widget.ready":
|
|
858
|
+
this.ready = true;
|
|
859
|
+
this.send({
|
|
860
|
+
type: "host.ready",
|
|
861
|
+
protocol: PROTOCOL_VERSION,
|
|
862
|
+
origin: location.origin
|
|
863
|
+
});
|
|
864
|
+
this.flush();
|
|
865
|
+
if (this.inline) void this.loadContext();
|
|
866
|
+
break;
|
|
867
|
+
case "widget.setOpen":
|
|
868
|
+
if (this.inline) break;
|
|
869
|
+
this.open = msg.open;
|
|
870
|
+
this.iframe?.setAttribute("data-open", String(msg.open));
|
|
871
|
+
if (!msg.open) {
|
|
872
|
+
this.expanded = false;
|
|
873
|
+
this.iframe?.setAttribute("data-expanded", "false");
|
|
874
|
+
}
|
|
875
|
+
if (msg.open) void this.loadContext();
|
|
876
|
+
break;
|
|
877
|
+
case "widget.setExpanded":
|
|
878
|
+
if (this.inline) break;
|
|
879
|
+
this.expanded = msg.expanded;
|
|
880
|
+
this.iframe?.setAttribute("data-expanded", String(msg.expanded));
|
|
881
|
+
if (this.iframe) this.iframe.style.height = "";
|
|
882
|
+
break;
|
|
883
|
+
case "widget.resize":
|
|
884
|
+
if (this.inline) break;
|
|
885
|
+
if (this.iframe && this.open && !this.expanded) {
|
|
886
|
+
this.iframe.style.height = `${Math.min(msg.height, window.innerHeight - 40)}px`;
|
|
887
|
+
}
|
|
888
|
+
break;
|
|
889
|
+
case "widget.requestSnapshot":
|
|
890
|
+
void this.loadContext().then((m) => m.sendSnapshot(this.send));
|
|
891
|
+
break;
|
|
892
|
+
case "widget.readRegion":
|
|
893
|
+
void this.loadContext().then((m) => m.sendRegion(msg.ref, this.send));
|
|
894
|
+
break;
|
|
895
|
+
case "widget.callTool":
|
|
896
|
+
void this.loadContext().then((m) => m.callTool(msg.call, this.send));
|
|
897
|
+
break;
|
|
898
|
+
// Everything below is LAUNCHER chrome: there is no launcher inline (the widget
|
|
899
|
+
// doesn't draw one), and moving or hiding the host's own panel from inside it
|
|
900
|
+
// would be us redecorating their app. An older cached loader could still be told
|
|
901
|
+
// any of these by a newer widget, so they're guarded rather than assumed absent.
|
|
902
|
+
case "widget.dragStart":
|
|
903
|
+
if (this.inline) break;
|
|
904
|
+
this.dragBase = this.cornerOffset();
|
|
905
|
+
break;
|
|
906
|
+
case "widget.dragMove":
|
|
907
|
+
if (this.inline) break;
|
|
908
|
+
this.dragTo(msg.dx, msg.dy);
|
|
909
|
+
break;
|
|
910
|
+
case "widget.dragEnd":
|
|
911
|
+
if (this.inline) break;
|
|
912
|
+
this.dragBase = null;
|
|
913
|
+
this.writePos(this.pos);
|
|
914
|
+
break;
|
|
915
|
+
case "widget.setMenu":
|
|
916
|
+
if (this.inline) break;
|
|
917
|
+
if (this.iframe && !this.open) {
|
|
918
|
+
this.iframe.style.height = msg.open ? `${64 + msg.height}px` : "";
|
|
919
|
+
}
|
|
920
|
+
break;
|
|
921
|
+
case "widget.resetPos":
|
|
922
|
+
if (this.inline) break;
|
|
923
|
+
this.resetPos();
|
|
924
|
+
break;
|
|
925
|
+
case "widget.hide":
|
|
926
|
+
if (this.inline) break;
|
|
927
|
+
if (this.hostEl) this.hostEl.style.display = "none";
|
|
928
|
+
break;
|
|
929
|
+
case "widget.needsAuth":
|
|
930
|
+
void this.provideAuth();
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
this.send = (msg) => {
|
|
935
|
+
if (!this.ready) {
|
|
936
|
+
this.queue.push(msg);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
this.iframe?.contentWindow?.postMessage(envelope(msg), this.config.origin);
|
|
940
|
+
};
|
|
941
|
+
this.inline = !!config.container;
|
|
942
|
+
}
|
|
943
|
+
mount() {
|
|
944
|
+
const host = document.createElement("div");
|
|
945
|
+
this.hostEl = host;
|
|
946
|
+
host.id = "matterfact-embed";
|
|
947
|
+
if (this.inline) {
|
|
948
|
+
host.style.cssText = [
|
|
949
|
+
"all: initial",
|
|
950
|
+
"position: relative",
|
|
951
|
+
"display: block",
|
|
952
|
+
"width: 100%",
|
|
953
|
+
"height: 100%",
|
|
954
|
+
"contain: layout style"
|
|
955
|
+
].join(";");
|
|
956
|
+
this.config.container.appendChild(host);
|
|
957
|
+
} else {
|
|
958
|
+
this.pos = this.readPos();
|
|
959
|
+
host.style.cssText = [
|
|
960
|
+
"all: initial",
|
|
961
|
+
"position: fixed",
|
|
962
|
+
`right: ${this.pos ? this.pos.right : 20}px`,
|
|
963
|
+
`bottom: ${this.pos ? this.pos.bottom : 20}px`,
|
|
964
|
+
// Below the max so a host that genuinely needs to cover us (a modal, a cookie
|
|
965
|
+
// banner they are legally obliged to show) still can.
|
|
966
|
+
"z-index: 2147483000",
|
|
967
|
+
"contain: layout style"
|
|
968
|
+
].join(";");
|
|
969
|
+
document.body.appendChild(host);
|
|
970
|
+
}
|
|
971
|
+
this.shadow = host.attachShadow({ mode: "closed" });
|
|
972
|
+
const style = document.createElement("style");
|
|
973
|
+
style.textContent = ":host{all:initial}iframe{border:0;display:block;color-scheme:light dark;}" + (this.inline ? "iframe{width:100%;height:100%}" : 'iframe{border-radius:12px;box-shadow:0 8px 40px rgba(0,0,0,.16);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)}');
|
|
974
|
+
this.shadow.appendChild(style);
|
|
975
|
+
const iframe = document.createElement("iframe");
|
|
976
|
+
iframe.title = "matterfact assistant";
|
|
977
|
+
iframe.setAttribute(
|
|
978
|
+
"sandbox",
|
|
979
|
+
"allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
980
|
+
);
|
|
981
|
+
iframe.setAttribute("allow", "microphone");
|
|
982
|
+
iframe.src = `${this.config.origin}/embed/chat?k=${encodeURIComponent(
|
|
983
|
+
this.config.publishableKey
|
|
984
|
+
)}&o=${encodeURIComponent(location.origin)}` + (this.config.surface ? `&s=${encodeURIComponent(this.config.surface)}` : "") + (devRequested() ? "&dev=1" : "") + (this.inline ? "&inline=1" : "");
|
|
985
|
+
this.iframe = iframe;
|
|
986
|
+
this.shadow.appendChild(iframe);
|
|
987
|
+
window.addEventListener("message", this.onMessage);
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Own a drag for its lifetime.
|
|
991
|
+
*
|
|
992
|
+
* The widget reports the press and then goes quiet: a cross-origin iframe only gets
|
|
993
|
+
* pointer events while the pointer is over it, and a drag leaves that box immediately.
|
|
994
|
+
* So we lay a transparent layer over the whole viewport and track the gesture in the
|
|
995
|
+
* host document, where it can't be lost. The layer also stops the pointer landing on
|
|
996
|
+
* the customer's own UI mid-drag (text selection, hover states, stray clicks).
|
|
997
|
+
*
|
|
998
|
+
* We are the dumb half on purpose — see `widget.dragMove` in the protocol for why the
|
|
999
|
+
* widget has to own the gesture. All we do is take a delta and place the element.
|
|
1000
|
+
*/
|
|
1001
|
+
cornerOffset() {
|
|
1002
|
+
const rect = this.hostEl.getBoundingClientRect();
|
|
1003
|
+
return {
|
|
1004
|
+
right: window.innerWidth - rect.right,
|
|
1005
|
+
bottom: window.innerHeight - rect.bottom
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
dragTo(dx, dy) {
|
|
1009
|
+
if (!this.dragBase || !this.hostEl) return;
|
|
1010
|
+
const rect = this.hostEl.getBoundingClientRect();
|
|
1011
|
+
const maxRight = Math.max(0, window.innerWidth - rect.width);
|
|
1012
|
+
const maxBottom = Math.max(0, window.innerHeight - rect.height);
|
|
1013
|
+
const right = Math.min(Math.max(0, this.dragBase.right - dx), maxRight);
|
|
1014
|
+
const bottom = Math.min(Math.max(0, this.dragBase.bottom - dy), maxBottom);
|
|
1015
|
+
this.pos = { right, bottom };
|
|
1016
|
+
this.hostEl.style.right = `${right}px`;
|
|
1017
|
+
this.hostEl.style.bottom = `${bottom}px`;
|
|
1018
|
+
}
|
|
1019
|
+
/** Reset to the default corner (a menu action — the drag is otherwise sticky). */
|
|
1020
|
+
resetPos() {
|
|
1021
|
+
this.pos = null;
|
|
1022
|
+
this.writePos(null);
|
|
1023
|
+
if (this.hostEl) {
|
|
1024
|
+
this.hostEl.style.right = "20px";
|
|
1025
|
+
this.hostEl.style.bottom = "20px";
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
readPos() {
|
|
1029
|
+
try {
|
|
1030
|
+
const raw = localStorage.getItem(POS_KEY);
|
|
1031
|
+
if (!raw) return null;
|
|
1032
|
+
const p = JSON.parse(raw);
|
|
1033
|
+
return typeof p?.right === "number" && typeof p?.bottom === "number" ? { right: p.right, bottom: p.bottom } : null;
|
|
1034
|
+
} catch {
|
|
1035
|
+
return null;
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
writePos(pos) {
|
|
1039
|
+
try {
|
|
1040
|
+
if (pos) localStorage.setItem(POS_KEY, JSON.stringify(pos));
|
|
1041
|
+
else localStorage.removeItem(POS_KEY);
|
|
1042
|
+
} catch {
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Answer `widget.needsAuth`. If a trusted first-party host has an auth-token provider,
|
|
1047
|
+
* call it and hand the token straight to the widget via `host.auth` — no popup. The
|
|
1048
|
+
* provider comes from EITHER the programmatic config (the React `<MatterfactAgent
|
|
1049
|
+
* getAuthToken>` prop) OR a global the host page sets for the `<script>` loader:
|
|
1050
|
+
*
|
|
1051
|
+
* window.matterfact = { getEmbedAuthToken: () => getIdToken(user) };
|
|
1052
|
+
*
|
|
1053
|
+
* Read fresh at call time (not at readConfig), so a global set after the loader booted
|
|
1054
|
+
* — e.g. once the host's auth is ready — is still picked up. Otherwise fall back to the
|
|
1055
|
+
* hosted-login popup. `expiresAt: 0`: the widget doesn't cache it, it exchanges the
|
|
1056
|
+
* token for a rotating embed session anyway.
|
|
1057
|
+
*/
|
|
1058
|
+
async provideAuth() {
|
|
1059
|
+
const provider = this.config.authTokenProvider ?? globalThis.matterfact?.getEmbedAuthToken;
|
|
1060
|
+
if (provider) {
|
|
1061
|
+
try {
|
|
1062
|
+
const token = await provider();
|
|
1063
|
+
if (token) {
|
|
1064
|
+
this.send({ type: "host.auth", token, expiresAt: 0 });
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
} catch {
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
void this.loadContext().then((m) => m.beginAuth(this.config, this.send));
|
|
1071
|
+
}
|
|
1072
|
+
loadContext() {
|
|
1073
|
+
this.context ?? (this.context = Promise.resolve().then(() => (init_context(), context_exports)).then((m) => {
|
|
1074
|
+
m.start(this.send, this.config.origin);
|
|
1075
|
+
return m;
|
|
1076
|
+
}));
|
|
1077
|
+
return this.context;
|
|
1078
|
+
}
|
|
1079
|
+
flush() {
|
|
1080
|
+
const pending = this.queue;
|
|
1081
|
+
this.queue = [];
|
|
1082
|
+
for (const m of pending) this.send(m);
|
|
1083
|
+
}
|
|
1084
|
+
/** Tear down: stop listening and remove the host element. For the React wrapper's
|
|
1085
|
+
* unmount — the vanilla `<script>` loader lives for the page's lifetime and never
|
|
1086
|
+
* calls this. */
|
|
1087
|
+
destroy() {
|
|
1088
|
+
window.removeEventListener("message", this.onMessage);
|
|
1089
|
+
this.hostEl?.remove();
|
|
1090
|
+
this.hostEl = null;
|
|
1091
|
+
this.iframe = null;
|
|
1092
|
+
this.shadow = null;
|
|
1093
|
+
this.ready = false;
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
function mount(config) {
|
|
1097
|
+
const host = new EmbedHost(config);
|
|
1098
|
+
host.mount();
|
|
1099
|
+
return host;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
exports.CHANNEL = CHANNEL;
|
|
1103
|
+
exports.EmbedHost = EmbedHost;
|
|
1104
|
+
exports.PROTOCOL_VERSION = PROTOCOL_VERSION;
|
|
1105
|
+
exports.envelope = envelope;
|
|
1106
|
+
exports.isEnvelope = isEnvelope;
|
|
1107
|
+
exports.mount = mount;
|
|
1108
|
+
exports.readConfig = readConfig;
|
|
1109
|
+
//# sourceMappingURL=index.cjs.map
|
|
1110
|
+
//# sourceMappingURL=index.cjs.map
|