@guidelytech/guide-runtime 0.5.0 → 0.6.2

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.
@@ -0,0 +1,857 @@
1
+ import {
2
+ describeElement,
3
+ isGuidelyElement,
4
+ normalizeText
5
+ } from "./chunk-GUSKGNNR.js";
6
+ import {
7
+ createScope
8
+ } from "./chunk-7DOGZKWD.js";
9
+
10
+ // src/capture-styles.ts
11
+ var CAPTURE_STYLES = `
12
+ .g-cap{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);z-index:calc(var(--g-z) + 10);pointer-events:auto;display:flex;flex-direction:column;align-items:center;gap:6px;max-width:calc(100vw - 32px);font:500 13px/1.4 var(--g-font);color:#fff}
13
+ .g-cap-row{display:flex;align-items:center;flex-wrap:wrap;gap:8px;padding:6px 6px 6px 14px;background:#111827;border-radius:999px;box-shadow:0 6px 24px rgba(0,0,0,.28)}
14
+ .g-cap-dot{position:relative;flex:none;width:10px;height:10px;border-radius:999px;background:#9ca3af}
15
+ .g-cap[data-state="recording"] .g-cap-dot{background:#ef4444}
16
+ .g-cap[data-state="recording"] .g-cap-dot::after{content:"";position:absolute;inset:0;border-radius:inherit;background:#ef4444;animation:g-pulse 1.4s ease-out infinite}
17
+ .g-cap-status{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:min(340px,60vw)}
18
+ .g-cap .g-btn{min-height:28px;padding:3px 12px;font-size:13px;border-radius:999px}
19
+ .g-cap .g-btn-secondary{background:rgba(255,255,255,.14);color:#fff;border-color:transparent}
20
+ .g-cap .g-btn-secondary:hover{background:rgba(255,255,255,.22)}
21
+ .g-cap .g-btn-ghost{color:#d1d5db}
22
+ .g-cap .g-btn-ghost:hover{background:rgba(255,255,255,.1);color:#fff}
23
+ .g-cap .g-cap-warn{background:#f59e0b;color:#111827}
24
+ .g-cap-note{max-width:min(420px,calc(100vw - 32px));padding:6px 12px;border-radius:10px;background:#7c2d12;box-shadow:0 6px 24px rgba(0,0,0,.28)}
25
+ .g-cap-count{display:none;font-variant-numeric:tabular-nums}
26
+ .g-cap[data-compact] .g-cap-row{gap:6px;padding:5px 10px;opacity:.85}
27
+ .g-cap[data-compact] .g-cap-row>:not(.g-cap-dot):not(.g-cap-count):not(.g-cap-warn){display:none}
28
+ .g-cap[data-compact] .g-cap-count{display:inline}
29
+ .g-cap[data-captured] .g-cap-dot,.g-cap[data-captured] .g-cap-dot::after{background:#22c55e}
30
+ `;
31
+
32
+ // src/capture-dom.ts
33
+ var CAPTURE_INTERACTIVE_SELECTOR = "a,button,input,select,textarea,label,summary,[role=button],[role=link],[role=tab],[role=menuitem],[role=menuitemcheckbox],[role=menuitemradio],[role=option],[role=checkbox],[role=radio],[role=switch],[role=combobox],[contenteditable]:not([contenteditable=false])";
34
+ var REDACT_SELECTOR = "[data-guidely-redact]";
35
+ var TEXT_INPUT_TYPES = /* @__PURE__ */ new Set(["", "text", "email", "search", "tel", "url", "number", "password", "date", "datetime-local", "month", "week", "time"]);
36
+ function closestSafe(el, selector) {
37
+ if (!selector) return null;
38
+ try {
39
+ return el.closest(selector);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function inputType(el) {
45
+ return el.tagName === "INPUT" ? (el.getAttribute("type") || "").toLowerCase() : "";
46
+ }
47
+ function isEditableHost(el) {
48
+ const v = el.getAttribute("contenteditable");
49
+ return v != null && v.toLowerCase() !== "false";
50
+ }
51
+ function isTextEntry(el) {
52
+ if (el.tagName === "TEXTAREA") return true;
53
+ if (el.tagName === "INPUT") return TEXT_INPUT_TYPES.has(inputType(el));
54
+ return isEditableHost(el) || !!closestSafe(el, "[contenteditable]:not([contenteditable=false])");
55
+ }
56
+ function actionTarget(target) {
57
+ const hit = closestSafe(target, CAPTURE_INTERACTIVE_SELECTOR) ?? target;
58
+ if (!isEditableHost(hit) && isTextEntry(hit) && hit.tagName !== "INPUT" && hit.tagName !== "TEXTAREA") return closestSafe(hit, "[contenteditable]:not([contenteditable=false])") ?? hit;
59
+ return hit;
60
+ }
61
+ function labelControl(el) {
62
+ if (el.tagName !== "LABEL") return null;
63
+ const label = el;
64
+ if (label.control) return label.control;
65
+ const forId = label.getAttribute("for");
66
+ if (forId) return label.ownerDocument.getElementById(forId);
67
+ return label.querySelector("input,select,textarea");
68
+ }
69
+ function isMaskedElement(el, maskSelector) {
70
+ return !!closestSafe(el, REDACT_SELECTOR) || !!closestSafe(el, maskSelector);
71
+ }
72
+ var SENSITIVE_NAME = /pass|card|cvv|cvc|ssn|secret|token|otp/i;
73
+ var SENSITIVE_AUTOCOMPLETE = /cc-|one-time-code|current-password|new-password/i;
74
+ function isSensitiveField(el, maskSelector = "") {
75
+ if (inputType(el) === "password") return true;
76
+ if (SENSITIVE_AUTOCOMPLETE.test(el.getAttribute("autocomplete") || "")) return true;
77
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName) && (SENSITIVE_NAME.test(el.getAttribute("name") || "") || SENSITIVE_NAME.test(el.id || ""))) return true;
78
+ return isMaskedElement(el, maskSelector);
79
+ }
80
+ var EMAIL = /[A-Z0-9._%+-]+@[A-Z0-9-]+(?:\.[A-Z0-9-]+)*\.[A-Z]{2,}/gi;
81
+ var NUMBER_RUN = /(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,4}\)[ .-]?)?\d+(?:[ .-]\d+)*/g;
82
+ function classifyNumber(run) {
83
+ const groups = run.match(/\d+/g) ?? [];
84
+ const lens = groups.map((g) => g.length);
85
+ const digits = lens.reduce((a, b) => a + b, 0);
86
+ const special = /[+(.]/.test(run);
87
+ if (lens.length === 3 && lens[0] === 3 && lens[1] === 2 && lens[2] === 4 && /^\d{3}[ -]\d{2}[ -]\d{4}$/.test(run)) return "ssn";
88
+ if (digits >= 13 && digits <= 19 && !special && lens.every((l) => l >= 4)) return "card";
89
+ if (digits >= 10 && digits <= 13 && (/[+(]/.test(run) || lens.length >= 3 && lens[lens.length - 1] === 4 && lens.every((l) => l >= 2) && !run.includes("."))) return "phone";
90
+ return null;
91
+ }
92
+ function findSensitiveText(text) {
93
+ const out = [];
94
+ if (text.length < 7) return out;
95
+ if (text.includes("@")) {
96
+ EMAIL.lastIndex = 0;
97
+ for (let m = EMAIL.exec(text); m && out.length < 200; m = EMAIL.exec(text)) out.push([m.index, m.index + m[0].length]);
98
+ }
99
+ if (/\d{3}/.test(text)) {
100
+ NUMBER_RUN.lastIndex = 0;
101
+ for (let m = NUMBER_RUN.exec(text); m && out.length < 200; m = NUMBER_RUN.exec(text)) {
102
+ if (m[0].length >= 9 && classifyNumber(m[0])) out.push([m.index, m.index + m[0].length]);
103
+ }
104
+ }
105
+ return out;
106
+ }
107
+ function looksLikeSecretValue(value) {
108
+ NUMBER_RUN.lastIndex = 0;
109
+ for (let m = NUMBER_RUN.exec(value); m; m = NUMBER_RUN.exec(value)) {
110
+ const kind = classifyNumber(m[0]);
111
+ if (kind === "card" || kind === "ssn") return true;
112
+ }
113
+ return false;
114
+ }
115
+ var ROLE_MAP = {
116
+ button: "button",
117
+ link: "link",
118
+ tab: "tab",
119
+ menuitem: "menuitem",
120
+ menuitemcheckbox: "menuitem",
121
+ menuitemradio: "menuitem",
122
+ checkbox: "checkbox",
123
+ radio: "radio",
124
+ switch: "switch",
125
+ option: "option",
126
+ combobox: "dropdown",
127
+ listbox: "dropdown",
128
+ textbox: "field",
129
+ searchbox: "field",
130
+ spinbutton: "field"
131
+ };
132
+ function captureRole(el) {
133
+ const explicit = (el.getAttribute("role") || "").trim().split(/\s+/)[0].toLowerCase();
134
+ if (ROLE_MAP[explicit]) return ROLE_MAP[explicit];
135
+ const tag = el.tagName.toLowerCase();
136
+ if (tag === "a") return el.hasAttribute("href") ? "link" : "element";
137
+ if (tag === "button" || tag === "summary") return "button";
138
+ if (tag === "select") return "dropdown";
139
+ if (tag === "option") return "option";
140
+ if (tag === "textarea") return "field";
141
+ if (tag === "input") {
142
+ const t = inputType(el);
143
+ if (t === "checkbox") return "checkbox";
144
+ if (t === "radio") return "radio";
145
+ if (t === "button" || t === "submit" || t === "reset" || t === "image") return "button";
146
+ return "field";
147
+ }
148
+ if (isEditableHost(el)) return "field";
149
+ return "element";
150
+ }
151
+ var clip = (s, max = 80) => s.length > max ? `${s.slice(0, max - 1).trimEnd()}\u2026` : s;
152
+ function textOf(el) {
153
+ return normalizeText(el.innerText ?? el.textContent);
154
+ }
155
+ function labelledBy(el) {
156
+ const ids = (el.getAttribute("aria-labelledby") || "").split(/\s+/).filter(Boolean);
157
+ return normalizeText(ids.map((id) => el.ownerDocument.getElementById(id)).filter((x) => !!x).map(textOf).join(" "));
158
+ }
159
+ function associatedLabel(el) {
160
+ const labels = el.labels;
161
+ if (labels && labels.length) return normalizeText(Array.from(labels).map(textOf).join(" "));
162
+ if (el.id) {
163
+ try {
164
+ const l = el.ownerDocument.querySelector(`label[for="${el.id.replace(/["\\]/g, "\\$&")}"]`);
165
+ if (l) return textOf(l);
166
+ } catch {
167
+ }
168
+ }
169
+ const wrap = el.parentElement?.closest("label");
170
+ return wrap ? textOf(wrap) : "";
171
+ }
172
+ function captureLabel(el) {
173
+ const attr = (name) => normalizeText(el.getAttribute(name));
174
+ const tag = el.tagName;
175
+ const field = tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || isEditableHost(el);
176
+ const candidates = [
177
+ () => attr("aria-label"),
178
+ () => labelledBy(el),
179
+ () => /^(INPUT|TEXTAREA|SELECT|METER|PROGRESS|OUTPUT)$/.test(tag) ? associatedLabel(el) : "",
180
+ () => tag === "INPUT" && /^(button|submit|reset)$/.test(inputType(el)) ? normalizeText(el.value) : "",
181
+ () => field ? "" : textOf(el),
182
+ () => attr("placeholder"),
183
+ () => attr("alt"),
184
+ () => normalizeText(el.querySelector?.("img[alt]")?.getAttribute("alt")),
185
+ () => attr("title"),
186
+ () => normalizeText(el.querySelector?.("svg title")?.textContent),
187
+ () => attr("name")
188
+ ];
189
+ for (const c of candidates) {
190
+ const v = c();
191
+ if (v) return clip(v);
192
+ }
193
+ return "";
194
+ }
195
+ function describeCapturedElement(el, maskSelector = "") {
196
+ return { label: isMaskedElement(el, maskSelector) ? "" : captureLabel(el), role: captureRole(el), tag: el.tagName.toLowerCase() };
197
+ }
198
+ function fieldValue(el) {
199
+ const raw = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el.value : textOf(el);
200
+ return (raw || "").trim().slice(0, 200);
201
+ }
202
+ function selectedOptionText(el) {
203
+ return normalizeText(Array.from(el.selectedOptions ?? []).map((o) => o.text || o.value).join(", ")).slice(0, 200);
204
+ }
205
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
206
+ var round = (n) => Math.round(n * 1e4) / 1e4;
207
+ function normalizeRect(rect, viewport, pad = 4) {
208
+ if (!viewport.width || !viewport.height || rect.width <= 0 || rect.height <= 0) return null;
209
+ const x0 = clamp01((rect.x - pad) / viewport.width);
210
+ const y0 = clamp01((rect.y - pad) / viewport.height);
211
+ const x1 = clamp01((rect.x + rect.width + pad) / viewport.width);
212
+ const y1 = clamp01((rect.y + rect.height + pad) / viewport.height);
213
+ if (x1 <= x0 || y1 <= y0) return null;
214
+ return { x: round(x0), y: round(y0), width: round(x1 - x0), height: round(y1 - y0) };
215
+ }
216
+ function toFrameRect(rect, scaleX, scaleY, frame, pad = 0) {
217
+ const x0 = Math.max(0, Math.floor((rect.x - pad) * scaleX));
218
+ const y0 = Math.max(0, Math.floor((rect.y - pad) * scaleY));
219
+ const x1 = Math.min(frame.width, Math.ceil((rect.x + rect.width + pad) * scaleX));
220
+ const y1 = Math.min(frame.height, Math.ceil((rect.y + rect.height + pad) * scaleY));
221
+ return x1 > x0 && y1 > y0 ? { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } : null;
222
+ }
223
+ function intersectsViewport(r, vw, vh) {
224
+ return r.width > 0 && r.height > 0 && r.x < vw && r.y < vh && r.x + r.width > 0 && r.y + r.height > 0;
225
+ }
226
+ var SKIP_TEXT_PARENTS = /^(SCRIPT|STYLE|NOSCRIPT|TEMPLATE|TEXTAREA|OPTION)$/;
227
+ function collectRedactions(doc, options = {}) {
228
+ const win = doc.defaultView;
229
+ const vw = win?.innerWidth ?? 0;
230
+ const vh = win?.innerHeight ?? 0;
231
+ const out = [];
232
+ const push = (r) => {
233
+ if (intersectsViewport(r, vw, vh)) out.push({ x: r.x, y: r.y, width: r.width, height: r.height });
234
+ };
235
+ const root = doc.body;
236
+ if (!root) return out;
237
+ const mask = options.maskSelector || "";
238
+ const blocks = /* @__PURE__ */ new Set();
239
+ try {
240
+ root.querySelectorAll(mask ? `${REDACT_SELECTOR},${mask}` : REDACT_SELECTOR).forEach((el) => blocks.add(el));
241
+ } catch {
242
+ root.querySelectorAll(REDACT_SELECTOR).forEach((el) => blocks.add(el));
243
+ }
244
+ root.querySelectorAll("input,textarea,select,[contenteditable]:not([contenteditable=false])").forEach((el) => {
245
+ if (blocks.has(el)) return;
246
+ const t = inputType(el);
247
+ if (t === "hidden" || t === "checkbox" || t === "radio" || t === "button" || t === "submit" || t === "reset") return;
248
+ if (isSensitiveField(el, mask)) blocks.add(el);
249
+ else if ((el.tagName === "INPUT" || el.tagName === "TEXTAREA") && findSensitiveText(el.value || "").length) blocks.add(el);
250
+ });
251
+ blocks.forEach((el) => push(el.getBoundingClientRect()));
252
+ const max = options.maxTextNodes ?? 5e3;
253
+ const walker = doc.createTreeWalker(
254
+ root,
255
+ 4
256
+ /* NodeFilter.SHOW_TEXT */
257
+ );
258
+ let range = null;
259
+ let seen = 0;
260
+ for (let node = walker.nextNode(); node && seen < max; node = walker.nextNode()) {
261
+ const text = node.nodeValue || "";
262
+ if (text.length < 7) continue;
263
+ seen++;
264
+ const parent = node.parentElement;
265
+ if (!parent || SKIP_TEXT_PARENTS.test(parent.tagName)) continue;
266
+ const hits = findSensitiveText(text);
267
+ if (!hits.length) continue;
268
+ if (Array.from(blocks).some((b) => b.contains(parent))) continue;
269
+ range ?? (range = doc.createRange());
270
+ for (const [start, end] of hits) {
271
+ try {
272
+ range.setStart(node, start);
273
+ range.setEnd(node, end);
274
+ Array.from(range.getClientRects()).forEach(push);
275
+ } catch {
276
+ }
277
+ }
278
+ }
279
+ return out;
280
+ }
281
+
282
+ // src/capture-screen.ts
283
+ function canCaptureScreen() {
284
+ return typeof navigator !== "undefined" && !!navigator.mediaDevices && typeof navigator.mediaDevices.getDisplayMedia === "function";
285
+ }
286
+ async function requestTabCapture(container, doc = document) {
287
+ if (!canCaptureScreen()) return { ok: false, reason: "unsupported" };
288
+ let stream;
289
+ try {
290
+ stream = await navigator.mediaDevices.getDisplayMedia({
291
+ video: { displaySurface: "browser", frameRate: 5 },
292
+ audio: false,
293
+ preferCurrentTab: true,
294
+ selfBrowserSurface: "include",
295
+ surfaceSwitching: "exclude"
296
+ });
297
+ } catch (err) {
298
+ return { ok: false, reason: err?.name === "NotAllowedError" ? "denied" : "error" };
299
+ }
300
+ const track = stream.getVideoTracks()[0];
301
+ if (!track) {
302
+ stream.getTracks().forEach((t) => t.stop());
303
+ return { ok: false, reason: "error" };
304
+ }
305
+ const surface = track.getSettings?.()?.displaySurface;
306
+ if (surface && surface !== "browser") {
307
+ stream.getTracks().forEach((t) => t.stop());
308
+ return { ok: false, reason: "wrong-surface" };
309
+ }
310
+ const video = doc.createElement("video");
311
+ video.muted = true;
312
+ video.playsInline = true;
313
+ video.setAttribute("aria-hidden", "true");
314
+ video.style.cssText = "position:fixed;left:0;top:0;width:2px;height:2px;opacity:0;pointer-events:none";
315
+ try {
316
+ video.srcObject = stream;
317
+ container.appendChild(video);
318
+ } catch {
319
+ stream.getTracks().forEach((t) => t.stop());
320
+ return { ok: false, reason: "error" };
321
+ }
322
+ try {
323
+ await video.play();
324
+ if (!video.videoWidth) await new Promise((resolve) => {
325
+ const done = () => resolve();
326
+ video.addEventListener("loadeddata", done, { once: true });
327
+ setTimeout(done, 1500);
328
+ });
329
+ } catch {
330
+ }
331
+ let ended = false;
332
+ const listeners = [];
333
+ const fv = video;
334
+ const rvfc = typeof fv.requestVideoFrameCallback === "function";
335
+ let frameAt = 0;
336
+ let waiters = [];
337
+ const onFrame = (now) => {
338
+ frameAt = now;
339
+ waiters.splice(0).forEach((w) => w());
340
+ if (!ended) fv.requestVideoFrameCallback(onFrame);
341
+ };
342
+ if (rvfc) fv.requestVideoFrameCallback(onFrame);
343
+ const end = () => {
344
+ if (ended) return;
345
+ ended = true;
346
+ stream.getTracks().forEach((t) => t.stop());
347
+ video.srcObject = null;
348
+ video.remove();
349
+ listeners.splice(0).forEach((cb) => cb());
350
+ };
351
+ track.addEventListener("ended", end);
352
+ return {
353
+ ok: true,
354
+ screen: {
355
+ live: () => !ended && track.readyState === "live",
356
+ onEnded: (cb) => ended ? cb() : listeners.push(cb),
357
+ grab: () => ended ? null : grabFrame(video, doc),
358
+ freshSince: (changedAt) => rvfc ? frameAt > changedAt : performance.now() - changedAt > 250,
359
+ nextFrame: (timeoutMs = 500) => new Promise((resolve) => {
360
+ const t = setTimeout(done, rvfc ? timeoutMs : 250);
361
+ function done() {
362
+ clearTimeout(t);
363
+ waiters = waiters.filter((w) => w !== done);
364
+ resolve();
365
+ }
366
+ if (rvfc) waiters.push(done);
367
+ }),
368
+ stop: () => {
369
+ listeners.length = 0;
370
+ end();
371
+ }
372
+ }
373
+ };
374
+ }
375
+ function grabFrame(video, doc = document) {
376
+ const w = video.videoWidth;
377
+ const h = video.videoHeight;
378
+ if (!w || !h) return null;
379
+ const canvas = doc.createElement("canvas");
380
+ canvas.width = w;
381
+ canvas.height = h;
382
+ const ctx = canvas.getContext("2d");
383
+ if (!ctx) return null;
384
+ ctx.drawImage(video, 0, 0, w, h);
385
+ return canvas;
386
+ }
387
+ function redactCanvas(canvas, rects, viewport) {
388
+ const ctx = canvas.getContext("2d");
389
+ if (!ctx || !rects.length || !viewport.width || !viewport.height) return 0;
390
+ const sx = canvas.width / viewport.width;
391
+ const sy = canvas.height / viewport.height;
392
+ const tmp = canvas.ownerDocument.createElement("canvas");
393
+ const tctx = tmp.getContext("2d");
394
+ if (!tctx) return 0;
395
+ let count = 0;
396
+ for (const rect of rects) {
397
+ const r = toFrameRect(rect, sx, sy, canvas, 4);
398
+ if (!r) continue;
399
+ const block = Math.max(16 * sy, Math.min(r.height, 48 * sy));
400
+ tmp.width = Math.max(1, Math.round(r.width / block));
401
+ tmp.height = Math.max(1, Math.round(r.height / block));
402
+ tctx.imageSmoothingEnabled = true;
403
+ tctx.drawImage(canvas, r.x, r.y, r.width, r.height, 0, 0, tmp.width, tmp.height);
404
+ ctx.imageSmoothingEnabled = true;
405
+ ctx.drawImage(tmp, 0, 0, tmp.width, tmp.height, r.x, r.y, r.width, r.height);
406
+ ctx.fillStyle = "rgba(120,120,120,.28)";
407
+ ctx.fillRect(r.x, r.y, r.width, r.height);
408
+ count++;
409
+ }
410
+ return count;
411
+ }
412
+ function scaled(canvas, maxWidth) {
413
+ if (canvas.width <= maxWidth) return canvas;
414
+ const out = canvas.ownerDocument.createElement("canvas");
415
+ out.width = maxWidth;
416
+ out.height = Math.round(canvas.height * maxWidth / canvas.width);
417
+ const ctx = out.getContext("2d");
418
+ if (!ctx) return canvas;
419
+ ctx.imageSmoothingQuality = "high";
420
+ ctx.drawImage(canvas, 0, 0, out.width, out.height);
421
+ return out;
422
+ }
423
+ var toBlob = (canvas, type, quality) => new Promise((resolve) => {
424
+ try {
425
+ canvas.toBlob((b) => resolve(b), type, quality);
426
+ } catch {
427
+ resolve(null);
428
+ }
429
+ });
430
+ async function encodeScreenshot(canvas, options = {}) {
431
+ const src = scaled(canvas, options.maxWidth ?? 2560);
432
+ const maxBytes = options.maxBytes ?? 8 * 1024 * 1024;
433
+ let blob = await toBlob(src, "image/webp", 0.85);
434
+ if (!blob || blob.type !== "image/webp") blob = await toBlob(src, "image/jpeg", 0.85);
435
+ if (blob && blob.size > maxBytes) blob = await toBlob(src, "image/jpeg", 0.6);
436
+ return blob && blob.size <= maxBytes ? blob : null;
437
+ }
438
+ function screenshotDataUrl(canvas) {
439
+ try {
440
+ return scaled(canvas, 1600).toDataURL("image/jpeg", 0.7);
441
+ } catch {
442
+ return null;
443
+ }
444
+ }
445
+
446
+ // src/capture.ts
447
+ var DOUBLE_CLICK_MS = 400;
448
+ var NAV_AFTER_ACTION_MS = 1500;
449
+ var NAV_SETTLE_MS = 600;
450
+ var NOTICES = {
451
+ "wrong-surface": "Share this tab (not a window or the whole screen) so screenshots match the page.",
452
+ denied: "Screen sharing was cancelled. Start again, or record without screenshots.",
453
+ error: "Screen capture could not start. Try again, or record without screenshots.",
454
+ unsupported: "This browser cannot capture screenshots; steps are recorded without them.",
455
+ lost: "Screenshots paused: sharing stopped or the page reloaded."
456
+ };
457
+ function button(doc, text, cls, attr) {
458
+ const b = doc.createElement("button");
459
+ b.type = "button";
460
+ b.className = `g-btn ${cls}`;
461
+ b.textContent = text;
462
+ b.setAttribute(attr, "");
463
+ return b;
464
+ }
465
+ function eventTarget(e) {
466
+ const path = typeof e.composedPath === "function" ? e.composedPath() : [];
467
+ const first = path[0] ?? e.target;
468
+ if (!first) return null;
469
+ return first.nodeType === 1 ? first : first.parentElement;
470
+ }
471
+ function deepActiveElementIn(el) {
472
+ const root = el.getRootNode();
473
+ return root.activeElement ?? null;
474
+ }
475
+ function deepActiveElement(doc) {
476
+ let el = doc.activeElement;
477
+ while (el?.shadowRoot?.activeElement) el = el.shadowRoot.activeElement;
478
+ return el;
479
+ }
480
+ function startCapture(options) {
481
+ const doc = options.doc ?? document;
482
+ const win = doc.defaultView ?? window;
483
+ const mask = options.maskSelector ?? "";
484
+ const shotsAllowed = options.screenshots !== false;
485
+ const scope = createScope(null, { theme: "light", animation: "fade" }, doc);
486
+ const style = doc.createElement("style");
487
+ style.textContent = CAPTURE_STYLES;
488
+ scope.el.appendChild(style);
489
+ const bar = doc.createElement("div");
490
+ bar.className = "g-cap";
491
+ bar.setAttribute("data-guidely-capture-bar", "");
492
+ bar.setAttribute("role", "toolbar");
493
+ bar.setAttribute("aria-label", "Guidely how-to recorder");
494
+ const note = doc.createElement("div");
495
+ note.className = "g-cap-note";
496
+ note.setAttribute("data-guidely-capture-notice", "");
497
+ note.hidden = true;
498
+ const row = doc.createElement("div");
499
+ row.className = "g-cap-row";
500
+ const dot = doc.createElement("span");
501
+ dot.className = "g-cap-dot";
502
+ const status = doc.createElement("span");
503
+ status.className = "g-cap-status";
504
+ status.setAttribute("aria-live", "polite");
505
+ const count_ = doc.createElement("span");
506
+ count_.className = "g-cap-count";
507
+ count_.setAttribute("aria-hidden", "true");
508
+ const start = button(doc, "Start recording", "g-btn-primary", "data-guidely-capture-start");
509
+ const noShots = button(doc, "Record without screenshots", "g-btn-secondary", "data-guidely-capture-no-screenshots");
510
+ const resumeShots = button(doc, "Screenshots paused \u2014 Resume", "g-cap-warn", "data-guidely-capture-resume-screenshots");
511
+ const pause = button(doc, "Pause", "g-btn-secondary", "data-guidely-capture-pause");
512
+ const done = button(doc, "Done", "g-btn-primary", "data-guidely-capture-done");
513
+ const cancel = button(doc, "Cancel", "g-btn-ghost", "data-guidely-capture-cancel");
514
+ row.append(dot, count_, status, start, noShots, resumeShots, pause, done, cancel);
515
+ bar.append(note, row);
516
+ scope.el.appendChild(bar);
517
+ let phase = "ready";
518
+ let count = options.stepCount ?? 0;
519
+ let screen = null;
520
+ let wantShots = false;
521
+ let requesting = false;
522
+ let stopped = false;
523
+ let lastAction = 0;
524
+ let movedAt = -1;
525
+ let lastPress = null;
526
+ let lastUrl = win.location.href;
527
+ let navTimer = null;
528
+ let pulseTimer = null;
529
+ let typedAt = -1;
530
+ let expanded = false;
531
+ const baseline = /* @__PURE__ */ new WeakMap();
532
+ const recording = () => phase === "recording" && !stopped;
533
+ const steps = (n) => `${n} step${n === 1 ? "" : "s"}`;
534
+ const render = () => {
535
+ bar.setAttribute("data-state", phase);
536
+ bar.toggleAttribute("data-compact", phase === "recording" && !expanded);
537
+ const ready = phase === "ready";
538
+ const active = phase === "recording" || phase === "paused";
539
+ start.hidden = !ready;
540
+ start.disabled = requesting;
541
+ noShots.hidden = !ready || !shotsAllowed || !canCaptureScreen();
542
+ noShots.disabled = requesting;
543
+ resumeShots.hidden = !active || !wantShots || !!screen?.live();
544
+ resumeShots.disabled = requesting;
545
+ pause.hidden = !active;
546
+ pause.textContent = phase === "paused" ? "Resume" : "Pause";
547
+ done.hidden = !active;
548
+ cancel.hidden = phase === "saving";
549
+ if (ready) status.textContent = requesting ? "Choose this tab to share\u2026" : `Ready to record \u201C${options.title}\u201D`;
550
+ else if (phase === "saving") status.textContent = "Saving steps\u2026";
551
+ else status.textContent = `${phase === "paused" ? "Paused" : "Recording"} \xB7 ${steps(count)}`;
552
+ count_.textContent = String(count);
553
+ };
554
+ const showNotice = (text) => {
555
+ note.textContent = text ?? "";
556
+ note.hidden = !text;
557
+ };
558
+ const pulse = () => {
559
+ bar.setAttribute("data-captured", "");
560
+ if (pulseTimer) clearTimeout(pulseTimer);
561
+ pulseTimer = setTimeout(() => bar.removeAttribute("data-captured"), 700);
562
+ };
563
+ const shoot = (step, highlightEl) => {
564
+ const frame = screen?.live() ? screen.grab() : null;
565
+ if (!frame) return null;
566
+ const viewport = { width: win.innerWidth, height: win.innerHeight };
567
+ try {
568
+ step.redactions = redactCanvas(frame, collectRedactions(doc, { maskSelector: mask }), viewport);
569
+ } catch {
570
+ return null;
571
+ }
572
+ const highlight = highlightEl?.isConnected ? normalizeRect(highlightEl.getBoundingClientRect(), viewport) : null;
573
+ if (highlight) step.highlight = highlight;
574
+ else delete step.highlight;
575
+ return frame;
576
+ };
577
+ const record = (action, el, extra = {}) => {
578
+ const step = { action, url: win.location.href, pageTitle: (doc.title || "").slice(0, 300), redactions: 0, occurredAt: (/* @__PURE__ */ new Date()).toISOString() };
579
+ let highlightEl = null;
580
+ if (el) {
581
+ const masked = isMaskedElement(el, mask);
582
+ step.element = describeCapturedElement(el, mask);
583
+ step.selectors = describeElement(el).selectors.filter((s) => !masked || s.kind !== "text");
584
+ const box = extra.highlightEl?.getBoundingClientRect();
585
+ highlightEl = extra.highlightEl && box && (box.width || box.height) ? extra.highlightEl : el;
586
+ }
587
+ let frame = null;
588
+ if (screen?.live()) {
589
+ const s = screen;
590
+ const changedAt = action === "type" || action === "select" ? Math.max(movedAt, typedAt) : movedAt;
591
+ if (s.freshSince(changedAt)) frame = shoot(step, highlightEl);
592
+ else frame = s.nextFrame().then(() => shoot(step, highlightEl));
593
+ }
594
+ if (extra.value !== void 0) step.value = extra.value;
595
+ if (extra.valueRedacted) step.valueRedacted = true;
596
+ if (action !== "navigate") lastAction = Date.now();
597
+ count++;
598
+ render();
599
+ pulse();
600
+ options.onStep({ step, frame });
601
+ };
602
+ const flushField = (el) => {
603
+ if (!recording() || !isTextEntry(el)) return;
604
+ const host = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el : actionTarget(el);
605
+ const value = fieldValue(host);
606
+ if (value === (baseline.get(host) ?? "")) return;
607
+ baseline.set(host, value);
608
+ if (!value) return;
609
+ if (isSensitiveField(host, mask) || looksLikeSecretValue(value)) record("type", host, { valueRedacted: true });
610
+ else record("type", host, { value });
611
+ };
612
+ const onPointerDown = (e) => {
613
+ if (!recording() || e.button !== 0) return;
614
+ const target = eventTarget(e);
615
+ if (!target || isGuidelyElement(target)) return;
616
+ const active = deepActiveElement(doc);
617
+ if (active && active !== target && !active.contains(target) && isTextEntry(active)) flushField(active);
618
+ let el = actionTarget(target);
619
+ let highlightEl = null;
620
+ const control = labelControl(el);
621
+ if (control) {
622
+ if (isTextEntry(control) || control.tagName === "SELECT") return;
623
+ highlightEl = el;
624
+ el = control;
625
+ }
626
+ if (isTextEntry(el) || el.tagName === "SELECT" || el.closest("select")) return;
627
+ if (el.disabled) return;
628
+ const now = Date.now();
629
+ if (lastPress && lastPress.el === el && now - lastPress.at < DOUBLE_CLICK_MS) {
630
+ lastPress.at = now;
631
+ return;
632
+ }
633
+ lastPress = { el, at: now };
634
+ record("click", el, { highlightEl });
635
+ };
636
+ const onChange = (e) => {
637
+ if (!recording()) return;
638
+ const el = eventTarget(e);
639
+ if (!el || isGuidelyElement(el)) return;
640
+ if (el.tagName === "SELECT") {
641
+ const sensitive = isSensitiveField(el, mask);
642
+ const value = sensitive ? "" : selectedOptionText(el);
643
+ record("select", el, sensitive ? { valueRedacted: true } : value ? { value } : {});
644
+ return;
645
+ }
646
+ const type = el.tagName === "INPUT" ? (el.getAttribute("type") || "").toLowerCase() : "";
647
+ if (type === "checkbox" || type === "radio") {
648
+ const pressed = lastPress && Date.now() - lastPress.at < 1e3 && lastPress.el === el;
649
+ if (!pressed) record("click", el);
650
+ return;
651
+ }
652
+ if (isTextEntry(el)) flushField(el);
653
+ };
654
+ const onFocusIn = (e) => {
655
+ const el = eventTarget(e);
656
+ if (el && !isGuidelyElement(el) && isTextEntry(el)) {
657
+ const host = el.tagName === "INPUT" || el.tagName === "TEXTAREA" ? el : actionTarget(el);
658
+ baseline.set(host, fieldValue(host));
659
+ }
660
+ };
661
+ const onFocusOut = (e) => {
662
+ const el = eventTarget(e);
663
+ if (el && el.tagName !== "INPUT" && el.tagName !== "TEXTAREA" && isTextEntry(el)) flushField(el);
664
+ };
665
+ const onKeyDown = (e) => {
666
+ if (e.key !== "Enter" || e.isComposing) return;
667
+ const el = eventTarget(e);
668
+ if (el && (el.tagName === "INPUT" || (e.metaKey || e.ctrlKey) && isTextEntry(el))) flushField(el);
669
+ };
670
+ const checkUrl = () => {
671
+ const url = win.location.href;
672
+ if (url === lastUrl) return;
673
+ lastUrl = url;
674
+ if (!recording() || Date.now() - lastAction < NAV_AFTER_ACTION_MS) return;
675
+ if (navTimer) clearTimeout(navTimer);
676
+ navTimer = setTimeout(() => {
677
+ navTimer = null;
678
+ if (recording()) record("navigate", null);
679
+ }, NAV_SETTLE_MS);
680
+ };
681
+ const poll = setInterval(checkUrl, 300);
682
+ const onMove = () => {
683
+ movedAt = performance.now();
684
+ };
685
+ const onInput = (e) => {
686
+ const el = eventTarget(e);
687
+ if (el && !isGuidelyElement(el)) typedAt = performance.now();
688
+ };
689
+ const expand = (open) => {
690
+ if (expanded === open) return;
691
+ expanded = open;
692
+ render();
693
+ };
694
+ const onBarEnter = () => expand(true);
695
+ const onBarLeave = () => expand(bar.contains(deepActiveElementIn(bar)));
696
+ const onBarFocusOut = (e) => expand(!!e.relatedTarget && bar.contains(e.relatedTarget));
697
+ bar.addEventListener("pointerenter", onBarEnter);
698
+ bar.addEventListener("pointerleave", onBarLeave);
699
+ bar.addEventListener("focusin", onBarEnter);
700
+ bar.addEventListener("focusout", onBarFocusOut);
701
+ const listen = { capture: true, passive: true };
702
+ doc.addEventListener("scroll", onMove, listen);
703
+ win.addEventListener("resize", onMove);
704
+ doc.addEventListener("pointerdown", onPointerDown, listen);
705
+ doc.addEventListener("change", onChange, listen);
706
+ doc.addEventListener("input", onInput, listen);
707
+ doc.addEventListener("focusin", onFocusIn, listen);
708
+ doc.addEventListener("focusout", onFocusOut, listen);
709
+ doc.addEventListener("keydown", onKeyDown, listen);
710
+ win.addEventListener("popstate", checkUrl);
711
+ win.addEventListener("hashchange", checkUrl);
712
+ const onLost = () => {
713
+ screen = null;
714
+ if (!stopped && wantShots) showNotice(NOTICES.lost);
715
+ render();
716
+ };
717
+ const requestScreen = async () => {
718
+ requesting = true;
719
+ showNotice(null);
720
+ render();
721
+ const res = await requestTabCapture(scope.el, doc);
722
+ requesting = false;
723
+ if (stopped) {
724
+ if (res.ok) res.screen.stop();
725
+ return false;
726
+ }
727
+ if (res.ok) {
728
+ screen = res.screen;
729
+ screen.onEnded(onLost);
730
+ } else showNotice(NOTICES[res.reason]);
731
+ render();
732
+ return res.ok;
733
+ };
734
+ const begin = (shots) => {
735
+ wantShots = shots;
736
+ phase = "recording";
737
+ expanded = false;
738
+ movedAt = performance.now();
739
+ primeActive();
740
+ lastUrl = win.location.href;
741
+ render();
742
+ options.onStart?.({ screenshots: shots });
743
+ record("navigate", null);
744
+ };
745
+ start.addEventListener("click", () => {
746
+ if (requesting) return;
747
+ if (!shotsAllowed || !canCaptureScreen()) {
748
+ if (shotsAllowed) showNotice(NOTICES.unsupported);
749
+ return begin(false);
750
+ }
751
+ void requestScreen().then((ok) => {
752
+ if (ok) begin(true);
753
+ });
754
+ });
755
+ noShots.addEventListener("click", () => {
756
+ showNotice(null);
757
+ begin(false);
758
+ });
759
+ resumeShots.addEventListener("click", () => {
760
+ if (!requesting) void requestScreen();
761
+ });
762
+ pause.addEventListener("click", () => {
763
+ if (phase === "recording") {
764
+ flushActive();
765
+ phase = "paused";
766
+ } else if (phase === "paused") {
767
+ phase = "recording";
768
+ lastUrl = win.location.href;
769
+ }
770
+ render();
771
+ });
772
+ const flushActive = () => {
773
+ const active = deepActiveElement(doc);
774
+ if (active && !isGuidelyElement(active) && isTextEntry(active)) flushField(active);
775
+ };
776
+ const primeActive = () => {
777
+ const active = deepActiveElement(doc);
778
+ if (active && !isGuidelyElement(active) && isTextEntry(active)) baseline.set(active, fieldValue(active));
779
+ };
780
+ done.addEventListener("click", () => {
781
+ if (phase === "saving") return;
782
+ flushActive();
783
+ options.onFinish();
784
+ });
785
+ cancel.addEventListener("click", () => {
786
+ stop();
787
+ options.onCancel();
788
+ });
789
+ const stop = () => {
790
+ if (stopped) return;
791
+ stopped = true;
792
+ clearInterval(poll);
793
+ if (navTimer) clearTimeout(navTimer);
794
+ if (pulseTimer) clearTimeout(pulseTimer);
795
+ doc.removeEventListener("scroll", onMove, listen);
796
+ win.removeEventListener("resize", onMove);
797
+ doc.removeEventListener("pointerdown", onPointerDown, listen);
798
+ doc.removeEventListener("change", onChange, listen);
799
+ doc.removeEventListener("input", onInput, listen);
800
+ doc.removeEventListener("focusin", onFocusIn, listen);
801
+ doc.removeEventListener("focusout", onFocusOut, listen);
802
+ doc.removeEventListener("keydown", onKeyDown, listen);
803
+ win.removeEventListener("popstate", checkUrl);
804
+ win.removeEventListener("hashchange", checkUrl);
805
+ screen?.stop();
806
+ screen = null;
807
+ scope.destroy();
808
+ };
809
+ if (options.resume) {
810
+ wantShots = shotsAllowed && options.resume.screenshots && canCaptureScreen();
811
+ phase = "recording";
812
+ if (wantShots) showNotice(NOTICES.lost);
813
+ primeActive();
814
+ render();
815
+ if (options.resume.navigate) record("navigate", null);
816
+ } else render();
817
+ return {
818
+ setSaving: (saving) => {
819
+ if (stopped) return;
820
+ phase = saving ? "saving" : "recording";
821
+ render();
822
+ },
823
+ isRecording: recording,
824
+ stop
825
+ };
826
+ }
827
+ export {
828
+ CAPTURE_INTERACTIVE_SELECTOR,
829
+ DOUBLE_CLICK_MS,
830
+ NAV_AFTER_ACTION_MS,
831
+ NAV_SETTLE_MS,
832
+ REDACT_SELECTOR,
833
+ actionTarget,
834
+ canCaptureScreen,
835
+ captureLabel,
836
+ captureRole,
837
+ classifyNumber,
838
+ collectRedactions,
839
+ describeCapturedElement,
840
+ encodeScreenshot,
841
+ fieldValue,
842
+ findSensitiveText,
843
+ grabFrame,
844
+ isMaskedElement,
845
+ isSensitiveField,
846
+ isTextEntry,
847
+ labelControl,
848
+ looksLikeSecretValue,
849
+ normalizeRect,
850
+ redactCanvas,
851
+ requestTabCapture,
852
+ screenshotDataUrl,
853
+ selectedOptionText,
854
+ startCapture,
855
+ toFrameRect
856
+ };
857
+ //# sourceMappingURL=capture.js.map