@qaiddev/thumbs-embed 1.7.0 → 1.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.
- package/README.md +77 -33
- package/dist/bootstrap.d.ts +4 -2
- package/dist/chunks/{annotate-CMq95aXG.js → annotate-D-o8xCV9.js} +23 -17
- package/dist/chunks/annotate-D-o8xCV9.js.map +1 -0
- package/dist/chunks/dom-DvdtcUS8.js +17 -0
- package/dist/chunks/dom-DvdtcUS8.js.map +1 -0
- package/dist/chunks/{loader-CYM1rxDH.js → loader-XbJ2b8KE.js} +229 -167
- package/dist/chunks/loader-XbJ2b8KE.js.map +1 -0
- package/dist/chunks/modal-XxYobTKE.js +213 -0
- package/dist/chunks/modal-XxYobTKE.js.map +1 -0
- package/dist/chunks/{recording-BVFd_ryf.js → recording-Dx8AX-BC.js} +103 -98
- package/dist/chunks/recording-Dx8AX-BC.js.map +1 -0
- package/dist/chunks/{screenshot-BkOCLuS2.js → screenshot-DYOKg2bS.js} +2 -2
- package/dist/chunks/{screenshot-BkOCLuS2.js.map → screenshot-DYOKg2bS.js.map} +1 -1
- package/dist/chunks/{targeting-1pK6nJI9.js → targeting-Dz6z1BYl.js} +2 -2
- package/dist/chunks/{targeting-1pK6nJI9.js.map → targeting-Dz6z1BYl.js.map} +1 -1
- package/dist/chunks/{video-BOYqf2Im.js → video-Bd_29-rK.js} +76 -71
- package/dist/chunks/video-Bd_29-rK.js.map +1 -0
- package/dist/embed.d.ts +11 -0
- package/dist/icons.d.ts +2 -0
- package/dist/index.js +599 -448
- package/dist/index.js.map +1 -1
- package/dist/loader.js +1 -1
- package/dist/modal.d.ts +14 -4
- package/dist/qaid.umd.cjs +14 -38
- package/dist/qaid.umd.cjs.map +1 -1
- package/dist/recording.d.ts +6 -0
- package/dist/screenshot.d.ts +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/video-capture.d.ts +6 -0
- package/package.json +1 -1
- package/dist/chunks/annotate-CMq95aXG.js.map +0 -1
- package/dist/chunks/loader-CYM1rxDH.js.map +0 -1
- package/dist/chunks/modal-Br5x7m6f.js +0 -150
- package/dist/chunks/modal-Br5x7m6f.js.map +0 -1
- package/dist/chunks/recording-BVFd_ryf.js.map +0 -1
- package/dist/chunks/video-BOYqf2Im.js.map +0 -1
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { c as h } from "./loader-CYM1rxDH.js";
|
|
2
|
-
import { T as d, a as c, F as r, D as p } from "./annotate-CMq95aXG.js";
|
|
3
|
-
class f {
|
|
4
|
-
constructor(e) {
|
|
5
|
-
this.host = e;
|
|
6
|
-
}
|
|
7
|
-
host;
|
|
8
|
-
modalContainer = null;
|
|
9
|
-
backdrop = null;
|
|
10
|
-
open() {
|
|
11
|
-
const e = this.host.ensureOverlayHost();
|
|
12
|
-
this.backdrop = document.createElement("div"), this.backdrop.className = "qaid-backdrop", this.backdrop.style.zIndex = String(this.host.config.zIndex + 2), this.backdrop.style.background = `rgba(0, 0, 0, ${this.host.config.backdropOpacity})`, this.backdrop.addEventListener("click", () => this.close()), this.host.applyVars(this.backdrop), this.host.isMobile ? this.showBottomSheet() : this.showPositionedModal(), e.appendChild(this.backdrop), document.addEventListener("keydown", this.host.boundKeyDown);
|
|
13
|
-
}
|
|
14
|
-
/** Escape / external close. Runs the finalize-PATCH + teardown. */
|
|
15
|
-
close() {
|
|
16
|
-
this.host.state === "MODAL_OPEN" && this.host.feedbackId && fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {
|
|
17
|
-
method: "PATCH",
|
|
18
|
-
headers: { "Content-Type": "application/json" },
|
|
19
|
-
body: JSON.stringify({ message: null })
|
|
20
|
-
}).catch((e) => console.error("Failed to finalize feedback:", e)), this.teardown(), this.host.resetFeedbackUi();
|
|
21
|
-
}
|
|
22
|
-
/** DOM/listener teardown only — no PATCH, no state reset (for embed destroy). */
|
|
23
|
-
destroy() {
|
|
24
|
-
this.teardown();
|
|
25
|
-
}
|
|
26
|
-
teardown() {
|
|
27
|
-
this.host.closeDialogA11y(), this.modalContainer && (this.modalContainer.remove(), this.modalContainer = null), this.backdrop && (this.backdrop.remove(), this.backdrop = null), document.removeEventListener("keydown", this.host.boundKeyDown);
|
|
28
|
-
}
|
|
29
|
-
showBottomSheet() {
|
|
30
|
-
const e = this.host.ensureOverlayHost(), t = document.createElement("div");
|
|
31
|
-
t.className = "qaid-bottom-sheet", t.style.zIndex = String(this.host.config.zIndex + 3), t.innerHTML = `
|
|
32
|
-
<div class="qaid-bottom-sheet-content">
|
|
33
|
-
<div class="qaid-bottom-sheet-handle"></div>
|
|
34
|
-
${this.getModalContent()}
|
|
35
|
-
</div>
|
|
36
|
-
`, this.host.applyVars(t), e.appendChild(t), this.modalContainer = t, this.setupModalInteractions();
|
|
37
|
-
}
|
|
38
|
-
showPositionedModal() {
|
|
39
|
-
const e = this.host.ensureOverlayHost(), { modal: t, arrow: i } = h(
|
|
40
|
-
this.host.selectedBounds,
|
|
41
|
-
window.innerWidth,
|
|
42
|
-
window.innerHeight,
|
|
43
|
-
{
|
|
44
|
-
width: this.host.config.modalWidth,
|
|
45
|
-
height: 280,
|
|
46
|
-
arrowHeight: 12,
|
|
47
|
-
gap: 8,
|
|
48
|
-
viewportPadding: 16
|
|
49
|
-
}
|
|
50
|
-
);
|
|
51
|
-
this.modalContainer = document.createElement("div"), this.modalContainer.className = `qaid-modal-container qaid-${t.position}`, this.modalContainer.style.top = `${t.top}px`, this.modalContainer.style.left = `${t.left}px`, this.modalContainer.style.zIndex = String(this.host.config.zIndex + 3);
|
|
52
|
-
const o = document.createElement("div");
|
|
53
|
-
o.className = "qaid-modal-arrow", o.style.left = `${i.left}px`;
|
|
54
|
-
const s = document.createElement("div");
|
|
55
|
-
s.className = "qaid-modal-box", s.innerHTML = this.getModalContent(), this.modalContainer.appendChild(o), this.modalContainer.appendChild(s), this.host.applyVars(this.modalContainer), e.appendChild(this.modalContainer), this.setupModalInteractions();
|
|
56
|
-
}
|
|
57
|
-
getModalContent() {
|
|
58
|
-
const e = this.host.feedbackData.feedbackType, t = e === "up", i = this.host.config.positiveIcon || d, o = this.host.config.negativeIcon || c, s = this.host.config.buttonClass ? `qaid-type-toggle qaid-type-toggle-custom ${this.host.config.buttonClass} ${t ? "qaid-btn-up" : "qaid-btn-down"}` : `qaid-type-toggle ${t ? "qaid-type-up" : "qaid-type-down"}`;
|
|
59
|
-
return `
|
|
60
|
-
<div class="qaid-modal-header">
|
|
61
|
-
${e === "neutral" ? `<span class="qaid-type-static" aria-hidden="true">${this.host.config.feedbackIcon || r}</span>` : `<button type="button" class="${s}" title="Click to switch" aria-pressed="${t}" aria-label="${t ? "Feedback type: positive" : "Feedback type: negative"}">
|
|
62
|
-
${t ? i : o}
|
|
63
|
-
</button>`}
|
|
64
|
-
<div class="qaid-modal-header-text">
|
|
65
|
-
<h3 class="qaid-modal-title" id="qaid-modal-title-${this.host.uid}">${this.host.config.text.modalTitle}</h3>
|
|
66
|
-
<p class="qaid-modal-subtitle" id="qaid-modal-subtitle-${this.host.uid}">${this.host.config.text.modalSubtitle}</p>
|
|
67
|
-
</div>
|
|
68
|
-
</div>
|
|
69
|
-
<textarea class="qaid-textarea" aria-label="${this.host.config.text.modalSubtitle}" placeholder="${this.host.config.text.placeholder}"></textarea>
|
|
70
|
-
<div class="qaid-btn-row">
|
|
71
|
-
<button type="button" class="qaid-btn-submit">${this.host.config.text.skipButton}</button>
|
|
72
|
-
</div>
|
|
73
|
-
`;
|
|
74
|
-
}
|
|
75
|
-
setupModalInteractions() {
|
|
76
|
-
this.host.openDialogA11y(this.modalContainer, {
|
|
77
|
-
labelledbyId: `qaid-modal-title-${this.host.uid}`,
|
|
78
|
-
describedbyId: `qaid-modal-subtitle-${this.host.uid}`
|
|
79
|
-
});
|
|
80
|
-
const e = this.modalContainer.querySelector(".qaid-textarea"), t = this.modalContainer.querySelector(".qaid-btn-submit");
|
|
81
|
-
e && (setTimeout(() => e.focus(), 100), e.addEventListener("input", () => {
|
|
82
|
-
t && (t.textContent = e.value.trim() ? this.host.config.text.submitButton : this.host.config.text.skipButton);
|
|
83
|
-
})), t && t.addEventListener("click", () => {
|
|
84
|
-
const o = e?.value.trim() || null;
|
|
85
|
-
this.submitMessage(o);
|
|
86
|
-
});
|
|
87
|
-
const i = this.modalContainer.querySelector(".qaid-type-toggle");
|
|
88
|
-
i && i.addEventListener("click", () => {
|
|
89
|
-
const o = this.host.feedbackData.feedbackType === "up" ? "down" : "up";
|
|
90
|
-
this.host.feedbackData.feedbackType = o, this.host.config.buttonClass ? (i.classList.toggle("qaid-btn-up", o === "up"), i.classList.toggle("qaid-btn-down", o === "down")) : (i.classList.toggle("qaid-type-up", o === "up"), i.classList.toggle("qaid-type-down", o === "down"));
|
|
91
|
-
const s = this.host.config.positiveIcon || d, n = this.host.config.negativeIcon || c;
|
|
92
|
-
i.innerHTML = o === "up" ? s : n;
|
|
93
|
-
const a = o === "up" ? "Feedback type: positive" : "Feedback type: negative";
|
|
94
|
-
i.setAttribute("aria-pressed", String(o === "up")), i.setAttribute("aria-label", a), this.host.announceMsg(a), this.host.feedbackId && fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {
|
|
95
|
-
method: "PATCH",
|
|
96
|
-
headers: { "Content-Type": "application/json" },
|
|
97
|
-
body: JSON.stringify({ feedbackType: o })
|
|
98
|
-
}).catch((l) => console.error("Failed to update feedback type:", l));
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
async submitMessage(e) {
|
|
102
|
-
let t = !1;
|
|
103
|
-
if (this.host.feedbackId) {
|
|
104
|
-
try {
|
|
105
|
-
await fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {
|
|
106
|
-
method: "PATCH",
|
|
107
|
-
headers: { "Content-Type": "application/json" },
|
|
108
|
-
body: JSON.stringify({ message: e })
|
|
109
|
-
}), t = !0;
|
|
110
|
-
} catch (i) {
|
|
111
|
-
console.error("Failed to submit feedback message:", i);
|
|
112
|
-
}
|
|
113
|
-
this.host.setFeedbackId(null);
|
|
114
|
-
}
|
|
115
|
-
if (t && !this.host.config.hideConfirmation) {
|
|
116
|
-
this.showConfirmation();
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
this.close();
|
|
120
|
-
}
|
|
121
|
-
/**
|
|
122
|
-
* Replace the modal's contents with a checkmark and a short acknowledgement.
|
|
123
|
-
*
|
|
124
|
-
* The submit button that had focus is gone by this point, so focus moves to
|
|
125
|
-
* the heading (WCAG 2.4.3) and the message is announced. Mirrors the quests
|
|
126
|
-
* embed's thank-you screen so the two products confirm the same way.
|
|
127
|
-
*/
|
|
128
|
-
showConfirmation() {
|
|
129
|
-
const e = this.modalContainer?.querySelector(".qaid-modal-box") ?? this.modalContainer?.querySelector(".qaid-bottom-sheet-content") ?? this.modalContainer;
|
|
130
|
-
if (!e) {
|
|
131
|
-
this.close();
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const t = this.host.config.text, i = `qaid-confirm-title-${this.host.uid}`;
|
|
135
|
-
e.innerHTML = `
|
|
136
|
-
<div class="qaid-confirm">
|
|
137
|
-
<span class="qaid-confirm-icon" aria-hidden="true">${p}</span>
|
|
138
|
-
<h3 class="qaid-confirm-title" id="${i}" tabindex="-1">${t.confirmationTitle}</h3>
|
|
139
|
-
<p class="qaid-confirm-message">${t.confirmationMessage}</p>
|
|
140
|
-
<button type="button" class="qaid-btn-submit qaid-confirm-close">${t.confirmationClose}</button>
|
|
141
|
-
</div>
|
|
142
|
-
`, e.querySelector(".qaid-confirm-close")?.addEventListener("click", () => this.close());
|
|
143
|
-
const o = e.querySelector(`#${CSS.escape(i)}`);
|
|
144
|
-
requestAnimationFrame(() => o?.focus()), this.host.announceMsg(`${t.confirmationTitle} ${t.confirmationMessage}`, !0);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
export {
|
|
148
|
-
f as ModalController
|
|
149
|
-
};
|
|
150
|
-
//# sourceMappingURL=modal-Br5x7m6f.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"modal-Br5x7m6f.js","sources":["../../src/modal.ts"],"sourcesContent":["/**\n * Feedback message modal — extracted from embed.ts into its own lazily-loaded\n * chunk. It's fetched the first time a message modal opens (after feedback is\n * submitted), and pre-warmed while the user is targeting an element.\n *\n * The controller owns the modal/backdrop DOM and the message PATCH flow, and\n * talks back to the embed through the narrow `ModalHost` interface.\n */\n\nimport { calculateModalAndArrowPosition } from \"./modal-positioning\";\nimport { THUMBS_UP_ICON, THUMBS_DOWN_ICON, FEEDBACK_ICON, DONE_ICON } from \"./icons\";\nimport type {\n ResolvedFeedbackConfig,\n EmbedState,\n FeedbackData,\n SelectedBounds,\n} from \"./types\";\n\n/** The slice of the embed the modal needs. */\nexport interface ModalHost {\n readonly config: ResolvedFeedbackConfig;\n readonly uid: string;\n readonly isMobile: boolean;\n readonly state: EmbedState;\n readonly boundKeyDown: (e: KeyboardEvent) => void;\n /** Live references — the toggle mutates feedbackType; positioning reads bounds. */\n readonly feedbackData: FeedbackData;\n readonly selectedBounds: SelectedBounds;\n readonly feedbackId: number | null;\n setFeedbackId(id: number | null): void;\n ensureOverlayHost(): ShadowRoot;\n applyVars(el: HTMLElement): void;\n announceMsg(message: string, assertive?: boolean): void;\n openDialogA11y(\n container: HTMLElement,\n opts: { labelledbyId?: string; describedbyId?: string; label?: string }\n ): void;\n closeDialogA11y(): void;\n resetFeedbackUi(): void;\n}\n\nexport class ModalController {\n private modalContainer: HTMLDivElement | null = null;\n private backdrop: HTMLDivElement | null = null;\n\n constructor(private host: ModalHost) {}\n\n open(): void {\n const root = this.host.ensureOverlayHost();\n\n // Create backdrop\n this.backdrop = document.createElement(\"div\");\n this.backdrop.className = \"qaid-backdrop\";\n this.backdrop.style.zIndex = String(this.host.config.zIndex + 2);\n this.backdrop.style.background = `rgba(0, 0, 0, ${this.host.config.backdropOpacity})`;\n this.backdrop.addEventListener(\"click\", () => this.close());\n this.host.applyVars(this.backdrop);\n\n if (this.host.isMobile) {\n this.showBottomSheet();\n } else {\n this.showPositionedModal();\n }\n\n root.appendChild(this.backdrop);\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n /** Escape / external close. Runs the finalize-PATCH + teardown. */\n close(): void {\n // If the modal is open and we're closing without submitting, still finalize.\n if (this.host.state === \"MODAL_OPEN\" && this.host.feedbackId) {\n fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message: null }),\n }).catch((err) => console.error(\"Failed to finalize feedback:\", err));\n }\n\n this.teardown();\n this.host.resetFeedbackUi();\n }\n\n /** DOM/listener teardown only — no PATCH, no state reset (for embed destroy). */\n destroy(): void {\n this.teardown();\n }\n\n private teardown(): void {\n this.host.closeDialogA11y();\n if (this.modalContainer) {\n this.modalContainer.remove();\n this.modalContainer = null;\n }\n if (this.backdrop) {\n this.backdrop.remove();\n this.backdrop = null;\n }\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n private showBottomSheet(): void {\n const root = this.host.ensureOverlayHost();\n\n const sheet = document.createElement(\"div\");\n sheet.className = \"qaid-bottom-sheet\";\n sheet.style.zIndex = String(this.host.config.zIndex + 3);\n\n sheet.innerHTML = `\n <div class=\"qaid-bottom-sheet-content\">\n <div class=\"qaid-bottom-sheet-handle\"></div>\n ${this.getModalContent()}\n </div>\n `;\n\n this.host.applyVars(sheet);\n root.appendChild(sheet);\n this.modalContainer = sheet;\n\n this.setupModalInteractions();\n }\n\n private showPositionedModal(): void {\n const root = this.host.ensureOverlayHost();\n\n const { modal, arrow } = calculateModalAndArrowPosition(\n this.host.selectedBounds,\n window.innerWidth,\n window.innerHeight,\n {\n width: this.host.config.modalWidth,\n height: 280,\n arrowHeight: 12,\n gap: 8,\n viewportPadding: 16,\n }\n );\n\n this.modalContainer = document.createElement(\"div\");\n this.modalContainer.className = `qaid-modal-container qaid-${modal.position}`;\n this.modalContainer.style.top = `${modal.top}px`;\n this.modalContainer.style.left = `${modal.left}px`;\n this.modalContainer.style.zIndex = String(this.host.config.zIndex + 3);\n\n const arrowEl = document.createElement(\"div\");\n arrowEl.className = \"qaid-modal-arrow\";\n arrowEl.style.left = `${arrow.left}px`;\n\n const box = document.createElement(\"div\");\n box.className = \"qaid-modal-box\";\n box.innerHTML = this.getModalContent();\n\n this.modalContainer.appendChild(arrowEl);\n this.modalContainer.appendChild(box);\n this.host.applyVars(this.modalContainer);\n root.appendChild(this.modalContainer);\n\n this.setupModalInteractions();\n }\n\n private getModalContent(): string {\n const type = this.host.feedbackData.feedbackType;\n const isUp = type === \"up\";\n const positiveIcon = this.host.config.positiveIcon || THUMBS_UP_ICON;\n const negativeIcon = this.host.config.negativeIcon || THUMBS_DOWN_ICON;\n const toggleClass = this.host.config.buttonClass\n ? `qaid-type-toggle qaid-type-toggle-custom ${this.host.config.buttonClass} ${isUp ? \"qaid-btn-up\" : \"qaid-btn-down\"}`\n : `qaid-type-toggle ${isUp ? \"qaid-type-up\" : \"qaid-type-down\"}`;\n const toggleLabel = isUp ? \"Feedback type: positive\" : \"Feedback type: negative\";\n // Neutral (single-button) feedback has no sentiment to toggle — show a\n // static feedback icon in place of the up/down toggle.\n const header =\n type === \"neutral\"\n ? `<span class=\"qaid-type-static\" aria-hidden=\"true\">${this.host.config.feedbackIcon || FEEDBACK_ICON}</span>`\n : `<button type=\"button\" class=\"${toggleClass}\" title=\"Click to switch\" aria-pressed=\"${isUp}\" aria-label=\"${toggleLabel}\">\n ${isUp ? positiveIcon : negativeIcon}\n </button>`;\n return `\n <div class=\"qaid-modal-header\">\n ${header}\n <div class=\"qaid-modal-header-text\">\n <h3 class=\"qaid-modal-title\" id=\"qaid-modal-title-${this.host.uid}\">${this.host.config.text.modalTitle}</h3>\n <p class=\"qaid-modal-subtitle\" id=\"qaid-modal-subtitle-${this.host.uid}\">${this.host.config.text.modalSubtitle}</p>\n </div>\n </div>\n <textarea class=\"qaid-textarea\" aria-label=\"${this.host.config.text.modalSubtitle}\" placeholder=\"${this.host.config.text.placeholder}\"></textarea>\n <div class=\"qaid-btn-row\">\n <button type=\"button\" class=\"qaid-btn-submit\">${this.host.config.text.skipButton}</button>\n </div>\n `;\n }\n\n private setupModalInteractions(): void {\n // Dialog semantics + focus trap + background inert\n this.host.openDialogA11y(this.modalContainer!, {\n labelledbyId: `qaid-modal-title-${this.host.uid}`,\n describedbyId: `qaid-modal-subtitle-${this.host.uid}`,\n });\n\n const textarea =\n this.modalContainer!.querySelector<HTMLTextAreaElement>(\".qaid-textarea\");\n const submitBtn =\n this.modalContainer!.querySelector<HTMLButtonElement>(\".qaid-btn-submit\");\n if (textarea) {\n // Auto-focus\n setTimeout(() => textarea.focus(), 100);\n\n // Update button text based on content\n textarea.addEventListener(\"input\", () => {\n if (submitBtn) {\n submitBtn.textContent = textarea.value.trim()\n ? this.host.config.text.submitButton\n : this.host.config.text.skipButton;\n }\n });\n }\n\n if (submitBtn) {\n submitBtn.addEventListener(\"click\", () => {\n const message = textarea?.value.trim() || null;\n this.submitMessage(message);\n });\n }\n\n // Handle feedback type toggle\n const typeToggle = this.modalContainer!.querySelector<HTMLButtonElement>(\".qaid-type-toggle\");\n if (typeToggle) {\n typeToggle.addEventListener(\"click\", () => {\n const newType = this.host.feedbackData.feedbackType === \"up\" ? \"down\" : \"up\";\n this.host.feedbackData.feedbackType = newType;\n\n // Update UI — use different classes depending on custom buttonClass\n if (this.host.config.buttonClass) {\n typeToggle.classList.toggle(\"qaid-btn-up\", newType === \"up\");\n typeToggle.classList.toggle(\"qaid-btn-down\", newType === \"down\");\n } else {\n typeToggle.classList.toggle(\"qaid-type-up\", newType === \"up\");\n typeToggle.classList.toggle(\"qaid-type-down\", newType === \"down\");\n }\n const positiveIcon = this.host.config.positiveIcon || THUMBS_UP_ICON;\n const negativeIcon = this.host.config.negativeIcon || THUMBS_DOWN_ICON;\n typeToggle.innerHTML = newType === \"up\" ? positiveIcon : negativeIcon;\n\n // Update accessible state and announce the change\n const toggleLabel = newType === \"up\" ? \"Feedback type: positive\" : \"Feedback type: negative\";\n typeToggle.setAttribute(\"aria-pressed\", String(newType === \"up\"));\n typeToggle.setAttribute(\"aria-label\", toggleLabel);\n this.host.announceMsg(toggleLabel);\n\n // Update feedback on server\n if (this.host.feedbackId) {\n fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ feedbackType: newType }),\n }).catch((err) => console.error(\"Failed to update feedback type:\", err));\n }\n });\n }\n }\n\n private async submitMessage(message: string | null): Promise<void> {\n let sent = false;\n\n if (this.host.feedbackId) {\n try {\n await fetch(`${this.host.config.endpoint}/${this.host.feedbackId}`, {\n method: \"PATCH\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ message }),\n });\n sent = true;\n } catch (error) {\n console.error(\"Failed to submit feedback message:\", error);\n }\n // Clear feedbackId so close() doesn't send another PATCH.\n this.host.setFeedbackId(null);\n }\n\n // A submit that simply removes the modal reads as a failure — nothing\n // acknowledges that the message was sent. Show a success screen unless the\n // host opted out.\n //\n // Only on a genuine success, though: with no feedbackId the initial POST\n // never landed, and a failed PATCH means the message is gone. Telling\n // someone their feedback was received in either case would be a lie, so\n // those paths close exactly as they did before.\n if (sent && !this.host.config.hideConfirmation) {\n this.showConfirmation();\n return;\n }\n this.close();\n }\n\n /**\n * Replace the modal's contents with a checkmark and a short acknowledgement.\n *\n * The submit button that had focus is gone by this point, so focus moves to\n * the heading (WCAG 2.4.3) and the message is announced. Mirrors the quests\n * embed's thank-you screen so the two products confirm the same way.\n */\n private showConfirmation(): void {\n const box = this.modalContainer?.querySelector(\".qaid-modal-box\")\n ?? this.modalContainer?.querySelector(\".qaid-bottom-sheet-content\")\n ?? this.modalContainer;\n\n if (!box) {\n // Nothing to draw into — never strand the user with an open modal.\n this.close();\n return;\n }\n\n const text = this.host.config.text;\n const titleId = `qaid-confirm-title-${this.host.uid}`;\n\n box.innerHTML = `\n <div class=\"qaid-confirm\">\n <span class=\"qaid-confirm-icon\" aria-hidden=\"true\">${DONE_ICON}</span>\n <h3 class=\"qaid-confirm-title\" id=\"${titleId}\" tabindex=\"-1\">${text.confirmationTitle}</h3>\n <p class=\"qaid-confirm-message\">${text.confirmationMessage}</p>\n <button type=\"button\" class=\"qaid-btn-submit qaid-confirm-close\">${text.confirmationClose}</button>\n </div>\n `;\n\n box.querySelector(\".qaid-confirm-close\")?.addEventListener(\"click\", () => this.close());\n\n const title = box.querySelector<HTMLElement>(`#${CSS.escape(titleId)}`);\n // Focus after paint, matching the quests embed.\n requestAnimationFrame(() => title?.focus());\n this.host.announceMsg(`${text.confirmationTitle} ${text.confirmationMessage}`, true);\n }\n}\n"],"names":["ModalController","host","root","err","sheet","modal","arrow","calculateModalAndArrowPosition","arrowEl","box","type","isUp","positiveIcon","THUMBS_UP_ICON","negativeIcon","THUMBS_DOWN_ICON","toggleClass","FEEDBACK_ICON","textarea","submitBtn","message","typeToggle","newType","toggleLabel","sent","error","text","titleId","DONE_ICON","title"],"mappings":";;AAyCO,MAAMA,EAAgB;AAAA,EAI3B,YAAoBC,GAAiB;AAAjB,SAAA,OAAAA;AAAA,EAAkB;AAAA,EAAlB;AAAA,EAHZ,iBAAwC;AAAA,EACxC,WAAkC;AAAA,EAI1C,OAAa;AACX,UAAMC,IAAO,KAAK,KAAK,kBAAA;AAGvB,SAAK,WAAW,SAAS,cAAc,KAAK,GAC5C,KAAK,SAAS,YAAY,iBAC1B,KAAK,SAAS,MAAM,SAAS,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC,GAC/D,KAAK,SAAS,MAAM,aAAa,iBAAiB,KAAK,KAAK,OAAO,eAAe,KAClF,KAAK,SAAS,iBAAiB,SAAS,MAAM,KAAK,OAAO,GAC1D,KAAK,KAAK,UAAU,KAAK,QAAQ,GAE7B,KAAK,KAAK,WACZ,KAAK,gBAAA,IAEL,KAAK,oBAAA,GAGPA,EAAK,YAAY,KAAK,QAAQ,GAC9B,SAAS,iBAAiB,WAAW,KAAK,KAAK,YAAY;AAAA,EAC7D;AAAA;AAAA,EAGA,QAAc;AAEZ,IAAI,KAAK,KAAK,UAAU,gBAAgB,KAAK,KAAK,cAChD,MAAM,GAAG,KAAK,KAAK,OAAO,QAAQ,IAAI,KAAK,KAAK,UAAU,IAAI;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAA;AAAA,MAC3B,MAAM,KAAK,UAAU,EAAE,SAAS,MAAM;AAAA,IAAA,CACvC,EAAE,MAAM,CAACC,MAAQ,QAAQ,MAAM,gCAAgCA,CAAG,CAAC,GAGtE,KAAK,SAAA,GACL,KAAK,KAAK,gBAAA;AAAA,EACZ;AAAA;AAAA,EAGA,UAAgB;AACd,SAAK,SAAA;AAAA,EACP;AAAA,EAEQ,WAAiB;AACvB,SAAK,KAAK,gBAAA,GACN,KAAK,mBACP,KAAK,eAAe,OAAA,GACpB,KAAK,iBAAiB,OAEpB,KAAK,aACP,KAAK,SAAS,OAAA,GACd,KAAK,WAAW,OAElB,SAAS,oBAAoB,WAAW,KAAK,KAAK,YAAY;AAAA,EAChE;AAAA,EAEQ,kBAAwB;AAC9B,UAAMD,IAAO,KAAK,KAAK,kBAAA,GAEjBE,IAAQ,SAAS,cAAc,KAAK;AAC1C,IAAAA,EAAM,YAAY,qBAClBA,EAAM,MAAM,SAAS,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC,GAEvDA,EAAM,YAAY;AAAA;AAAA;AAAA,UAGZ,KAAK,iBAAiB;AAAA;AAAA,OAI5B,KAAK,KAAK,UAAUA,CAAK,GACzBF,EAAK,YAAYE,CAAK,GACtB,KAAK,iBAAiBA,GAEtB,KAAK,uBAAA;AAAA,EACP;AAAA,EAEQ,sBAA4B;AAClC,UAAMF,IAAO,KAAK,KAAK,kBAAA,GAEjB,EAAE,OAAAG,GAAO,OAAAC,EAAA,IAAUC;AAAA,MACvB,KAAK,KAAK;AAAA,MACV,OAAO;AAAA,MACP,OAAO;AAAA,MACP;AAAA,QACE,OAAO,KAAK,KAAK,OAAO;AAAA,QACxB,QAAQ;AAAA,QACR,aAAa;AAAA,QACb,KAAK;AAAA,QACL,iBAAiB;AAAA,MAAA;AAAA,IACnB;AAGF,SAAK,iBAAiB,SAAS,cAAc,KAAK,GAClD,KAAK,eAAe,YAAY,6BAA6BF,EAAM,QAAQ,IAC3E,KAAK,eAAe,MAAM,MAAM,GAAGA,EAAM,GAAG,MAC5C,KAAK,eAAe,MAAM,OAAO,GAAGA,EAAM,IAAI,MAC9C,KAAK,eAAe,MAAM,SAAS,OAAO,KAAK,KAAK,OAAO,SAAS,CAAC;AAErE,UAAMG,IAAU,SAAS,cAAc,KAAK;AAC5C,IAAAA,EAAQ,YAAY,oBACpBA,EAAQ,MAAM,OAAO,GAAGF,EAAM,IAAI;AAElC,UAAMG,IAAM,SAAS,cAAc,KAAK;AACxC,IAAAA,EAAI,YAAY,kBAChBA,EAAI,YAAY,KAAK,gBAAA,GAErB,KAAK,eAAe,YAAYD,CAAO,GACvC,KAAK,eAAe,YAAYC,CAAG,GACnC,KAAK,KAAK,UAAU,KAAK,cAAc,GACvCP,EAAK,YAAY,KAAK,cAAc,GAEpC,KAAK,uBAAA;AAAA,EACP;AAAA,EAEQ,kBAA0B;AAChC,UAAMQ,IAAO,KAAK,KAAK,aAAa,cAC9BC,IAAOD,MAAS,MAChBE,IAAe,KAAK,KAAK,OAAO,gBAAgBC,GAChDC,IAAe,KAAK,KAAK,OAAO,gBAAgBC,GAChDC,IAAc,KAAK,KAAK,OAAO,cACjC,4CAA4C,KAAK,KAAK,OAAO,WAAW,IAAIL,IAAO,gBAAgB,eAAe,KAClH,oBAAoBA,IAAO,iBAAiB,gBAAgB;AAUhE,WAAO;AAAA;AAAA,UALLD,MAAS,YACL,qDAAqD,KAAK,KAAK,OAAO,gBAAgBO,CAAa,YACnG,gCAAgCD,CAAW,2CAA2CL,CAAI,iBAN5EA,IAAO,4BAA4B,yBAMqE;AAAA,YACpHA,IAAOC,IAAeE,CAAY;AAAA,kBAI9B;AAAA;AAAA,8DAE8C,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,OAAO,KAAK,UAAU;AAAA,mEAC7C,KAAK,KAAK,GAAG,KAAK,KAAK,KAAK,OAAO,KAAK,aAAa;AAAA;AAAA;AAAA,oDAGpE,KAAK,KAAK,OAAO,KAAK,aAAa,kBAAkB,KAAK,KAAK,OAAO,KAAK,WAAW;AAAA;AAAA,wDAElF,KAAK,KAAK,OAAO,KAAK,UAAU;AAAA;AAAA;AAAA,EAGtF;AAAA,EAEQ,yBAA+B;AAErC,SAAK,KAAK,eAAe,KAAK,gBAAiB;AAAA,MAC7C,cAAc,oBAAoB,KAAK,KAAK,GAAG;AAAA,MAC/C,eAAe,uBAAuB,KAAK,KAAK,GAAG;AAAA,IAAA,CACpD;AAED,UAAMI,IACJ,KAAK,eAAgB,cAAmC,gBAAgB,GACpEC,IACJ,KAAK,eAAgB,cAAiC,kBAAkB;AAC1E,IAAID,MAEF,WAAW,MAAMA,EAAS,MAAA,GAAS,GAAG,GAGtCA,EAAS,iBAAiB,SAAS,MAAM;AACvC,MAAIC,MACFA,EAAU,cAAcD,EAAS,MAAM,KAAA,IACnC,KAAK,KAAK,OAAO,KAAK,eACtB,KAAK,KAAK,OAAO,KAAK;AAAA,IAE9B,CAAC,IAGCC,KACFA,EAAU,iBAAiB,SAAS,MAAM;AACxC,YAAMC,IAAUF,GAAU,MAAM,KAAA,KAAU;AAC1C,WAAK,cAAcE,CAAO;AAAA,IAC5B,CAAC;AAIH,UAAMC,IAAa,KAAK,eAAgB,cAAiC,mBAAmB;AAC5F,IAAIA,KACFA,EAAW,iBAAiB,SAAS,MAAM;AACzC,YAAMC,IAAU,KAAK,KAAK,aAAa,iBAAiB,OAAO,SAAS;AACxE,WAAK,KAAK,aAAa,eAAeA,GAGlC,KAAK,KAAK,OAAO,eACnBD,EAAW,UAAU,OAAO,eAAeC,MAAY,IAAI,GAC3DD,EAAW,UAAU,OAAO,iBAAiBC,MAAY,MAAM,MAE/DD,EAAW,UAAU,OAAO,gBAAgBC,MAAY,IAAI,GAC5DD,EAAW,UAAU,OAAO,kBAAkBC,MAAY,MAAM;AAElE,YAAMV,IAAe,KAAK,KAAK,OAAO,gBAAgBC,GAChDC,IAAe,KAAK,KAAK,OAAO,gBAAgBC;AACtD,MAAAM,EAAW,YAAYC,MAAY,OAAOV,IAAeE;AAGzD,YAAMS,IAAcD,MAAY,OAAO,4BAA4B;AACnE,MAAAD,EAAW,aAAa,gBAAgB,OAAOC,MAAY,IAAI,CAAC,GAChED,EAAW,aAAa,cAAcE,CAAW,GACjD,KAAK,KAAK,YAAYA,CAAW,GAG7B,KAAK,KAAK,cACZ,MAAM,GAAG,KAAK,KAAK,OAAO,QAAQ,IAAI,KAAK,KAAK,UAAU,IAAI;AAAA,QAC5D,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAA;AAAA,QAC3B,MAAM,KAAK,UAAU,EAAE,cAAcD,GAAS;AAAA,MAAA,CAC/C,EAAE,MAAM,CAACnB,MAAQ,QAAQ,MAAM,mCAAmCA,CAAG,CAAC;AAAA,IAE3E,CAAC;AAAA,EAEL;AAAA,EAEA,MAAc,cAAciB,GAAuC;AACjE,QAAII,IAAO;AAEX,QAAI,KAAK,KAAK,YAAY;AACxB,UAAI;AACF,cAAM,MAAM,GAAG,KAAK,KAAK,OAAO,QAAQ,IAAI,KAAK,KAAK,UAAU,IAAI;AAAA,UAClE,QAAQ;AAAA,UACR,SAAS,EAAE,gBAAgB,mBAAA;AAAA,UAC3B,MAAM,KAAK,UAAU,EAAE,SAAAJ,GAAS;AAAA,QAAA,CACjC,GACDI,IAAO;AAAA,MACT,SAASC,GAAO;AACd,gBAAQ,MAAM,sCAAsCA,CAAK;AAAA,MAC3D;AAEA,WAAK,KAAK,cAAc,IAAI;AAAA,IAC9B;AAUA,QAAID,KAAQ,CAAC,KAAK,KAAK,OAAO,kBAAkB;AAC9C,WAAK,iBAAA;AACL;AAAA,IACF;AACA,SAAK,MAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASQ,mBAAyB;AAC/B,UAAMf,IAAM,KAAK,gBAAgB,cAAc,iBAAiB,KAC3D,KAAK,gBAAgB,cAAc,4BAA4B,KAC/D,KAAK;AAEV,QAAI,CAACA,GAAK;AAER,WAAK,MAAA;AACL;AAAA,IACF;AAEA,UAAMiB,IAAO,KAAK,KAAK,OAAO,MACxBC,IAAU,sBAAsB,KAAK,KAAK,GAAG;AAEnD,IAAAlB,EAAI,YAAY;AAAA;AAAA,6DAEyCmB,CAAS;AAAA,6CACzBD,CAAO,mBAAmBD,EAAK,iBAAiB;AAAA,0CACnDA,EAAK,mBAAmB;AAAA,2EACSA,EAAK,iBAAiB;AAAA;AAAA,OAI7FjB,EAAI,cAAc,qBAAqB,GAAG,iBAAiB,SAAS,MAAM,KAAK,OAAO;AAEtF,UAAMoB,IAAQpB,EAAI,cAA2B,IAAI,IAAI,OAAOkB,CAAO,CAAC,EAAE;AAEtE,0BAAsB,MAAME,GAAO,OAAO,GAC1C,KAAK,KAAK,YAAY,GAAGH,EAAK,iBAAiB,IAAIA,EAAK,mBAAmB,IAAI,EAAI;AAAA,EACrF;AACF;"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"recording-BVFd_ryf.js","sources":["../../src/dom.ts","../../src/network-capture.ts","../../src/recording.ts"],"sourcesContent":["/**\n * Nano DOM factory — a purpose-built, construction-only helper for this embed.\n *\n * The embed builds all of its UI imperatively inside a Shadow DOM. That means\n * the same create → set class/style/attrs → wire events → append dance repeats\n * across every button, overlay, modal, and recording control. `h()` collapses\n * that dance into a single declarative expression.\n *\n * It is deliberately NOT a framework: no reactivity, no virtual DOM, no runtime\n * state. It only constructs. That fits what the embed actually needs (one-shot\n * DOM construction plus a few imperative updates), adds no dependency, and\n * keeps the package zero-dependency. Aliasing `document` here also lets the\n * minifier mangle the call site, which the global `document.createElement`\n * can't be.\n */\n\nconst d = document;\n\nexport interface ElProps {\n /** className */\n class?: string;\n /** Inline styles as a cssText string. */\n style?: string;\n /** innerHTML — for icons / trusted SVG markup. */\n html?: string;\n /** textContent. */\n text?: string;\n /** Attributes set via setAttribute (aria-*, data-*, type, role, …). */\n attrs?: Record<string, string>;\n /** Event listeners keyed by type. */\n on?: Record<string, (e: Event) => void>;\n}\n\ntype Child = Node | string | null | undefined | false;\n\n/**\n * Create an element, apply props, and append children. Falsy children are\n * skipped so conditional content reads cleanly (`cond && h(...)`).\n */\nexport function h<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n props?: ElProps | null,\n ...children: Child[]\n): HTMLElementTagNameMap[K] {\n const el = d.createElement(tag);\n if (props) {\n if (props.class) el.className = props.class;\n if (props.style) el.style.cssText = props.style;\n if (props.html != null) el.innerHTML = props.html;\n if (props.text != null) el.textContent = props.text;\n if (props.attrs) {\n for (const k in props.attrs) el.setAttribute(k, props.attrs[k]);\n }\n if (props.on) {\n for (const k in props.on) el.addEventListener(k, props.on[k]);\n }\n }\n for (const child of children) {\n if (child != null && child !== false) el.append(child);\n }\n return el;\n}\n","/**\n * Network capture utilities\n * Intercepts fetch() and XMLHttpRequest to capture 4xx/5xx errors\n */\n\nimport type { NetworkError } from \"./types\";\n\nconst MAX_ENTRIES = 20;\nconst MAX_BODY_SIZE = 4096; // 4KB\n\nexport interface NetworkCapture {\n errors: NetworkError[];\n restore: () => void;\n}\n\n/**\n * Truncate a body to max size. Callers must guard against null/undefined.\n */\nexport function truncateBody(body: unknown): string {\n const str = typeof body === \"string\" ? body : JSON.stringify(body);\n if (str.length > MAX_BODY_SIZE) {\n return str.slice(0, MAX_BODY_SIZE) + \"…[truncated]\";\n }\n return str;\n}\n\n/**\n * Add an error to the buffer, evicting oldest if at capacity\n */\nexport function addEntry(errors: NetworkError[], entry: NetworkError): void {\n if (errors.length >= MAX_ENTRIES) {\n errors.shift();\n }\n errors.push(entry);\n}\n\n/**\n * Read response body as text, with size limit\n */\nasync function safeReadBody(response: Response): Promise<string | undefined> {\n try {\n const text = await response.clone().text();\n return truncateBody(text);\n } catch {\n return undefined;\n }\n}\n\n/**\n * Intercept fetch() and XMLHttpRequest to capture network errors (4xx/5xx)\n */\nexport function captureNetworkErrors(): NetworkCapture {\n const errors: NetworkError[] = [];\n const originalFetch = window.fetch;\n\n // Monkey-patch fetch\n window.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {\n const url = typeof input === \"string\" ? input : input instanceof URL ? input.toString() : input.url;\n const method = init?.method ?? (typeof input === \"object\" && \"method\" in input ? input.method : \"GET\");\n let requestBody: string | undefined;\n if (init?.body) {\n requestBody = truncateBody(init.body);\n }\n\n const response = await originalFetch.apply(window, [input, init]);\n\n if (response.status >= 400) {\n const responseBody = await safeReadBody(response);\n addEntry(errors, {\n url,\n method: method.toUpperCase(),\n status: response.status,\n statusText: response.statusText,\n requestBody,\n responseBody,\n timestamp: Date.now(),\n });\n }\n\n return response;\n };\n\n // Monkey-patch XMLHttpRequest\n const originalXHROpen = XMLHttpRequest.prototype.open;\n const originalXHRSend = XMLHttpRequest.prototype.send;\n\n XMLHttpRequest.prototype.open = function (method: string, url: string | URL, ...rest: unknown[]) {\n (this as XMLHttpRequest & { _qaid_method: string; _qaid_url: string })._qaid_method = method;\n (this as XMLHttpRequest & { _qaid_url: string })._qaid_url = typeof url === \"string\" ? url : url.toString();\n return originalXHROpen.apply(this, [method, url, ...rest] as Parameters<typeof originalXHROpen>);\n };\n\n XMLHttpRequest.prototype.send = function (body?: Document | XMLHttpRequestBodyInit | null) {\n const xhr = this as XMLHttpRequest & { _qaid_method: string; _qaid_url: string };\n const requestBody = body ? truncateBody(body) : undefined;\n\n xhr.addEventListener(\"load\", function () {\n if (xhr.status >= 400) {\n addEntry(errors, {\n url: xhr._qaid_url,\n method: xhr._qaid_method.toUpperCase(),\n status: xhr.status,\n statusText: xhr.statusText,\n requestBody,\n responseBody: truncateBody(xhr.responseText),\n timestamp: Date.now(),\n });\n }\n });\n\n return originalXHRSend.apply(this, [body]);\n };\n\n return {\n errors,\n restore: () => {\n window.fetch = originalFetch;\n XMLHttpRequest.prototype.open = originalXHROpen;\n XMLHttpRequest.prototype.send = originalXHRSend;\n },\n };\n}\n","/**\n * Video-recording subsystem — extracted from embed.ts so it lives in its own\n * lazily-loaded chunk. A thumbs-only visitor never downloads any of this; the\n * embed dynamically imports it the first time the record button is used (and\n * warms it on hover — see embed.ts prewarmVideo).\n *\n * The controller owns all recording state and UI (recorder, redaction picker,\n * indicator, preview, submit). It talks back to the embed through the narrow\n * `RecordingHost` interface, so embed internals stay private and the coupling\n * is explicit.\n */\n\nimport { h } from \"./dom\";\nimport { isEmbedElement } from \"./dom-utils\";\nimport { captureNetworkErrors, type NetworkCapture } from \"./network-capture\";\nimport type { VideoRecorder } from \"./video-capture\";\nimport type { ResolvedFeedbackConfig, EmbedState } from \"./types\";\nimport type { ConsoleCapture } from \"./console-capture\";\n\n/** The slice of the embed the recording subsystem needs. */\nexport interface RecordingHost {\n readonly config: ResolvedFeedbackConfig;\n readonly visitorId: string;\n readonly uid: string;\n readonly overlayShadowHost: HTMLDivElement | null;\n readonly consoleCapture: ConsoleCapture | null;\n readonly boundKeyDown: (e: KeyboardEvent) => void;\n readonly state: EmbedState;\n setState(state: EmbedState): void;\n ensureOverlayHost(): ShadowRoot;\n applyVars(el: HTMLElement): void;\n announceMsg(message: string, assertive?: boolean): void;\n openDialogA11y(\n container: HTMLElement,\n opts: { labelledbyId?: string; describedbyId?: string; label?: string }\n ): void;\n closeDialogA11y(): void;\n setButtonsDisabled(disabled: boolean): void;\n tryLaunchQuest(\n type: \"up\" | \"down\" | \"video\",\n feedbackId: number | string | null\n ): Promise<boolean>;\n}\n\nexport class RecordingController {\n private videoRecorder: VideoRecorder | null = null;\n private networkCapture: NetworkCapture | null = null;\n private recordedBlob: Blob | null = null;\n private recordingIndicator: HTMLDivElement | null = null;\n private videoPreview: HTMLDivElement | null = null;\n private isRecording = false;\n private isSendingVideo = false;\n\n // Pre-recording redaction picking.\n private redactPicks: Element[] = [];\n private redactPickerRoot: HTMLDivElement | null = null;\n private redactRafId = 0;\n private boundRedactClick = (e: MouseEvent): void => this.handleRedactPickClick(e);\n\n constructor(private host: RecordingHost) {}\n\n /** Escape-key handler; returns true when it consumed the key. */\n handleEscape(): boolean {\n if (this.isRecording) {\n void this.stopRecording();\n return true;\n }\n if (this.videoPreview) {\n this.cancelRecordingPreview();\n return true;\n }\n if (this.redactPickerRoot) {\n this.finishRedactionPicking(false);\n return true;\n }\n return false;\n }\n\n destroy(): void {\n if (this.redactRafId) cancelAnimationFrame(this.redactRafId);\n this.redactRafId = 0;\n document.removeEventListener(\"click\", this.boundRedactClick, true);\n if (this.redactPickerRoot) {\n this.redactPickerRoot.remove();\n this.redactPickerRoot = null;\n }\n this.redactPicks = [];\n this.cleanupRecording();\n this.removeVideoPreview();\n }\n\n async startRecording(redactionElements: Element[] = []): Promise<void> {\n // Don't start if already recording or in a targeting/modal flow.\n if (this.isRecording || this.host.state !== \"IDLE\") return;\n\n try {\n this.networkCapture = captureNetworkErrors();\n\n const { createVideoRecorder } = await import(\"./video-capture\");\n this.videoRecorder = createVideoRecorder({\n maxDuration: this.host.config.videoOptions.maxDuration,\n redactionElements,\n });\n\n this.videoRecorder.onTick((elapsed) => {\n this.updateRecordingTimer(elapsed);\n });\n\n // Handle unexpected stops (browser stop button, max duration).\n this.videoRecorder.onStop((blob) => {\n if (this.isRecording) {\n this.recordedBlob = blob;\n this.isRecording = false;\n this.host.announceMsg(\"Recording stopped\");\n this.removeRecordingIndicator();\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n this.host.setButtonsDisabled(false);\n\n if (blob && blob.size > 0) {\n this.showRecordingPreview();\n } else {\n this.cleanupRecording();\n }\n }\n });\n\n await this.videoRecorder.start();\n this.isRecording = true;\n this.host.announceMsg(\"Recording started\");\n this.host.setButtonsDisabled(true);\n this.showRecordingIndicator();\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n } catch (error) {\n // User denied screen share, shared a disallowed surface (window / whole\n // screen), or another error occurred. Surface the tab-only message so a\n // rejected non-tab share isn't a silent no-op.\n if (error instanceof Error && /current tab/i.test(error.message)) {\n this.host.announceMsg(error.message);\n }\n this.cleanupRecording();\n }\n }\n\n private async stopRecording(): Promise<void> {\n try {\n this.recordedBlob = await this.videoRecorder!.stop();\n } catch {\n this.recordedBlob = null;\n }\n\n this.isRecording = false;\n this.host.announceMsg(\"Recording stopped\");\n this.removeRecordingIndicator();\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n\n if (this.recordedBlob && this.recordedBlob.size > 0) {\n this.showRecordingPreview();\n } else {\n this.cleanupRecording();\n }\n }\n\n /**\n * Pre-recording redaction picker. The user clicks page areas to blur; each\n * gets an outline that tracks its position, then \"Start recording\" hands the\n * picks to the recorder, which blurs their live bounding boxes so the\n * redaction follows the content as the page scrolls. \"Cancel\"/Escape aborts.\n */\n startPicking(): void {\n if (this.isRecording || this.host.state !== \"IDLE\") return;\n this.host.setState(\"REDACT_PICKING\");\n this.redactPicks = [];\n\n const root = this.host.ensureOverlayHost();\n\n // Layer holding the per-pick outlines (repositioned every frame).\n const boxHost = h(\"div\", { style: \"position:absolute;inset:0;\" });\n\n const container = h(\n \"div\",\n { class: \"qaid-redact-picker\", style: \"position:fixed;inset:0;pointer-events:none;\" },\n boxHost,\n // Control bar — the only pointer-interactive part of the overlay.\n h(\n \"div\",\n {\n style:\n \"position:fixed;left:50%;bottom:24px;transform:translateX(-50%);\" +\n \"display:flex;gap:12px;align-items:center;pointer-events:auto;\" +\n \"background:#0b1220;color:#fff;border:1px solid #ff6b6b;border-radius:9999px;\" +\n \"padding:10px 16px;font:600 13px system-ui,sans-serif;box-shadow:0 8px 30px rgba(0,0,0,.5);\",\n },\n h(\"span\", {\n text: \"Click sensitive areas to blur\",\n attrs: { \"data-qaid-redact-label\": \"\" },\n }),\n h(\"button\", {\n text: \"Start recording\",\n attrs: { type: \"button\" },\n style:\n \"cursor:pointer;border:0;border-radius:9999px;padding:8px 14px;background:#ff6b6b;color:#0b1220;font:inherit;\",\n on: { click: () => this.finishRedactionPicking(true) },\n }),\n h(\"button\", {\n text: \"Cancel\",\n attrs: { type: \"button\" },\n style: \"cursor:pointer;border:0;background:transparent;color:#9fb3c8;font:inherit;\",\n on: { click: () => this.finishRedactionPicking(false) },\n }),\n ),\n );\n\n root.appendChild(container);\n this.redactPickerRoot = container;\n\n // Keep the outlines glued to their elements while the user scrolls/picks.\n const tick = (): void => {\n this.renderRedactPicks(boxHost);\n this.redactRafId = requestAnimationFrame(tick);\n };\n tick();\n\n document.addEventListener(\"click\", this.boundRedactClick, true);\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n this.host.announceMsg(\"Click sensitive areas to blur, then start recording\");\n }\n\n private handleRedactPickClick(e: MouseEvent): void {\n const target = e.target instanceof Element ? e.target : null;\n // Ignore clicks on the embed's own UI. isEmbedElement catches the retargeted\n // shadow host (real browsers compose click events to it); the contains()\n // check catches the picker's own control-bar buttons when the event isn't\n // retargeted, so their handlers still fire instead of being swallowed here.\n if (!target || isEmbedElement(target) || this.redactPickerRoot?.contains(target)) return;\n e.preventDefault();\n e.stopPropagation();\n\n const idx = this.redactPicks.indexOf(target);\n if (idx >= 0) {\n this.redactPicks.splice(idx, 1);\n } else {\n this.redactPicks.push(target);\n }\n\n const label = this.redactPickerRoot?.querySelector(\"[data-qaid-redact-label]\");\n if (label) {\n const n = this.redactPicks.length;\n label.textContent =\n n === 0 ? \"Click sensitive areas to blur\" : `${n} area${n === 1 ? \"\" : \"s\"} will be blurred`;\n }\n }\n\n private renderRedactPicks(layer: HTMLElement): void {\n // Rebuild the outline boxes to match current picks and their live positions.\n layer.textContent = \"\";\n for (const el of this.redactPicks) {\n const r = el.getBoundingClientRect();\n if (r.width <= 0 || r.height <= 0) continue;\n layer.appendChild(\n h(\"div\", {\n style:\n `position:fixed;left:${r.left}px;top:${r.top}px;width:${r.width}px;height:${r.height}px;` +\n \"border:2px solid #ff6b6b;border-radius:4px;background:rgba(255,107,107,.18);pointer-events:none;\",\n }),\n );\n }\n }\n\n private finishRedactionPicking(record: boolean): void {\n if (this.redactRafId) cancelAnimationFrame(this.redactRafId);\n this.redactRafId = 0;\n document.removeEventListener(\"click\", this.boundRedactClick, true);\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n if (this.redactPickerRoot) {\n this.redactPickerRoot.remove();\n this.redactPickerRoot = null;\n }\n const picks = this.redactPicks;\n this.redactPicks = [];\n this.host.setState(\"IDLE\");\n if (record) {\n void this.startRecording(picks);\n }\n }\n\n private showRecordingIndicator(): void {\n const root = this.host.ensureOverlayHost();\n\n this.recordingIndicator = h(\n \"div\",\n { class: \"qaid-recording-indicator\", style: `z-index:${this.host.config.zIndex + 100}` },\n h(\"div\", { class: \"qaid-recording-dot\" }),\n h(\"span\", {\n class: \"qaid-recording-time\",\n text: this.formatTime(this.host.config.videoOptions.maxDuration),\n }),\n h(\"button\", {\n class: \"qaid-recording-stop\",\n text: \"Stop\",\n attrs: { type: \"button\" },\n on: { click: () => this.stopRecording() },\n }),\n );\n\n this.host.applyVars(this.recordingIndicator);\n\n // Enable pointer events on the overlay host for the recording indicator.\n if (this.host.overlayShadowHost) {\n this.host.overlayShadowHost.style.pointerEvents = \"auto\";\n }\n\n root.appendChild(this.recordingIndicator);\n }\n\n private updateRecordingTimer(elapsed: number): void {\n if (!this.recordingIndicator) return;\n const timer = this.recordingIndicator.querySelector<HTMLSpanElement>(\".qaid-recording-time\");\n if (timer) {\n const remaining = Math.max(0, this.host.config.videoOptions.maxDuration - elapsed);\n timer.textContent = this.formatTime(remaining);\n // Announce the final countdown so it is perceivable non-visually.\n if (remaining > 0 && remaining <= 5) {\n this.host.announceMsg(`${remaining} second${remaining === 1 ? \"\" : \"s\"} remaining`);\n }\n }\n }\n\n private formatTime(seconds: number): string {\n const m = Math.floor(seconds / 60);\n const s = seconds % 60;\n return `${m}:${s.toString().padStart(2, \"0\")}`;\n }\n\n private removeRecordingIndicator(): void {\n if (this.recordingIndicator) {\n this.recordingIndicator.remove();\n this.recordingIndicator = null;\n }\n }\n\n private showRecordingPreview(): void {\n const root = this.host.ensureOverlayHost();\n\n const videoUrl = URL.createObjectURL(this.recordedBlob!);\n const titleId = `qaid-video-title-${this.host.uid}`;\n\n const title = h(\"h3\", { text: \"Review your recording\", attrs: { id: titleId } });\n\n // Boolean media props must be set as properties (the `muted` attribute is\n // unreliable for autoplay), so build the element then assign them.\n const videoEl = h(\"video\");\n videoEl.src = videoUrl;\n videoEl.controls = true;\n videoEl.autoplay = true;\n videoEl.muted = true;\n\n const textarea = h(\"textarea\", {\n attrs: {\n placeholder: \"Optional: Describe the issue you recorded...\",\n \"aria-label\": \"Describe the issue you recorded\",\n },\n });\n\n const sendBtn = h(\"button\", {\n class: \"qaid-video-btn qaid-video-btn-send\",\n text: \"Send\",\n attrs: { type: \"button\" },\n on: { click: () => this.submitVideoFeedback(textarea.value.trim() || null, sendBtn) },\n });\n\n const box = h(\n \"div\",\n { class: \"qaid-video-preview-box\" },\n title,\n videoEl,\n textarea,\n h(\n \"div\",\n { class: \"qaid-video-preview-actions\" },\n h(\"button\", {\n class: \"qaid-video-btn qaid-video-btn-cancel\",\n text: \"Cancel\",\n attrs: { type: \"button\" },\n on: { click: () => this.cancelRecordingPreview() },\n }),\n h(\"button\", {\n class: \"qaid-video-btn qaid-video-btn-rerecord\",\n text: \"Re-record\",\n attrs: { type: \"button\" },\n on: {\n click: () => {\n this.cancelRecordingPreview();\n this.startRecording();\n },\n },\n }),\n sendBtn,\n ),\n );\n\n this.videoPreview = h(\n \"div\",\n { class: \"qaid-video-preview\", style: `z-index:${this.host.config.zIndex + 100}` },\n box,\n );\n\n this.host.applyVars(this.videoPreview);\n\n // Enable pointer events on the overlay host for the video preview.\n if (this.host.overlayShadowHost) {\n this.host.overlayShadowHost.style.pointerEvents = \"auto\";\n }\n\n root.appendChild(this.videoPreview);\n\n // Dialog semantics + focus trap + background inert.\n this.host.openDialogA11y(box, { labelledbyId: title.id });\n\n // Listen for the escape key.\n document.addEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n private cancelRecordingPreview(): void {\n this.removeVideoPreview();\n this.cleanupRecording();\n }\n\n private removeVideoPreview(): void {\n // Release focus trap / inert and restore focus before removing the DOM.\n this.host.closeDialogA11y();\n if (this.videoPreview) {\n const videoEl = this.videoPreview.querySelector<HTMLVideoElement>(\"video\");\n if (videoEl?.src) {\n URL.revokeObjectURL(videoEl.src);\n }\n this.videoPreview.remove();\n this.videoPreview = null;\n }\n document.removeEventListener(\"keydown\", this.host.boundKeyDown);\n }\n\n private async submitVideoFeedback(message: string | null, sendBtn: HTMLButtonElement): Promise<void> {\n if (!this.recordedBlob || this.isSendingVideo) return;\n\n this.isSendingVideo = true;\n sendBtn.disabled = true;\n sendBtn.textContent = \"Sending...\";\n\n const formData = new FormData();\n formData.append(\"video\", this.recordedBlob, `recording.${this.recordedBlob.type.includes(\"mp4\") ? \"mp4\" : \"webm\"}`);\n formData.append(\"pageUrl\", window.location.href);\n formData.append(\"visitorId\", this.host.visitorId);\n\n if (this.host.config.apiKey) {\n formData.append(\"apiKey\", this.host.config.apiKey);\n }\n if (message) {\n formData.append(\"message\", message);\n }\n if (this.host.consoleCapture) {\n formData.append(\"consoleErrors\", JSON.stringify(this.host.consoleCapture.errors));\n }\n if (this.networkCapture) {\n formData.append(\"networkErrors\", JSON.stringify(this.networkCapture.errors));\n }\n\n let videoFeedbackId: string | number | null = null;\n try {\n const response = await fetch(`${this.host.config.endpoint}/video`, {\n method: \"POST\",\n body: formData,\n });\n\n if (response.ok) {\n this.host.announceMsg(\"Recording sent\");\n try {\n const data = (await response.json()) as { id?: string | number };\n videoFeedbackId = data?.id ?? null;\n } catch {\n // Non-JSON / no id — the quest can still launch, just unlinked.\n }\n } else {\n console.error(\"Failed to submit video feedback:\", await response.text());\n this.host.announceMsg(\"Failed to send recording\", true);\n }\n } catch (error) {\n console.error(\"Failed to submit video feedback:\", error);\n this.host.announceMsg(\"Failed to send recording\", true);\n }\n\n this.isSendingVideo = false;\n this.removeVideoPreview();\n this.cleanupRecording();\n\n // Once the recording is safely sent, launch the linked quest (if any),\n // passing the new feedback record id so its response is joinable.\n await this.host.tryLaunchQuest(\"video\", videoFeedbackId);\n }\n\n private cleanupRecording(): void {\n this.isRecording = false;\n this.removeRecordingIndicator();\n\n if (this.videoRecorder) {\n this.videoRecorder.destroy();\n this.videoRecorder = null;\n }\n if (this.networkCapture) {\n this.networkCapture.restore();\n this.networkCapture = null;\n }\n if (this.recordedBlob) {\n this.recordedBlob = null;\n }\n\n this.host.setButtonsDisabled(false);\n\n // Reset overlay host pointer events if nothing else needs them.\n if (this.host.overlayShadowHost && this.host.state === \"IDLE\") {\n this.host.overlayShadowHost.style.pointerEvents = \"none\";\n }\n }\n}\n"],"names":["d","h","tag","props","children","el","k","child","MAX_ENTRIES","MAX_BODY_SIZE","truncateBody","body","str","addEntry","errors","entry","safeReadBody","response","text","captureNetworkErrors","originalFetch","input","init","url","method","requestBody","responseBody","originalXHROpen","originalXHRSend","rest","xhr","RecordingController","host","e","redactionElements","createVideoRecorder","elapsed","blob","error","root","boxHost","container","tick","target","isEmbedElement","idx","label","n","layer","r","record","picks","timer","remaining","seconds","m","s","videoUrl","titleId","title","videoEl","textarea","sendBtn","box","message","formData","videoFeedbackId"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"video-BOYqf2Im.js","sources":["../../src/video-orientation.ts","../../src/video-redaction.ts","../../src/video-capture.ts"],"sourcesContent":["/**\n * iOS/iPadOS screen-recording orientation fix.\n *\n * On iOS/iPadOS, `getDisplayMedia` + `MediaRecorder` writes a device-orientation\n * rotation into the recorded file (even for screen captures). Players disagree\n * on whether to honour it — the widget's own preview `<video>` renders it\n * sideways, and so does the dashboard. Re-encoding the *live* decoded frames\n * (which carry no container rotation) through a canvas produces a file whose\n * pixels are already upright with no rotation metadata, so it renders the same\n * everywhere.\n *\n * If the live frames themselves come back with swapped dimensions (portrait\n * frame while the viewport is landscape, or vice versa), we additionally rotate\n * the canvas 90° to match the viewport. That branch only triggers on a clear\n * orientation mismatch, so a correctly-oriented recording is never rotated.\n */\n\nexport interface OrientationTransform {\n /** Output canvas width in px. */\n width: number;\n /** Output canvas height in px. */\n height: number;\n /** Clockwise rotation to apply while drawing: 0, 90, or -90. */\n rotate: 0 | 90 | -90;\n}\n\nexport interface OrientationCorrectedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\n/** True on iPhone/iPad/iPod, including iPadOS 13+ which reports as desktop Mac. */\nexport function isIOSDevice(): boolean {\n const ua = navigator.userAgent || \"\";\n return (\n /iP(hone|ad|od)/.test(ua) ||\n // iPadOS 13+ masquerades as \"MacIntel\" but is a multi-touch device.\n (navigator.platform === \"MacIntel\" && (navigator.maxTouchPoints || 0) > 1)\n );\n}\n\n/**\n * Decide the output canvas size and rotation needed to render a `frameW×frameH`\n * capture upright for a `viewportW×viewportH` viewport. When the frame and\n * viewport share an orientation, no rotation is applied (a straight re-encode,\n * which still strips the bad container rotation). Otherwise rotate 90°, its\n * direction chosen from the screen orientation angle.\n */\nexport function computeOrientationTransform(\n frameW: number,\n frameH: number,\n viewportW: number,\n viewportH: number,\n screenAngle: number\n): OrientationTransform {\n const frameLandscape = frameW >= frameH;\n const viewportLandscape = viewportW >= viewportH;\n\n if (frameLandscape === viewportLandscape) {\n return { width: frameW, height: frameH, rotate: 0 };\n }\n\n // Landscape-secondary (angle 270) is turned the opposite way to primary (90).\n const rotate: 90 | -90 = screenAngle === 270 ? -90 : 90;\n return { width: frameH, height: frameW, rotate };\n}\n\n/** Draw one video frame onto the canvas context, applying the transform. */\nexport function drawRotatedFrame(\n ctx: CanvasRenderingContext2D,\n source: CanvasImageSource,\n t: OrientationTransform\n): void {\n if (t.rotate === 0) {\n ctx.drawImage(source, 0, 0, t.width, t.height);\n return;\n }\n\n ctx.save();\n if (t.rotate === 90) {\n ctx.translate(t.width, 0);\n ctx.rotate(Math.PI / 2);\n } else {\n ctx.translate(0, t.height);\n ctx.rotate(-Math.PI / 2);\n }\n // The source is drawn at its natural (unrotated) extents, which are the\n // output dimensions swapped back.\n ctx.drawImage(source, 0, 0, t.height, t.width);\n ctx.restore();\n}\n\nfunction getScreenAngle(): number {\n const orientation =\n typeof screen !== \"undefined\" ? screen.orientation : undefined;\n return orientation && typeof orientation.angle === \"number\"\n ? orientation.angle\n : 0;\n}\n\nexport function waitForVideoDimensions(video: HTMLVideoElement): Promise<void> {\n return new Promise<void>((resolve) => {\n if (video.videoWidth > 0) {\n resolve();\n return;\n }\n video.addEventListener(\"loadedmetadata\", () => resolve(), { once: true });\n });\n}\n\nexport async function playSilently(video: HTMLVideoElement): Promise<void> {\n try {\n await video.play();\n } catch {\n // Autoplay can reject without a gesture; the element still produces frames\n // for the canvas, so this is non-fatal.\n }\n}\n\n/**\n * Build a canvas-backed, orientation-normalised copy of a display-capture\n * stream. Returns null when the browser can't support the canvas pipeline, in\n * which case the caller should record the original stream directly.\n */\nexport async function buildOrientationCorrectedStream(\n displayStream: MediaStream,\n frameRate = 15\n): Promise<OrientationCorrectedStream | null> {\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n\n const transform = computeOrientationTransform(\n video.videoWidth,\n video.videoHeight,\n window.innerWidth,\n window.innerHeight,\n getScreenAngle()\n );\n canvas.width = transform.width;\n canvas.height = transform.height;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRotatedFrame(ctx, video, transform);\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Live video redaction.\n *\n * Blurs the on-screen bounding boxes of chosen elements while the current tab\n * is recorded. Every frame re-reads each element's `getBoundingClientRect()`,\n * so the blur tracks the content as the page scrolls or reflows — no pixel\n * tracking and no post-recording re-encode. The captured frame is the tab's\n * viewport (video-capture restricts capture to the current tab), so viewport\n * CSS coordinates map onto the frame with a single scale factor.\n *\n * The display stream is drawn to a canvas, the tracked regions are blurred on\n * top, and MediaRecorder records the canvas stream instead of the raw capture —\n * mirroring the iOS orientation pipeline in `video-orientation.ts`.\n */\n\nimport { waitForVideoDimensions, playSilently } from \"./video-orientation\";\n\nexport interface RedactedStream {\n /** Canvas-sourced stream to hand to MediaRecorder. */\n stream: MediaStream;\n /** Stop drawing and release the canvas stream + source video. */\n stop: () => void;\n}\n\nexport interface RedactionOptions {\n /** Output frame rate of the redacted stream. Default: 15. */\n frameRate?: number;\n /** Gaussian blur radius in px applied to each region. Default: 12. */\n blurRadius?: number;\n}\n\ninterface Box {\n x: number;\n y: number;\n w: number;\n h: number;\n}\n\n/**\n * Map a viewport rect (CSS px, from `getBoundingClientRect()`) onto the\n * captured frame, clamped to the frame bounds. Returns null when the rect is\n * fully outside the viewport (the element scrolled away) or has no area.\n */\nexport function mapRectToFrame(\n rect: { left: number; top: number; width: number; height: number },\n scaleX: number,\n scaleY: number,\n frameW: number,\n frameH: number\n): Box | null {\n const x = Math.max(0, Math.min(frameW, rect.left * scaleX));\n const y = Math.max(0, Math.min(frameH, rect.top * scaleY));\n const right = Math.max(0, Math.min(frameW, (rect.left + rect.width) * scaleX));\n const bottom = Math.max(0, Math.min(frameH, (rect.top + rect.height) * scaleY));\n const w = right - x;\n const h = bottom - y;\n if (w <= 0 || h <= 0) return null;\n return { x, y, w, h };\n}\n\n/**\n * Draw one frame: the full capture, then each element's live bounding box\n * blurred on top. Exported for unit testing the per-frame logic without a real\n * capture stream. When the 2D context can't apply a blur filter, the region is\n * filled solid instead so sensitive content is still hidden.\n */\nexport function drawRedactedFrame(\n ctx: CanvasRenderingContext2D,\n video: CanvasImageSource,\n elements: Element[],\n frameW: number,\n frameH: number,\n blurRadius: number,\n supportsFilter: boolean\n): void {\n ctx.drawImage(video, 0, 0, frameW, frameH);\n\n // The current-tab capture is the viewport, so this scale is ~devicePixelRatio;\n // deriving it live also absorbs viewport resizes mid-recording.\n const scaleX = frameW / Math.max(1, window.innerWidth);\n const scaleY = frameH / Math.max(1, window.innerHeight);\n\n for (const el of elements) {\n const box = mapRectToFrame(\n el.getBoundingClientRect(),\n scaleX,\n scaleY,\n frameW,\n frameH\n );\n if (!box) continue; // scrolled out of view\n\n ctx.save();\n if (supportsFilter) {\n ctx.filter = `blur(${blurRadius}px)`;\n // Re-sample just this region from the live frame so the blur source is\n // the region itself.\n ctx.drawImage(video, box.x, box.y, box.w, box.h, box.x, box.y, box.w, box.h);\n } else {\n // No canvas filter support — hide the region with a solid fill.\n ctx.fillStyle = \"#0b0b0b\";\n ctx.fillRect(box.x, box.y, box.w, box.h);\n }\n ctx.restore();\n }\n}\n\n/**\n * Build a canvas-backed copy of a display-capture stream that blurs the live\n * bounding boxes of `elements`. Returns null when the browser can't support the\n * canvas pipeline (no `captureStream` / 2D context), in which case the caller\n * should record the original stream unredacted.\n */\nexport async function buildRedactedStream(\n displayStream: MediaStream,\n elements: Element[],\n options: RedactionOptions = {}\n): Promise<RedactedStream | null> {\n const frameRate = options.frameRate ?? 15;\n const blurRadius = options.blurRadius ?? 12;\n\n const canvas = document.createElement(\"canvas\");\n if (typeof canvas.captureStream !== \"function\") return null;\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return null;\n\n // Feature-detect canvas blur; fall back to solid fill where unsupported.\n const supportsFilter = \"filter\" in ctx;\n\n const video = document.createElement(\"video\");\n video.muted = true;\n video.playsInline = true;\n video.srcObject = displayStream;\n\n await waitForVideoDimensions(video);\n canvas.width = video.videoWidth;\n canvas.height = video.videoHeight;\n\n await playSilently(video);\n\n let rafId = 0;\n const draw = (): void => {\n drawRedactedFrame(\n ctx,\n video,\n elements,\n canvas.width,\n canvas.height,\n blurRadius,\n supportsFilter\n );\n rafId = requestAnimationFrame(draw);\n };\n draw();\n\n const stream = canvas.captureStream(frameRate);\n\n return {\n stream,\n stop: (): void => {\n if (rafId) cancelAnimationFrame(rafId);\n rafId = 0;\n stream.getTracks().forEach((t) => t.stop());\n video.pause();\n video.srcObject = null;\n },\n };\n}\n","/**\n * Video capture utilities\n * Uses getDisplayMedia + MediaRecorder for screen recording\n */\n\nimport {\n isIOSDevice,\n buildOrientationCorrectedStream,\n type OrientationCorrectedStream,\n} from \"./video-orientation\";\nimport { buildRedactedStream, type RedactedStream } from \"./video-redaction\";\n\nexport interface VideoRecorderOptions {\n /** Max recording duration in seconds. Default: 15 */\n maxDuration?: number;\n /** Video bitrate in bps. Default: 800000 (800kbps) */\n videoBitsPerSecond?: number;\n /**\n * Elements whose live bounding boxes are blurred out of the recording. The\n * blur tracks each element as the page scrolls. Empty/omitted = no redaction.\n */\n redactionElements?: Element[];\n /** Blur radius in px for redacted regions. Default: 12. */\n redactionBlurRadius?: number;\n}\n\nexport interface VideoRecorder {\n /** Start recording. Requests getDisplayMedia if not already started. */\n start(): Promise<void>;\n /** Stop recording and return the video blob. */\n stop(): Promise<Blob>;\n /** Register a callback for each second tick (receives seconds elapsed). */\n onTick(callback: (elapsed: number) => void): void;\n /** Register a callback for when recording stops (e.g., browser stop button, max duration). */\n onStop(callback: (blob: Blob | null) => void): void;\n /** Clean up all resources. */\n destroy(): void;\n}\n\n/**\n * Check if video recording is supported in this browser\n */\nexport function isVideoRecordingSupported(): boolean {\n return (\n typeof navigator !== \"undefined\" &&\n !!navigator.mediaDevices &&\n typeof navigator.mediaDevices.getDisplayMedia === \"function\" &&\n typeof MediaRecorder !== \"undefined\"\n );\n}\n\n/**\n * Detect the best supported MIME type for recording\n */\nexport function getSupportedMimeType(): string {\n if (typeof MediaRecorder === \"undefined\") return \"\";\n\n const candidates = [\n \"video/webm;codecs=vp9\",\n \"video/webm;codecs=vp8\",\n \"video/webm\",\n \"video/mp4\",\n ];\n\n for (const type of candidates) {\n if (MediaRecorder.isTypeSupported(type)) {\n return type;\n }\n }\n\n return \"\";\n}\n\n/**\n * Create a video recorder that captures the screen\n */\nexport function createVideoRecorder(options: VideoRecorderOptions = {}): VideoRecorder {\n const maxDuration = options.maxDuration ?? 15;\n const videoBitsPerSecond = options.videoBitsPerSecond ?? 800_000;\n const redactionElements = options.redactionElements ?? [];\n const redactionBlurRadius = options.redactionBlurRadius;\n\n let stream: MediaStream | null = null;\n let orientationCorrection: OrientationCorrectedStream | null = null;\n let redaction: RedactedStream | null = null;\n let recorder: MediaRecorder | null = null;\n let chunks: Blob[] = [];\n let tickCallback: ((elapsed: number) => void) | null = null;\n let stopCallback: ((blob: Blob | null) => void) | null = null;\n let tickInterval: ReturnType<typeof setInterval> | null = null;\n let startTime = 0;\n let resolveStop: ((blob: Blob) => void) | null = null;\n let rejectStop: ((err: Error) => void) | null = null;\n let stopped = false;\n let autoStopTimeout: ReturnType<typeof setTimeout> | null = null;\n\n function cleanup(): void {\n if (tickInterval !== null) {\n clearInterval(tickInterval);\n tickInterval = null;\n }\n if (autoStopTimeout !== null) {\n clearTimeout(autoStopTimeout);\n autoStopTimeout = null;\n }\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n }\n recorder = null;\n chunks = [];\n tickCallback = null;\n stopCallback = null;\n resolveStop = null;\n rejectStop = null;\n }\n\n function finishRecording(): void {\n if (stopped) return;\n stopped = true;\n\n if (recorder && recorder.state !== \"inactive\") {\n recorder.stop();\n }\n }\n\n return {\n async start(): Promise<void> {\n stopped = false;\n chunks = [];\n\n const mimeType = getSupportedMimeType();\n if (!mimeType) {\n throw new Error(\"No supported video MIME type found\");\n }\n\n // Request screen capture, restricted to the current tab (the page the\n // embed is running on). `preferCurrentTab` collapses the Chromium picker\n // to a single \"share this tab\" choice; the other hints drop window and\n // whole-screen options where the browser honours them. These keys are\n // not in the DOM types yet.\n stream = await navigator.mediaDevices.getDisplayMedia({\n video: {\n frameRate: 15,\n displaySurface: \"browser\",\n },\n audio: false,\n // @ts-expect-error preferCurrentTab is not in the TS types yet\n preferCurrentTab: true,\n selfBrowserSurface: \"include\",\n monitorTypeSurfaces: \"exclude\",\n surfaceSwitching: \"exclude\",\n });\n\n // Enforce tab-only capture. In browsers that ignore the hints above and\n // still let the user share a window or the whole screen, reject it — qaid\n // records only the tab it is embedded in, never other windows or the\n // desktop.\n const videoTrack = stream.getVideoTracks()[0];\n const surface = videoTrack?.getSettings?.()?.displaySurface;\n if (surface === \"monitor\" || surface === \"window\") {\n stream.getTracks().forEach((t) => t.stop());\n stream = null;\n throw new Error(\n \"qaid records only the current tab — please share this tab, not a window or your whole screen.\",\n );\n }\n\n // Handle browser \"Stop sharing\" button\n if (videoTrack) {\n videoTrack.addEventListener(\"ended\", () => {\n finishRecording();\n });\n }\n\n // Route the raw capture through a canvas pipeline when needed. On\n // iOS/iPadOS the captured file is rotated by the OS, so re-encode the live\n // frames upright. Otherwise, if the user marked regions to redact, blur\n // their live bounding boxes into the recording. Both fall back to the raw\n // stream if the canvas pipeline isn't available.\n let recordStream: MediaStream = stream;\n if (isIOSDevice()) {\n const corrected = await buildOrientationCorrectedStream(stream);\n if (corrected) {\n orientationCorrection = corrected;\n recordStream = corrected.stream;\n }\n } else if (redactionElements.length > 0) {\n const redacted = await buildRedactedStream(stream, redactionElements, {\n blurRadius: redactionBlurRadius,\n });\n if (redacted) {\n redaction = redacted;\n recordStream = redacted.stream;\n }\n }\n\n recorder = new MediaRecorder(recordStream, {\n mimeType,\n videoBitsPerSecond,\n });\n\n recorder.ondataavailable = (e: BlobEvent) => {\n if (e.data.size > 0) {\n chunks.push(e.data);\n }\n };\n\n recorder.onstop = () => {\n const blob = new Blob(chunks, { type: mimeType });\n if (resolveStop) {\n resolveStop(blob);\n }\n // Notify via callback (for unexpected stops like browser stop button)\n if (stopCallback) {\n stopCallback(blob);\n }\n // Stop the canvas pipeline (iOS orientation / redaction) and the\n // display stream tracks.\n if (orientationCorrection) {\n orientationCorrection.stop();\n orientationCorrection = null;\n }\n if (redaction) {\n redaction.stop();\n redaction = null;\n }\n if (stream) {\n stream.getTracks().forEach((t) => t.stop());\n }\n };\n\n recorder.onerror = () => {\n if (rejectStop) {\n rejectStop(new Error(\"MediaRecorder error\"));\n }\n };\n\n recorder.start(1000); // collect data every second\n startTime = Date.now();\n\n // Start tick interval\n tickInterval = setInterval(() => {\n const elapsed = Math.floor((Date.now() - startTime) / 1000);\n if (tickCallback) {\n tickCallback(elapsed);\n }\n }, 1000);\n\n // Auto-stop at max duration\n autoStopTimeout = setTimeout(() => {\n finishRecording();\n }, maxDuration * 1000);\n },\n\n stop(): Promise<Blob> {\n return new Promise<Blob>((resolve, reject) => {\n resolveStop = resolve;\n rejectStop = reject;\n finishRecording();\n });\n },\n\n onTick(callback: (elapsed: number) => void): void {\n tickCallback = callback;\n },\n\n onStop(callback: (blob: Blob | null) => void): void {\n stopCallback = callback;\n },\n\n destroy(): void {\n finishRecording();\n cleanup();\n },\n };\n}\n"],"names":["isIOSDevice","ua","computeOrientationTransform","frameW","frameH","viewportW","viewportH","screenAngle","frameLandscape","viewportLandscape","drawRotatedFrame","ctx","source","t","getScreenAngle","orientation","waitForVideoDimensions","video","resolve","playSilently","buildOrientationCorrectedStream","displayStream","frameRate","canvas","transform","rafId","draw","stream","mapRectToFrame","rect","scaleX","scaleY","x","y","right","bottom","w","h","drawRedactedFrame","elements","blurRadius","supportsFilter","el","box","buildRedactedStream","options","isVideoRecordingSupported","getSupportedMimeType","candidates","type","createVideoRecorder","maxDuration","videoBitsPerSecond","redactionElements","redactionBlurRadius","orientationCorrection","redaction","recorder","chunks","tickCallback","stopCallback","tickInterval","startTime","resolveStop","rejectStop","stopped","autoStopTimeout","cleanup","finishRecording","mimeType","videoTrack","surface","recordStream","corrected","redacted","e","blob","elapsed","reject","callback"],"mappings":"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"}
|