@sphoro/bugcatch-web-sdk 0.13.1

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,4393 @@
1
+ var It = (t) => {
2
+ throw TypeError(t);
3
+ };
4
+ var kn = (t, e, n) => e.has(t) || It("Cannot " + n);
5
+ var At = (t, e, n) => e.has(t) ? It("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, n);
6
+ var N = (t, e, n) => (kn(t, e, "access private method"), n);
7
+ const Fe = "[REDACTED]", Sn = /* @__PURE__ */ new Set([
8
+ "auth",
9
+ "bearer",
10
+ "session",
11
+ "sid",
12
+ "phpsessid",
13
+ "otp",
14
+ "pass",
15
+ "passwd",
16
+ "pwd",
17
+ "sig",
18
+ "codeverifier"
19
+ ]), Cn = [
20
+ "token",
21
+ "secret",
22
+ "password",
23
+ "passwd",
24
+ "apikey",
25
+ "signature",
26
+ "credential",
27
+ "sessionid",
28
+ "privatekey",
29
+ "accesskey",
30
+ "authorization"
31
+ ], En = /* @__PURE__ */ new Set(["key", "code", "state", "nonce", "salt", "hash"]), Ft = [
32
+ /eyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]+/g,
33
+ // JWT
34
+ /(?:AKIA|ASIA|AROA|AIDA|AGPA|ANPA|ANVA|APKA)[0-9A-Z]{16}/g,
35
+ // AWS key id
36
+ /AIza[\w-]{35}/g,
37
+ // Google API key
38
+ /gh[pousr]_[A-Za-z0-9]{36,}/g,
39
+ // GitHub token
40
+ /github_pat_\w{22,}/g,
41
+ // GitHub fine-grained PAT
42
+ /[sprk]k_(?:live|test)_[0-9A-Za-z]{10,}/g,
43
+ // Stripe
44
+ /xox[abposr]-[A-Za-z0-9-]{10,}/g,
45
+ // Slack
46
+ /sk-ant-[\w-]{20,}/g,
47
+ // Anthropic
48
+ /sk-[\w-]{20,}/g,
49
+ // OpenAI
50
+ /glpat-[\w-]{20,}/g,
51
+ // GitLab
52
+ /shp(?:at|ss|ca|pa)_[A-Za-z0-9]{32}/g
53
+ // Shopify
54
+ ], Tn = /\bBearer\s+[\w.~+/=-]{8,}/gi, $n = /^([A-Za-z][A-Za-z0-9+.-]*:\/\/)([^/?#@]+)@/;
55
+ function Yt(t) {
56
+ try {
57
+ return decodeURIComponent(t.replace(/\+/g, " "));
58
+ } catch {
59
+ return t;
60
+ }
61
+ }
62
+ function Bn(t) {
63
+ return Yt(t).toLowerCase().replace(/[^a-z0-9]/g, "");
64
+ }
65
+ function Mn(t) {
66
+ return t.length >= 20 ? !0 : Ft.some((e) => (e.lastIndex = 0, e.test(t)));
67
+ }
68
+ function Ln(t, e) {
69
+ const n = Bn(t);
70
+ if (!n) return !1;
71
+ if (Sn.has(n)) return !0;
72
+ for (const a of Cn) if (n.includes(a)) return !0;
73
+ return En.has(n) ? Mn(Yt(e)) : !1;
74
+ }
75
+ function ot(t) {
76
+ return t.split("&").map((e) => {
77
+ const n = e.indexOf("=");
78
+ if (n === -1) return e;
79
+ const a = e.slice(0, n), o = e.slice(n + 1);
80
+ return o && Ln(a, o) ? `${a}=${Fe}` : e;
81
+ }).join("&");
82
+ }
83
+ function Zt(t) {
84
+ let e = t;
85
+ for (const n of Ft)
86
+ n.lastIndex = 0, e = e.replace(n, Fe);
87
+ return e;
88
+ }
89
+ function Xt(t) {
90
+ if (t == null) return t;
91
+ let e = String(t);
92
+ if (!e) return e;
93
+ e = e.replace($n, `$1${Fe}@`);
94
+ const n = e.indexOf("#");
95
+ let a = n === -1 ? e : e.slice(0, n), o = n === -1 ? "" : e.slice(n);
96
+ const r = a.indexOf("?");
97
+ if (r !== -1 && (a = a.slice(0, r + 1) + ot(a.slice(r + 1))), o.length > 1) {
98
+ const s = o.slice(1), l = s.indexOf("?");
99
+ l !== -1 ? o = `#${s.slice(0, l + 1)}${ot(s.slice(l + 1))}` : s.includes("=") && (o = `#${ot(s)}`);
100
+ }
101
+ return Zt(a + o);
102
+ }
103
+ function In(t) {
104
+ if (t == null) return t;
105
+ let e = String(t);
106
+ return e && (e = e.replace(/[A-Za-z][A-Za-z0-9+.-]{0,30}:\/\/[^\s"'<>)\]]+/g, (n) => Xt(n)), e = e.replace(Tn, `Bearer ${Fe}`), Zt(e));
107
+ }
108
+ function He(t) {
109
+ const e = [];
110
+ return {
111
+ push(n) {
112
+ e.push(n), e.length > t && e.splice(0, e.length - t);
113
+ },
114
+ all: () => e.slice(),
115
+ clear: () => {
116
+ e.length = 0;
117
+ },
118
+ get size() {
119
+ return e.length;
120
+ }
121
+ };
122
+ }
123
+ const Qt = ["log", "info", "warn", "error", "debug"];
124
+ function An(t, { levels: e = ["warn", "error", "log", "info"] } = {}) {
125
+ const n = {};
126
+ for (const a of Qt)
127
+ n[a] = console[a], e.includes(a) && (console[a] = (...o) => {
128
+ try {
129
+ t.push({
130
+ level: a,
131
+ text: o.map(Gt).join(" ").slice(0, 2e3),
132
+ at: (/* @__PURE__ */ new Date()).toISOString()
133
+ });
134
+ } catch {
135
+ }
136
+ n[a].apply(console, o);
137
+ });
138
+ return () => {
139
+ for (const a of Qt) console[a] = n[a];
140
+ };
141
+ }
142
+ function Qn(t, { slowMs: e = 2e3 } = {}) {
143
+ const n = window.fetch, a = XMLHttpRequest.prototype.open, o = XMLHttpRequest.prototype.send;
144
+ return n && (window.fetch = async function(...s) {
145
+ var b, d, w;
146
+ const l = performance.now(), u = typeof s[0] == "string" ? s[0] : (b = s[0]) == null ? void 0 : b.url, g = ((d = s[1]) == null ? void 0 : d.method) || (typeof s[0] == "object" ? (w = s[0]) == null ? void 0 : w.method : "GET") || "GET";
147
+ try {
148
+ const h = await n.apply(this, s);
149
+ return rt(t, { url: u, method: g, status: h.status, ms: Math.round(performance.now() - l), slowMs: e }), h;
150
+ } catch (h) {
151
+ throw rt(t, { url: u, method: g, status: 0, error: String((h == null ? void 0 : h.message) || h), ms: Math.round(performance.now() - l), slowMs: e }), h;
152
+ }
153
+ }), XMLHttpRequest.prototype.open = function(s, l, ...u) {
154
+ return this.__bc = { method: s, url: l, started: 0 }, a.call(this, s, l, ...u);
155
+ }, XMLHttpRequest.prototype.send = function(...s) {
156
+ return this.__bc && (this.__bc.started = performance.now(), this.addEventListener("loadend", () => {
157
+ rt(t, {
158
+ url: this.__bc.url,
159
+ method: this.__bc.method,
160
+ status: this.status,
161
+ ms: Math.round(performance.now() - this.__bc.started),
162
+ slowMs: e
163
+ });
164
+ })), o.apply(this, s);
165
+ }, () => {
166
+ n && (window.fetch = n), XMLHttpRequest.prototype.open = a, XMLHttpRequest.prototype.send = o;
167
+ };
168
+ }
169
+ function rt(t, { url: e, method: n, status: a, ms: o, error: r, slowMs: s }) {
170
+ try {
171
+ t.push({
172
+ // Scrubbed before it is buffered, not before it is sent: a credential
173
+ // that never enters the ring buffer cannot leave with a report, and
174
+ // cannot be read back out of memory by the page either. Redact first,
175
+ // then truncate — truncating first would split a token into something
176
+ // no pattern recognises, and the fragment would still be a fragment of
177
+ // a live credential.
178
+ url: Xt(String(e)).slice(0, 500),
179
+ method: n,
180
+ status: a,
181
+ ms: o,
182
+ error: r === void 0 ? void 0 : In(r),
183
+ // Flagged so triage can spot the interesting rows without reading everything.
184
+ failed: a === 0 || a >= 400,
185
+ slow: o >= s,
186
+ at: (/* @__PURE__ */ new Date()).toISOString()
187
+ });
188
+ } catch {
189
+ }
190
+ }
191
+ function Rn(t) {
192
+ const e = (a) => {
193
+ const o = String(a.message || "Error"), r = a.filename ? ` (${a.filename}:${a.lineno}:${a.colno})` : "";
194
+ t.push({
195
+ level: "error",
196
+ text: (/^uncaught/i.test(o) ? o : `Uncaught ${o}`) + r,
197
+ at: (/* @__PURE__ */ new Date()).toISOString()
198
+ });
199
+ }, n = (a) => {
200
+ t.push({
201
+ level: "error",
202
+ text: `Unhandled rejection: ${Gt(a.reason)}`,
203
+ at: (/* @__PURE__ */ new Date()).toISOString()
204
+ });
205
+ };
206
+ return window.addEventListener("error", e), window.addEventListener("unhandledrejection", n), () => {
207
+ window.removeEventListener("error", e), window.removeEventListener("unhandledrejection", n);
208
+ };
209
+ }
210
+ async function Rt() {
211
+ const t = Jt();
212
+ try {
213
+ if (navigator.getBattery) {
214
+ const e = await navigator.getBattery();
215
+ t.battery = Math.round(e.level * 100), t.charging = e.charging;
216
+ }
217
+ } catch {
218
+ }
219
+ return t;
220
+ }
221
+ function Jt() {
222
+ var e, n, a, o, r, s, l, u, g;
223
+ const t = window.navigator || {};
224
+ return {
225
+ userAgent: t.userAgent,
226
+ platform: t.platform,
227
+ language: t.language,
228
+ languages: t.languages ? [...t.languages] : null,
229
+ online: t.onLine,
230
+ cookieEnabled: t.cookieEnabled,
231
+ maxTouchPoints: t.maxTouchPoints ?? null,
232
+ viewport: { width: window.innerWidth, height: window.innerHeight },
233
+ screen: {
234
+ width: (e = window.screen) == null ? void 0 : e.width,
235
+ height: (n = window.screen) == null ? void 0 : n.height,
236
+ availWidth: (a = window.screen) == null ? void 0 : a.availWidth,
237
+ availHeight: (o = window.screen) == null ? void 0 : o.availHeight,
238
+ colorDepth: (r = window.screen) == null ? void 0 : r.colorDepth,
239
+ orientation: ((l = (s = window.screen) == null ? void 0 : s.orientation) == null ? void 0 : l.type) || null
240
+ },
241
+ devicePixelRatio: window.devicePixelRatio,
242
+ referrer: document.referrer || null,
243
+ title: document.title,
244
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
245
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
246
+ timezoneOffsetMinutes: (/* @__PURE__ */ new Date()).getTimezoneOffset(),
247
+ // A hint, not a guarantee: `webdriver` is the only broadly-available signal
248
+ // that something other than a person is driving the page.
249
+ browserMode: t.webdriver ? "Automated" : "Normal",
250
+ doNotTrack: t.doNotTrack || null,
251
+ hardwareConcurrency: t.hardwareConcurrency ?? null,
252
+ deviceMemory: t.deviceMemory ?? null,
253
+ connection: t.connection ? {
254
+ effectiveType: t.connection.effectiveType,
255
+ downlink: t.connection.downlink,
256
+ rtt: t.connection.rtt,
257
+ saveData: t.connection.saveData
258
+ } : null,
259
+ prefersColorScheme: (u = window.matchMedia) != null && u.call(window, "(prefers-color-scheme: dark)").matches ? "dark" : "light",
260
+ prefersReducedMotion: ((g = window.matchMedia) == null ? void 0 : g.call(window, "(prefers-reduced-motion: reduce)").matches) || !1
261
+ };
262
+ }
263
+ function Gt(t) {
264
+ if (typeof t == "string") return t;
265
+ if (t instanceof Error) return `${t.name}: ${t.message}`;
266
+ try {
267
+ return JSON.stringify(t);
268
+ } catch {
269
+ return String(t);
270
+ }
271
+ }
272
+ const jn = typeof window < "u" && window.fetch ? window.fetch.bind(window) : null, zn = typeof XMLHttpRequest < "u" ? XMLHttpRequest.prototype.open : null, On = typeof XMLHttpRequest < "u" ? XMLHttpRequest.prototype.send : null, en = [
273
+ "image/png",
274
+ "image/jpeg",
275
+ "image/webp",
276
+ "image/gif",
277
+ "video/webm",
278
+ "video/mp4",
279
+ "audio/webm",
280
+ "audio/ogg",
281
+ "audio/mpeg",
282
+ "audio/mp4",
283
+ "audio/wav",
284
+ "application/pdf",
285
+ "text/plain",
286
+ "text/csv",
287
+ "application/json",
288
+ "application/zip",
289
+ "application/msword",
290
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
291
+ "application/vnd.ms-excel",
292
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
293
+ ], Pe = [
294
+ "image/png",
295
+ "image/jpeg",
296
+ "image/webp",
297
+ "video/webm",
298
+ "video/mp4"
299
+ ], Hn = () => {
300
+ var t;
301
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getUserMedia && typeof MediaRecorder < "u");
302
+ }, qn = () => {
303
+ var t;
304
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getDisplayMedia && typeof MediaRecorder < "u");
305
+ }, Un = () => {
306
+ var t;
307
+ return !!((t = navigator == null ? void 0 : navigator.mediaDevices) != null && t.getDisplayMedia);
308
+ };
309
+ function Ke(t, e = en) {
310
+ const n = String(t || "").split(";")[0].trim().toLowerCase();
311
+ return e.includes(n) ? n : null;
312
+ }
313
+ async function Dn() {
314
+ const t = await navigator.mediaDevices.getDisplayMedia({
315
+ video: { frameRate: 1 },
316
+ audio: !1
317
+ });
318
+ try {
319
+ const e = t.getVideoTracks()[0], n = document.createElement("video");
320
+ n.srcObject = t, n.muted = !0, n.playsInline = !0, await n.play(), await new Promise((l) => requestAnimationFrame(() => requestAnimationFrame(l)));
321
+ const { width: a, height: o } = e.getSettings(), r = document.createElement("canvas");
322
+ r.width = a || n.videoWidth, r.height = o || n.videoHeight, r.getContext("2d").drawImage(n, 0, 0, r.width, r.height);
323
+ const s = await new Promise((l) => r.toBlob(l, "image/png"));
324
+ if (!s) throw new Error("The browser returned an empty screenshot.");
325
+ return { blob: s, mime: "image/png", kind: "SCREENSHOT" };
326
+ } finally {
327
+ t.getTracks().forEach((e) => e.stop());
328
+ }
329
+ }
330
+ async function Nn({ maxMs: t = 12e4, onTick: e, onAutoStop: n } = {}) {
331
+ const a = await navigator.mediaDevices.getDisplayMedia({
332
+ video: { frameRate: 15 },
333
+ audio: !1
334
+ }), o = ["video/webm;codecs=vp9", "video/webm;codecs=vp8", "video/webm"].find((h) => MediaRecorder.isTypeSupported(h)) || "video/webm", r = new MediaRecorder(a, { mimeType: o, videoBitsPerSecond: 25e5 }), s = [];
335
+ r.ondataavailable = (h) => {
336
+ h.data.size && s.push(h.data);
337
+ }, r.start(1e3);
338
+ const l = Date.now(), u = setInterval(() => e == null ? void 0 : e(Math.floor((Date.now() - l) / 1e3)), 500), g = () => {
339
+ clearInterval(u), clearTimeout(w), a.getTracks().forEach((h) => h.stop());
340
+ }, b = new Promise((h) => {
341
+ r.onstop = () => {
342
+ g(), h(new Blob(s, { type: o }));
343
+ };
344
+ }), d = () => {
345
+ r.state !== "inactive" && r.stop(), n == null || n();
346
+ };
347
+ a.getVideoTracks()[0].addEventListener("ended", d);
348
+ const w = setTimeout(d, t);
349
+ return {
350
+ mime: o,
351
+ maxMs: t,
352
+ async stop() {
353
+ return clearTimeout(w), r.state !== "inactive" && r.stop(), { blob: await b, mime: o, kind: "VIDEO" };
354
+ },
355
+ cancel() {
356
+ g(), r.state !== "inactive" && r.stop();
357
+ }
358
+ };
359
+ }
360
+ async function Vn({ maxMs: t = 12e4, onTick: e, onAutoStop: n } = {}) {
361
+ const a = await navigator.mediaDevices.getUserMedia({ audio: !0 }), o = ["audio/webm;codecs=opus", "audio/webm", "audio/mp4", "audio/ogg"].find((h) => MediaRecorder.isTypeSupported(h)) || "", r = new MediaRecorder(a, o ? { mimeType: o } : void 0), s = [];
362
+ r.ondataavailable = (h) => {
363
+ h.data.size && s.push(h.data);
364
+ }, r.start(1e3);
365
+ const l = Date.now(), u = setInterval(() => e == null ? void 0 : e(Math.floor((Date.now() - l) / 1e3)), 500), g = () => {
366
+ clearInterval(u), clearTimeout(w), a.getTracks().forEach((h) => h.stop());
367
+ }, b = new Promise((h) => {
368
+ r.onstop = () => {
369
+ g(), h(new Blob(s, { type: r.mimeType || o }));
370
+ };
371
+ }), w = setTimeout(() => {
372
+ r.state !== "inactive" && r.stop(), n == null || n();
373
+ }, t);
374
+ return {
375
+ mime: r.mimeType || o,
376
+ maxMs: t,
377
+ async stop() {
378
+ clearTimeout(w), r.state !== "inactive" && r.stop();
379
+ const h = await b;
380
+ return { blob: h, mime: h.type || r.mimeType || o, kind: "AUDIO" };
381
+ },
382
+ cancel() {
383
+ g(), r.state !== "inactive" && r.stop();
384
+ }
385
+ };
386
+ }
387
+ async function Pn({ apiBase: t, apiKey: e, blob: n, mime: a, onProgress: o }) {
388
+ const r = Ke(a || n.type);
389
+ if (!r)
390
+ throw new Error(`[BugCatch] ${a || n.type || "that file"} can't be attached.`);
391
+ const s = await jn(`${t}/ingest/presign`, {
392
+ method: "POST",
393
+ headers: { "Content-Type": "application/json", "X-BugCatch-Key": e },
394
+ body: JSON.stringify({ mime: r })
395
+ });
396
+ if (!s.ok) throw new Error(`[BugCatch] could not start the upload (HTTP ${s.status}).`);
397
+ const { key: l, uploadUrl: u } = await s.json();
398
+ return await Kn(u, n, r, o), { storageKey: l, mime: r };
399
+ }
400
+ function Kn(t, e, n, a) {
401
+ return new Promise((o, r) => {
402
+ const s = new XMLHttpRequest();
403
+ zn.call(s, "PUT", t, !0), s.setRequestHeader("Content-Type", n), s.upload.onprogress = (l) => {
404
+ l.lengthComputable && (a == null || a(l.loaded / l.total));
405
+ }, s.onload = () => {
406
+ s.status >= 200 && s.status < 300 ? o() : r(new Error(`[BugCatch] upload failed (HTTP ${s.status}).`));
407
+ }, s.onerror = () => r(new Error(
408
+ "[BugCatch] the browser blocked the upload to storage. If this is a new environment, the media bucket is probably missing its CORS policy."
409
+ )), s.onabort = () => r(new Error("[BugCatch] upload cancelled.")), On.call(s, e);
410
+ });
411
+ }
412
+ function Te(t) {
413
+ return t < 1024 ? `${t} B` : t < 1024 * 1024 ? `${(t / 1024).toFixed(0)} KB` : `${(t / 1024 / 1024).toFixed(1)} MB`;
414
+ }
415
+ function qe(t) {
416
+ const e = Math.floor(t / 60), n = t % 60;
417
+ return `${e}:${String(n).padStart(2, "0")}`;
418
+ }
419
+ const bt = "bugcatch.watch", jt = [2e3, 5e3, 15e3, 3e4, 6e4], _n = 5e3;
420
+ function ne() {
421
+ var t;
422
+ try {
423
+ return ((t = window.localStorage) == null ? void 0 : t.getItem(bt)) || null;
424
+ } catch {
425
+ return null;
426
+ }
427
+ }
428
+ function lt(t) {
429
+ var e;
430
+ if (t)
431
+ try {
432
+ (e = window.localStorage) == null || e.setItem(bt, t);
433
+ } catch {
434
+ }
435
+ }
436
+ function ge() {
437
+ var t;
438
+ try {
439
+ (t = window.localStorage) == null || t.removeItem(bt);
440
+ } catch {
441
+ }
442
+ tn();
443
+ }
444
+ const _e = "bugcatch.unread";
445
+ function Wn(t) {
446
+ return [(t == null ? void 0 : t.bugId) || "report", (t == null ? void 0 : t.kind) || "status", (t == null ? void 0 : t.at) || ""].join("|");
447
+ }
448
+ function Ie() {
449
+ var t;
450
+ try {
451
+ const e = (t = window.localStorage) == null ? void 0 : t.getItem(_e), n = e ? JSON.parse(e) : [];
452
+ return Array.isArray(n) ? n.filter((a) => a && typeof a.id == "string") : [];
453
+ } catch {
454
+ return [];
455
+ }
456
+ }
457
+ function Ye(t) {
458
+ var e, n;
459
+ try {
460
+ t.length ? (e = window.localStorage) == null || e.setItem(_e, JSON.stringify(t)) : (n = window.localStorage) == null || n.removeItem(_e);
461
+ } catch {
462
+ }
463
+ }
464
+ function Fn(t) {
465
+ const e = { ...t, id: Wn(t) }, n = Ie().filter((a) => a.id !== e.id);
466
+ return n.push(e), Ye(n.slice(-20)), e;
467
+ }
468
+ function Yn(t) {
469
+ const e = Ie().filter((n) => n.id !== t);
470
+ return Ye(e), e;
471
+ }
472
+ function Zn(t) {
473
+ const e = Ie().filter((n) => n.bugId !== t);
474
+ return Ye(e), e;
475
+ }
476
+ function tn() {
477
+ return Ye([]), [];
478
+ }
479
+ function Xn(t, e) {
480
+ let n = t, a = n.indexOf(`
481
+
482
+ `);
483
+ for (; a !== -1; ) {
484
+ const o = n.slice(0, a);
485
+ n = n.slice(a + 2);
486
+ const r = o.split(`
487
+ `).filter((s) => s.startsWith("data:")).map((s) => s.slice(5).trim()).join(`
488
+ `);
489
+ if (r)
490
+ try {
491
+ e(JSON.parse(r));
492
+ } catch {
493
+ }
494
+ a = n.indexOf(`
495
+
496
+ `);
497
+ }
498
+ return n;
499
+ }
500
+ async function Jn({ apiBase: t }) {
501
+ const e = ne();
502
+ if (!e) return { reporter: null, reports: [] };
503
+ const n = await fetch(`${t}/ingest/watch/reports`, {
504
+ headers: { Authorization: `Bearer ${e}` }
505
+ });
506
+ if (n.status === 401)
507
+ return ge(), { reporter: null, reports: [] };
508
+ if (!n.ok) throw new Error(`[BugCatch] could not load your reports (HTTP ${n.status})`);
509
+ return n.json();
510
+ }
511
+ async function Gn({ apiBase: t, bugId: e }) {
512
+ const n = ne();
513
+ if (!n) return null;
514
+ const a = await fetch(`${t}/ingest/watch/reports/${encodeURIComponent(e)}`, {
515
+ headers: { Authorization: `Bearer ${n}` }
516
+ });
517
+ if (a.status === 401)
518
+ return ge(), null;
519
+ if (a.status === 404) return null;
520
+ if (!a.ok) throw new Error(`[BugCatch] could not load that report (HTTP ${a.status})`);
521
+ return a.json();
522
+ }
523
+ async function nn({ apiBase: t, bugId: e, body: n, attachments: a }) {
524
+ const o = ne();
525
+ if (!o) throw new Error("[BugCatch] nothing filed from this browser yet.");
526
+ const r = await fetch(
527
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/messages`,
528
+ {
529
+ method: "POST",
530
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${o}` },
531
+ body: JSON.stringify({
532
+ ...n ? { body: n } : {},
533
+ ...a != null && a.length ? { attachments: a } : {}
534
+ })
535
+ }
536
+ );
537
+ if (r.status === 401)
538
+ throw ge(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
539
+ if (!r.ok) {
540
+ let s = "";
541
+ try {
542
+ const l = await r.json();
543
+ s = Array.isArray(l.message) ? l.message.join(", ") : l.message || "";
544
+ } catch {
545
+ }
546
+ throw new Error(s || `[BugCatch] could not send that (HTTP ${r.status}).`);
547
+ }
548
+ return r.json();
549
+ }
550
+ async function ea({ apiBase: t, apiKey: e, projectId: n }) {
551
+ try {
552
+ const a = await fetch(
553
+ `${t}/ingest/calls?projectId=${encodeURIComponent(n)}`,
554
+ { headers: { "X-BugCatch-Key": e } }
555
+ );
556
+ if (!a.ok) return null;
557
+ const o = await a.json();
558
+ return o != null && o.available ? o.kind === "live" || o.kind === "agent" ? o.kind : "agent" : null;
559
+ } catch {
560
+ return null;
561
+ }
562
+ }
563
+ async function ta({ apiBase: t, apiKey: e, token: n, payload: a }) {
564
+ const o = await fetch(`${t}/ingest/calls`, {
565
+ method: "POST",
566
+ headers: {
567
+ "Content-Type": "application/json",
568
+ "X-BugCatch-Key": e,
569
+ // The same header `report()` sends, and for the same reason: this route
570
+ // files a report too. Without it a project that requires sign-in refuses
571
+ // the call outright — and the reporter is told to sign in by a panel that
572
+ // is saying, an inch below the message, who they are signed in as.
573
+ ...n ? { "X-BugCatch-Session": n } : {}
574
+ },
575
+ body: JSON.stringify(a)
576
+ });
577
+ if (!o.ok) {
578
+ let r = "";
579
+ try {
580
+ const l = await o.json();
581
+ r = Array.isArray(l.message) ? l.message.join(", ") : l.message || "";
582
+ } catch {
583
+ }
584
+ const s = new Error(
585
+ r || `[BugCatch] the call could not be started (HTTP ${o.status}).`
586
+ );
587
+ throw s.status = o.status, s;
588
+ }
589
+ return o.json();
590
+ }
591
+ async function na({ apiBase: t, bugId: e }) {
592
+ const n = ne();
593
+ if (!n) return !1;
594
+ try {
595
+ const a = await fetch(
596
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/voice-call`,
597
+ { headers: { Authorization: `Bearer ${n}` } }
598
+ );
599
+ if (!a.ok) return !1;
600
+ const o = await a.json();
601
+ return !!(o != null && o.offered);
602
+ } catch {
603
+ return !1;
604
+ }
605
+ }
606
+ async function aa({ apiBase: t, bugId: e }) {
607
+ const n = ne();
608
+ if (!n) throw new Error("[BugCatch] nothing filed from this browser yet.");
609
+ const a = await fetch(
610
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/voice-call`,
611
+ { method: "POST", headers: { Authorization: `Bearer ${n}` } }
612
+ );
613
+ if (a.status === 401)
614
+ throw ge(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
615
+ if (!a.ok) {
616
+ let o = "";
617
+ try {
618
+ const r = await a.json();
619
+ o = Array.isArray(r.message) ? r.message.join(", ") : r.message || "";
620
+ } catch {
621
+ }
622
+ throw new Error(o || `[BugCatch] the call could not be started (HTTP ${a.status}).`);
623
+ }
624
+ return a.json();
625
+ }
626
+ async function oa({ apiBase: t, bugId: e }) {
627
+ const n = ne();
628
+ if (!n) return !1;
629
+ try {
630
+ const a = await fetch(
631
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call`,
632
+ { headers: { Authorization: `Bearer ${n}` } }
633
+ );
634
+ if (!a.ok) return !1;
635
+ const o = await a.json();
636
+ return !!(o != null && o.offered);
637
+ } catch {
638
+ return !1;
639
+ }
640
+ }
641
+ async function ra({ apiBase: t, bugId: e }) {
642
+ const n = ne();
643
+ if (!n) throw new Error("[BugCatch] nothing filed from this browser yet.");
644
+ const a = await fetch(
645
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call`,
646
+ { method: "POST", headers: { Authorization: `Bearer ${n}` } }
647
+ );
648
+ if (a.status === 401)
649
+ throw ge(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
650
+ if (!a.ok) {
651
+ let o = "";
652
+ try {
653
+ const r = await a.json();
654
+ o = Array.isArray(r.message) ? r.message.join(", ") : r.message || "";
655
+ } catch {
656
+ }
657
+ throw new Error(o || `[BugCatch] the call could not be started (HTTP ${a.status}).`);
658
+ }
659
+ return a.json();
660
+ }
661
+ async function ia({ apiBase: t, bugId: e, callId: n }) {
662
+ const a = ne();
663
+ if (a)
664
+ try {
665
+ await fetch(
666
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/give-up`,
667
+ { method: "POST", headers: { Authorization: `Bearer ${a}` } }
668
+ );
669
+ } catch {
670
+ }
671
+ }
672
+ async function sa({ apiBase: t, bugId: e, callId: n }) {
673
+ const a = ne();
674
+ if (!a) throw new Error("[BugCatch] nothing filed from this browser yet.");
675
+ const o = await fetch(
676
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/answer`,
677
+ { method: "POST", headers: { Authorization: `Bearer ${a}` } }
678
+ );
679
+ if (o.status === 401)
680
+ throw ge(), new Error("[BugCatch] this browser can no longer prove it filed that report.");
681
+ if (o.status === 409)
682
+ throw new Error("[BugCatch] that call is no longer ringing.");
683
+ if (!o.ok) {
684
+ let r = "";
685
+ try {
686
+ const s = await o.json();
687
+ r = Array.isArray(s.message) ? s.message.join(", ") : s.message || "";
688
+ } catch {
689
+ }
690
+ throw new Error(r || `[BugCatch] the call could not be answered (HTTP ${o.status}).`);
691
+ }
692
+ return o.json();
693
+ }
694
+ async function zt({ apiBase: t, bugId: e, callId: n }) {
695
+ const a = ne();
696
+ if (a)
697
+ try {
698
+ await fetch(
699
+ `${t}/ingest/watch/reports/${encodeURIComponent(e)}/live-call/${encodeURIComponent(n)}/decline`,
700
+ { method: "POST", headers: { Authorization: `Bearer ${a}` } }
701
+ );
702
+ } catch {
703
+ }
704
+ }
705
+ function ca({ apiBase: t, onUpdate: e }) {
706
+ let n = !0, a = 0, o = null;
707
+ async function r() {
708
+ const s = ne();
709
+ if (!s) throw new Error("nothing filed from this browser yet");
710
+ o = new AbortController();
711
+ const l = await fetch(`${t}/ingest/watch/stream`, {
712
+ headers: { Authorization: `Bearer ${s}`, Accept: "text/event-stream" },
713
+ signal: o.signal
714
+ });
715
+ if (l.status === 401)
716
+ throw n = !1, ge(), new Error("subscription no longer valid");
717
+ if (!l.ok || !l.body) throw new Error(`stream failed (${l.status})`);
718
+ const u = Date.now(), g = l.body.getReader(), b = new TextDecoder();
719
+ let d = "";
720
+ try {
721
+ for (; ; ) {
722
+ const { value: w, done: h } = await g.read();
723
+ if (h) break;
724
+ d += b.decode(w, { stream: !0 }), d = Xn(d, (S) => {
725
+ if (S.type !== "ping")
726
+ try {
727
+ e(S);
728
+ } catch {
729
+ }
730
+ });
731
+ }
732
+ } finally {
733
+ Date.now() - u >= _n && (a = 0);
734
+ }
735
+ }
736
+ return (async () => {
737
+ for (; n; ) {
738
+ try {
739
+ await r();
740
+ } catch (l) {
741
+ if ((l == null ? void 0 : l.name) === "AbortError") return;
742
+ }
743
+ if (!n) return;
744
+ const s = jt[Math.min(a, jt.length - 1)];
745
+ a += 1, await new Promise((l) => setTimeout(l, s));
746
+ }
747
+ })(), () => {
748
+ n = !1, o == null || o.abort(), o = null;
749
+ };
750
+ }
751
+ const la = {
752
+ OPEN: "is open",
753
+ IN_PROGRESS: "is being worked on",
754
+ RESOLVED: "has been fixed",
755
+ CLOSED: "has been closed"
756
+ }, da = {
757
+ status: null,
758
+ // falls back to the host's configured accent
759
+ severity: "#e8912a",
760
+ assignment: "#4da3ff",
761
+ reply: "#35b87f"
762
+ };
763
+ function Ot(t) {
764
+ return t != null && t.summary ? t.summary : `Your report ${la[t == null ? void 0 : t.status] || "was updated"}`;
765
+ }
766
+ function ua(t) {
767
+ var e;
768
+ return (t == null ? void 0 : t.kind) === "reply" && ((e = t.reply) != null && e.body) ? `${t.reply.author ? `${t.reply.author}: ` : ""}${t.reply.body}` : (t == null ? void 0 : t.title) || "";
769
+ }
770
+ function pa(t) {
771
+ const e = Date.parse(t || "");
772
+ if (!Number.isFinite(e)) return "";
773
+ const n = Math.round((Date.now() - e) / 6e4);
774
+ if (n < 1) return "just now";
775
+ if (n < 60) return `${n}m ago`;
776
+ const a = Math.round(n / 60);
777
+ if (a < 24) return `${a}h ago`;
778
+ const o = Math.round(a / 24);
779
+ return o === 1 ? "yesterday" : `${o}d ago`;
780
+ }
781
+ function ha() {
782
+ var t;
783
+ try {
784
+ const e = document.querySelector('[data-bugcatch="widget"]'), n = (t = e == null ? void 0 : e.shadowRoot) == null ? void 0 : t.querySelector(".dock"), a = (n == null ? void 0 : n.getBoundingClientRect().height) || 0;
785
+ return a ? Math.round(a) + 12 : 0;
786
+ } catch {
787
+ return 0;
788
+ }
789
+ }
790
+ function fa({
791
+ accent: t = "#4F46E5",
792
+ position: e = "bottom-right",
793
+ onRead: n = null,
794
+ onReadAll: a = null,
795
+ onCall: o = null
796
+ } = {}) {
797
+ const [r, s] = String(e).split("-"), l = r === "top", u = s === "left", g = /* @__PURE__ */ new Map();
798
+ let b = null, d = null;
799
+ function w() {
800
+ return b != null && b.isConnected || (b = document.createElement("div"), b.setAttribute("data-bugcatch", "updates"), b.style.cssText = [
801
+ "position:fixed",
802
+ `${l ? "top" : "bottom"}:${20 + ha()}px`,
803
+ `${u ? "left" : "right"}:20px`,
804
+ // Under the widget's dock and its panel scrim, both of which are higher.
805
+ // A notification is worth interrupting a page for, not the report being
806
+ // read on top of it.
807
+ "z-index:2147482998",
808
+ "display:flex",
809
+ "flex-direction:column",
810
+ "gap:10px",
811
+ `align-items:${u ? "flex-start" : "flex-end"}`,
812
+ "width:min(320px,calc(100vw - 40px))",
813
+ // The gaps between cards belong to the page underneath.
814
+ "pointer-events:none"
815
+ ].join(";"), document.body.appendChild(b)), b;
816
+ }
817
+ function h(k) {
818
+ if (!k._bcShown) {
819
+ k.remove();
820
+ return;
821
+ }
822
+ k.style.opacity = "0", k.style.transform = `translateY(${l ? -8 : 8}px)`, setTimeout(() => k.remove(), 250);
823
+ }
824
+ function S(k, y) {
825
+ const x = document.createElement("button");
826
+ return x.type = "button", x.textContent = k, x.style.cssText = [
827
+ "padding:5px 11px",
828
+ `border:${y ? "0" : "1px solid rgba(236,238,242,.24)"}`,
829
+ "border-radius:6px",
830
+ `background:${y ? t : "transparent"}`,
831
+ `color:${y ? "#fff" : "#eceef2"}`,
832
+ "font:600 12px/1.4 inherit",
833
+ "cursor:pointer",
834
+ "white-space:nowrap"
835
+ ].join(";"), x;
836
+ }
837
+ function z(k) {
838
+ var K;
839
+ const y = document.createElement("div");
840
+ y.setAttribute("role", "status"), y.style.cssText = [
841
+ "box-sizing:border-box",
842
+ "width:100%",
843
+ "padding:12px 14px",
844
+ "border-radius:10px",
845
+ "background:#1b1e26",
846
+ "color:#eceef2",
847
+ 'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
848
+ "box-shadow:0 12px 30px rgba(0,0,0,0.45)",
849
+ `border-left:3px solid ${da[k == null ? void 0 : k.kind] || t}`,
850
+ "opacity:0",
851
+ `transform:translateY(${l ? -8 : 8}px)`,
852
+ "transition:opacity .2s ease,transform .2s ease",
853
+ "pointer-events:auto"
854
+ ].join(";");
855
+ const x = document.createElement("div");
856
+ x.style.cssText = "font-weight:600;margin-bottom:2px", x.textContent = Ot(k);
857
+ const C = document.createElement("div");
858
+ C.style.cssText = (k == null ? void 0 : k.kind) === "reply" ? "color:#a4abba;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden" : "color:#a4abba", C.textContent = ua(k);
859
+ const R = document.createElement("div");
860
+ R.style.cssText = "display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap";
861
+ const Y = document.createElement("span");
862
+ Y.style.cssText = "color:#7b8394;font-size:11px;margin-right:auto", Y.textContent = pa(k == null ? void 0 : k.at);
863
+ const O = S("Mark as read", !1);
864
+ if (O.setAttribute("aria-label", `Mark as read: ${Ot(k)}`), O.addEventListener("click", () => n == null ? void 0 : n(k.id)), R.append(Y, O), (K = k == null ? void 0 : k.call) != null && K.offered && typeof o == "function") {
865
+ const B = S("Talk it through", !0);
866
+ B.addEventListener("click", () => o(k)), R.insertBefore(B, O);
867
+ }
868
+ return y.append(x, C, R), y;
869
+ }
870
+ function F(k) {
871
+ const y = document.createElement("div");
872
+ y.style.cssText = [
873
+ "box-sizing:border-box",
874
+ "width:100%",
875
+ "display:flex",
876
+ "align-items:center",
877
+ "gap:8px",
878
+ "padding:8px 12px",
879
+ "border-radius:10px",
880
+ "background:#1b1e26",
881
+ "color:#a4abba",
882
+ 'font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
883
+ "box-shadow:0 12px 30px rgba(0,0,0,0.45)",
884
+ "pointer-events:auto"
885
+ ].join(";");
886
+ const x = document.createElement("span");
887
+ x.style.cssText = "margin-right:auto", x.textContent = `${k} more update${k === 1 ? "" : "s"}`;
888
+ const C = S("Mark all as read", !1);
889
+ return C.style.cssText += ";padding:4px 9px", C.addEventListener("click", () => a == null ? void 0 : a()), y.append(x, C), y;
890
+ }
891
+ return {
892
+ /**
893
+ * Draw what is unread, oldest first.
894
+ *
895
+ * The newest card sits nearest the corner the widget is in, which is the
896
+ * corner the reporter's eye is already trained on — so the stack grows away
897
+ * from the launcher rather than over it.
898
+ */
899
+ render(k) {
900
+ const y = Array.isArray(k) ? k : [];
901
+ if (!y.length) {
902
+ for (const B of g.values()) h(B);
903
+ g.clear(), d == null || d.remove(), d = null, b == null || b.remove(), b = null;
904
+ return;
905
+ }
906
+ const x = y.slice(-3), C = new Set(x.map((B) => B.id)), R = w();
907
+ for (const [B, X] of g)
908
+ C.has(B) || (h(X), g.delete(B));
909
+ const Y = [];
910
+ for (const B of x) {
911
+ if (g.has(B.id)) continue;
912
+ const X = z(B);
913
+ g.set(B.id, X), Y.push(X);
914
+ }
915
+ const O = l ? [...x].reverse() : x;
916
+ for (const B of O) R.appendChild(g.get(B.id));
917
+ const K = y.length - x.length;
918
+ K > 0 ? (d == null || d.remove(), d = F(K), l ? R.appendChild(d) : R.insertBefore(d, R.firstChild)) : (d == null || d.remove(), d = null), requestAnimationFrame(() => {
919
+ for (const B of Y)
920
+ B.isConnected && (B._bcShown = !0, B.style.opacity = "1", B.style.transform = "translateY(0)");
921
+ });
922
+ },
923
+ destroy() {
924
+ g.clear(), d = null, b == null || b.remove(), b = null;
925
+ }
926
+ };
927
+ }
928
+ async function ga({ apiBase: t, apiKey: e, projectId: n, sessionToken: a }) {
929
+ try {
930
+ const o = await fetch(
931
+ `${t}/ingest/known-issues?projectId=${encodeURIComponent(n)}`,
932
+ { headers: wt(e, a) }
933
+ );
934
+ if (!o.ok) return [];
935
+ const r = await o.json();
936
+ return Array.isArray(r == null ? void 0 : r.issues) ? r.issues : [];
937
+ } catch {
938
+ return [];
939
+ }
940
+ }
941
+ async function ma({ apiBase: t, apiKey: e, projectId: n, q: a, sessionToken: o, signal: r }) {
942
+ try {
943
+ const s = `${t}/ingest/known-issues/match?projectId=${encodeURIComponent(n)}&q=${encodeURIComponent(a)}`, l = await fetch(s, { headers: wt(e, o), signal: r });
944
+ if (!l.ok) return [];
945
+ const u = await l.json();
946
+ return Array.isArray(u == null ? void 0 : u.issues) ? u.issues : [];
947
+ } catch {
948
+ return [];
949
+ }
950
+ }
951
+ async function ba({
952
+ apiBase: t,
953
+ apiKey: e,
954
+ projectId: n,
955
+ bugId: a,
956
+ reporterEmail: o,
957
+ reporterName: r,
958
+ metadata: s,
959
+ logs: l,
960
+ sessionToken: u
961
+ }) {
962
+ const g = await fetch(
963
+ `${t}/ingest/known-issues/${encodeURIComponent(a)}/join`,
964
+ {
965
+ method: "POST",
966
+ headers: wt(e, u),
967
+ body: JSON.stringify({ projectId: n, reporterEmail: o, reporterName: r, metadata: s, logs: l })
968
+ }
969
+ );
970
+ if (!g.ok) throw new Error(await wa(g));
971
+ const b = await g.json();
972
+ return b != null && b.watchToken && lt(b.watchToken), b;
973
+ }
974
+ function wt(t, e) {
975
+ return {
976
+ "Content-Type": "application/json",
977
+ "X-BugCatch-Key": t,
978
+ ...e ? { "X-BugCatch-Session": e } : {}
979
+ };
980
+ }
981
+ async function wa(t) {
982
+ try {
983
+ const e = await t.json(), n = Array.isArray(e.message) ? e.message.join(", ") : e.message;
984
+ if (n) return n;
985
+ } catch {
986
+ }
987
+ return `Could not add you to this issue (HTTP ${t.status})`;
988
+ }
989
+ const Ht = {
990
+ "bottom-right": "bottom:20px;right:20px;",
991
+ "bottom-left": "bottom:20px;left:20px;",
992
+ "top-right": "top:20px;right:20px;",
993
+ "top-left": "top:20px;left:20px;"
994
+ }, ya = [
995
+ { value: "LOW", label: "Low" },
996
+ { value: "MEDIUM", label: "Medium" },
997
+ { value: "HIGH", label: "High" },
998
+ { value: "CRITICAL", label: "Critical" }
999
+ ], H = {
1000
+ camera: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5A1.5 1.5 0 0 1 4.5 7h2.2a1 1 0 0 0 .83-.45l.94-1.4A1 1 0 0 1 9.3 4.7h5.4a1 1 0 0 1 .83.45l.94 1.4a1 1 0 0 0 .83.45h2.2A1.5 1.5 0 0 1 21 8.5v9A1.5 1.5 0 0 1 19.5 19h-15A1.5 1.5 0 0 1 3 17.5z"/><circle cx="12" cy="13" r="3.4"/></svg>',
1001
+ video: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="6" width="12.5" height="12" rx="2.2"/><path d="M15.5 10.8l4.2-2.6a.6.6 0 0 1 .92.5v6.6a.6.6 0 0 1-.92.5l-4.2-2.6z"/></svg>',
1002
+ paperclip: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11.5l-7.6 7.6a4.6 4.6 0 0 1-6.5-6.5l8-8a3.2 3.2 0 0 1 4.5 4.5l-8 8a1.8 1.8 0 0 1-2.5-2.5l7.1-7.1"/></svg>',
1003
+ close: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>',
1004
+ mic: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M5.5 11a6.5 6.5 0 0 0 13 0M12 17.5V21M9 21h6"/></svg>',
1005
+ send: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l16-8-6 16-2.6-6.2L4 12Z"/></svg>',
1006
+ stopSquare: '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>',
1007
+ back: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 5.5L8 12l6.5 6.5"/></svg>',
1008
+ phone: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.2 3.5h3l1.5 3.7-2 1.4a11.5 11.5 0 0 0 5.7 5.7l1.4-2 3.7 1.5v3a1.8 1.8 0 0 1-2 1.8A15.6 15.6 0 0 1 4.4 5.5a1.8 1.8 0 0 1 1.8-2z"/></svg>',
1009
+ phoneOff: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M6.2 3.5h3l1.5 3.7-2 1.4a11.5 11.5 0 0 0 5.7 5.7l1.4-2 3.7 1.5v3a1.8 1.8 0 0 1-2 1.8A15.6 15.6 0 0 1 4.4 5.5a1.8 1.8 0 0 1 1.8-2z"/><path d="M3 21L21 3"/></svg>',
1010
+ bug: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6a4 4 0 0 1 8 0"/><rect x="7" y="7.5" width="10" height="12" rx="5"/><path d="M7 11H3.5M7 15.5H4M17 11h3.5M17 15.5h3M12 8v11.5"/></svg>',
1011
+ check: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.8l4.6 4.4L19 7"/></svg>',
1012
+ retry: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12a8 8 0 1 1-2.6-5.9M20 4v4.5h-4.5"/></svg>',
1013
+ sun: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4.2"/><path d="M12 2.6v2.2M12 19.2v2.2M2.6 12h2.2M19.2 12h2.2M5.4 5.4l1.6 1.6M17 17l1.6 1.6M18.6 5.4L17 7M7 17l-1.6 1.6"/></svg>',
1014
+ moon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13.4A8.2 8.2 0 1 1 10.6 4a6.6 6.6 0 0 0 9.4 9.4z"/></svg>',
1015
+ auto: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="2.8" y="4.4" width="18.4" height="12.6" rx="2.2"/><path d="M8.6 20.2h6.8"/><path d="M12 4.4v12.6"/><path d="M12 7.2a3.6 3.6 0 0 1 0 7.2z" fill="currentColor" stroke="none"/></svg>'
1016
+ }, va = '<svg class="bc-brand" viewBox="0 0 254 60" fill="none" role="img" aria-label="sphoro BugCatch"><g fill="currentColor"><path transform="translate(0.00 40.00) scale(0.04000 -0.04000)" d="M276.00 -14.00Q179.00 -14.00 117.00 28.00Q55.00 70.00 42.00 148.00L158.00 178.00Q165.00 143.00 181.50 123.00Q198.00 103.00 222.50 94.50Q247.00 86.00 276.00 86.00Q320.00 86.00 341.00 101.50Q362.00 117.00 362.00 140.00Q362.00 163.00 342.00 175.50Q322.00 188.00 278.00 196.00L250.00 201.00Q198.00 211.00 155.00 228.50Q112.00 246.00 86.00 277.00Q60.00 308.00 60.00 357.00Q60.00 431.00 114.00 470.50Q168.00 510.00 256.00 510.00Q339.00 510.00 394.00 473.00Q449.00 436.00 466.00 376.00L349.00 340.00Q341.00 378.00 316.50 394.00Q292.00 410.00 256.00 410.00Q220.00 410.00 201.00 397.50Q182.00 385.00 182.00 363.00Q182.00 339.00 202.00 327.50Q222.00 316.00 256.00 310.00L284.00 305.00Q340.00 295.00 385.50 278.50Q431.00 262.00 457.50 231.50Q484.00 201.00 484.00 149.00Q484.00 71.00 427.50 28.50Q371.00 -14.00 276.00 -14.00Z"/><path transform="translate(19.56 40.00) scale(0.04000 -0.04000)" d="M70.00 -200.00V496.00H194.00V436.00H212.00Q229.00 465.00 265.00 487.50Q301.00 510.00 368.00 510.00Q428.00 510.00 479.00 480.50Q530.00 451.00 561.00 394.00Q592.00 337.00 592.00 256.00V240.00Q592.00 159.00 561.00 102.00Q530.00 45.00 479.00 15.50Q428.00 -14.00 368.00 -14.00Q323.00 -14.00 292.50 -3.50Q262.00 7.00 243.50 23.50Q225.00 40.00 214.00 57.00H196.00V-200.00ZM330.00 96.00Q389.00 96.00 427.50 133.50Q466.00 171.00 466.00 243.00V253.00Q466.00 325.00 427.00 362.50Q388.00 400.00 330.00 400.00Q272.00 400.00 233.00 362.50Q194.00 325.00 194.00 253.00V243.00Q194.00 171.00 233.00 133.50Q272.00 96.00 330.00 96.00Z"/><path transform="translate(43.68 40.00) scale(0.04000 -0.04000)" d="M70.00 0.00V700.00H196.00V435.00H214.00Q222.00 451.00 239.00 467.00Q256.00 483.00 284.50 493.50Q313.00 504.00 357.00 504.00Q415.00 504.00 458.50 477.50Q502.00 451.00 526.00 404.50Q550.00 358.00 550.00 296.00V0.00H424.00V286.00Q424.00 342.00 396.50 370.00Q369.00 398.00 318.00 398.00Q260.00 398.00 228.00 359.50Q196.00 321.00 196.00 252.00V0.00Z"/><path transform="translate(66.92 40.00) scale(0.04000 -0.04000)" d="M306.00 -14.00Q232.00 -14.00 173.00 16.00Q114.00 46.00 80.00 103.00Q46.00 160.00 46.00 240.00V256.00Q46.00 336.00 80.00 393.00Q114.00 450.00 173.00 480.00Q232.00 510.00 306.00 510.00Q380.00 510.00 439.00 480.00Q498.00 450.00 532.00 393.00Q566.00 336.00 566.00 256.00V240.00Q566.00 160.00 532.00 103.00Q498.00 46.00 439.00 16.00Q380.00 -14.00 306.00 -14.00ZM306.00 98.00Q364.00 98.00 402.00 135.50Q440.00 173.00 440.00 243.00V253.00Q440.00 323.00 402.50 360.50Q365.00 398.00 306.00 398.00Q248.00 398.00 210.00 360.50Q172.00 323.00 172.00 253.00V243.00Q172.00 173.00 210.00 135.50Q248.00 98.00 306.00 98.00Z"/><path transform="translate(90.00 40.00) scale(0.04000 -0.04000)" d="M70.00 0.00V496.00H194.00V440.00H212.00Q223.00 470.00 248.50 484.00Q274.00 498.00 308.00 498.00H368.00V386.00H306.00Q258.00 386.00 227.00 360.50Q196.00 335.00 196.00 282.00V0.00Z"/><path transform="translate(104.44 40.00) scale(0.04000 -0.04000)" d="M306.00 -14.00Q232.00 -14.00 173.00 16.00Q114.00 46.00 80.00 103.00Q46.00 160.00 46.00 240.00V256.00Q46.00 336.00 80.00 393.00Q114.00 450.00 173.00 480.00Q232.00 510.00 306.00 510.00Q380.00 510.00 439.00 480.00Q498.00 450.00 532.00 393.00Q566.00 336.00 566.00 256.00V240.00Q566.00 160.00 532.00 103.00Q498.00 46.00 439.00 16.00Q380.00 -14.00 306.00 -14.00ZM306.00 98.00Q364.00 98.00 402.00 135.50Q440.00 173.00 440.00 243.00V253.00Q440.00 323.00 402.50 360.50Q365.00 398.00 306.00 398.00Q248.00 398.00 210.00 360.50Q172.00 323.00 172.00 253.00V243.00Q172.00 173.00 210.00 135.50Q248.00 98.00 306.00 98.00Z"/></g><rect x="139.52" y="16" width="1.6" height="26" rx="0.8" fill="var(--bc-brand-mark, #5647ee)" opacity="0.4"/><g fill="var(--bc-brand-mark, #5647ee)"><path transform="translate(154.52 35.00) scale(0.01400 -0.01400)" d="M46.35 0.00V105.85H139.75V594.15H46.35V700.00H404.08Q467.38 700.00 514.79 678.41Q562.20 656.83 588.70 617.33Q615.20 577.83 615.20 523.35V513.70Q615.20 465.52 597.03 434.76Q578.85 404.00 553.91 387.24Q528.98 370.48 506.28 363.30V346.35Q528.98 340.35 555.24 323.50Q581.50 306.65 600.00 275.48Q618.50 244.30 618.50 194.48V184.48Q618.50 126.65 591.41 85.24Q564.33 43.83 516.83 21.91Q469.33 0.00 406.85 0.00ZM259.85 109.15H392.60Q441.20 109.15 469.80 132.60Q498.40 156.05 498.40 198.55V208.20Q498.40 250.70 470.21 274.15Q442.02 297.60 392.60 297.60H259.85ZM259.85 406.75H391.65Q437.55 406.75 466.32 430.11Q495.10 453.48 495.10 493.80V503.80Q495.10 544.95 466.74 567.90Q438.37 590.85 391.65 590.85H259.85Z"/><path transform="translate(167.00 35.00) scale(0.01400 -0.01400)" d="M335.30 -14.00Q251.22 -14.00 190.60 16.63Q129.97 47.25 97.56 104.96Q65.15 162.68 65.15 243.03V700.00H185.25V239.73Q185.25 170.48 224.03 131.99Q262.80 93.50 335.30 93.50Q407.80 93.50 446.57 131.99Q485.35 170.48 485.35 239.73V700.00H605.45V243.03Q605.45 162.68 573.13 104.96Q540.80 47.25 480.09 16.63Q419.38 -14.00 335.30 -14.00Z"/><path transform="translate(179.60 35.00) scale(0.01400 -0.01400)" d="M311.85 -14.00Q238.45 -14.00 179.69 18.50Q120.92 51.00 86.69 113.67Q52.45 176.35 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.23 645.59Q598.33 577.17 598.33 460.87V456.70H479.87V464.52Q479.87 504.67 463.76 536.86Q447.65 569.05 414.99 587.77Q382.32 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 138.89Q257.63 91.85 334.00 91.85Q410.02 91.85 445.12 132.83Q480.22 173.80 480.22 237.92V249.67H302.85V351.88H598.33V0.00H487.52V69.10H470.57Q462.70 50.87 446.06 31.44Q429.42 12.00 397.64 -1.00Q365.85 -14.00 311.85 -14.00Z"/><path transform="translate(192.05 35.00) scale(0.01400 -0.01400)" d="M332.35 -14.00Q203.57 -14.00 128.01 58.50Q52.45 131.00 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.58 643.85Q599.03 573.70 599.03 451.57V445.75H480.57V455.22Q480.57 521.42 444.15 563.96Q407.72 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 139.71Q257.63 93.50 332.35 93.50Q407.72 93.50 444.15 136.45Q480.57 179.40 480.57 244.78V262.25H599.03V248.43Q599.03 126.30 529.58 56.15Q460.13 -14.00 332.35 -14.00Z"/><path transform="translate(204.25 35.00) scale(0.01400 -0.01400)" d="M19.75 0.00 211.62 700.00H421.33L613.20 0.00H489.27L447.42 159.60H185.53L143.68 0.00ZM214.25 270.40H418.70L324.95 624.87H308.00Z"/><path transform="translate(216.31 35.00) scale(0.01400 -0.01400)" d="M233.95 0.00V590.85H24.70V700.00H563.30V590.85H354.05V0.00Z"/><path transform="translate(227.75 35.00) scale(0.01400 -0.01400)" d="M332.35 -14.00Q203.57 -14.00 128.01 58.50Q52.45 131.00 52.45 266.87V433.13Q52.45 569.00 128.01 641.50Q203.57 714.00 332.35 714.00Q460.13 714.00 529.58 643.85Q599.03 573.70 599.03 451.57V445.75H480.57V455.22Q480.57 521.42 444.15 563.96Q407.72 606.50 332.35 606.50Q257.63 606.50 215.09 560.70Q172.55 514.90 172.55 435.13V264.87Q172.55 185.93 215.09 139.71Q257.63 93.50 332.35 93.50Q407.72 93.50 444.15 136.45Q480.57 179.40 480.57 244.78V262.25H599.03V248.43Q599.03 126.30 529.58 56.15Q460.13 -14.00 332.35 -14.00Z"/><path transform="translate(239.95 35.00) scale(0.01400 -0.01400)" d="M69.50 0.00V700.00H189.60V405.58H466.40V700.00H586.50V0.00H466.40V296.42H189.60V0.00Z"/></g></svg>', an = [
1017
+ { value: "light", label: "Light", icon: H.sun },
1018
+ { value: "dark", label: "Dark", icon: H.moon },
1019
+ { value: "auto", label: "System default", icon: H.auto }
1020
+ ], on = "bugcatch:theme";
1021
+ function xa() {
1022
+ try {
1023
+ const t = localStorage.getItem(on);
1024
+ return an.some((e) => e.value === t) ? t : null;
1025
+ } catch {
1026
+ return null;
1027
+ }
1028
+ }
1029
+ function ka(t) {
1030
+ try {
1031
+ localStorage.setItem(on, t);
1032
+ } catch {
1033
+ }
1034
+ }
1035
+ const Sa = (t) => `
1036
+ :host { all: initial; }
1037
+ :host {
1038
+ --bc-accent: ${t};
1039
+ --bc-on-accent: #fff;
1040
+ --bc-bg: #fff;
1041
+ --bc-elevated: #fff;
1042
+ --bc-fg: #14161c;
1043
+ --bc-muted: #6b7280;
1044
+ --bc-faint: #9aa1ad;
1045
+ --bc-border: #e3e6ec;
1046
+ --bc-subtle: #f5f6f8;
1047
+ --bc-raised: #fff;
1048
+ --bc-danger: #dc2626;
1049
+ --bc-ok: #059669;
1050
+ /* The lockup's own two colours, kept off --bc-accent on purpose: that one is
1051
+ the customer's brand, and this mark is ours. Values are the light cut of
1052
+ brand/sphoro-bugcatch-primary.svg, pulled up from near-black so the mark
1053
+ identifies itself without out-shouting the host's page. */
1054
+ --bc-brand-ink: #4b5563;
1055
+ --bc-brand-mark: #5647ee;
1056
+ --bc-shadow: 0 24px 60px rgba(10,12,16,.28), 0 2px 8px rgba(10,12,16,.08);
1057
+ --bc-dur: 240ms;
1058
+ --bc-ease: cubic-bezier(.22,1,.36,1);
1059
+ }
1060
+ :host(.bc-dark) {
1061
+ --bc-bg: #14161c;
1062
+ --bc-elevated: #1c1f27;
1063
+ --bc-fg: #eef0f4;
1064
+ --bc-muted: #9aa1ad;
1065
+ --bc-faint: #6b7280;
1066
+ --bc-border: #2b2f39;
1067
+ --bc-subtle: #21242c;
1068
+ --bc-raised: #363d4b;
1069
+ --bc-brand-ink: #9aa1ad;
1070
+ --bc-brand-mark: #9b8fff;
1071
+ --bc-shadow: 0 24px 60px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.4);
1072
+ }
1073
+ @media (prefers-color-scheme: dark) {
1074
+ :host(.bc-auto) {
1075
+ --bc-bg: #14161c;
1076
+ --bc-elevated: #1c1f27;
1077
+ --bc-fg: #eef0f4;
1078
+ --bc-muted: #9aa1ad;
1079
+ --bc-faint: #6b7280;
1080
+ --bc-border: #2b2f39;
1081
+ --bc-subtle: #21242c;
1082
+ --bc-raised: #363d4b;
1083
+ --bc-brand-ink: #9aa1ad;
1084
+ --bc-brand-mark: #9b8fff;
1085
+ --bc-shadow: 0 24px 60px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.4);
1086
+ }
1087
+ }
1088
+
1089
+ * { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif; }
1090
+ svg { width: 1em; height: 1em; display: block; }
1091
+ /* The hidden attribute has to actually hide. Nearly every element in here is given a
1092
+ display by a rule below, and any of them beats the attribute's own UA style
1093
+ — which is why .dock and .launcher each had to say so themselves. Said once,
1094
+ for all of them, so the next element hidden from JavaScript works. */
1095
+ [hidden] { display: none !important; }
1096
+
1097
+ /* Motion is decoration here — every animation below is an entrance or a state
1098
+ hint, so switching them off costs nothing but the polish. */
1099
+ @media (prefers-reduced-motion: reduce) {
1100
+ *, *::before, *::after {
1101
+ animation-duration: .01ms !important;
1102
+ animation-iteration-count: 1 !important;
1103
+ transition-duration: .01ms !important;
1104
+ }
1105
+ }
1106
+
1107
+ @keyframes bc-fade-in { from { opacity: 0 } to { opacity: 1 } }
1108
+ @keyframes bc-fade-out { to { opacity: 0 } }
1109
+ @keyframes bc-panel-in { from { opacity: 0; transform: translateY(16px) scale(.97) } to { opacity: 1; transform: none } }
1110
+ @keyframes bc-panel-out { to { opacity: 0; transform: translateY(8px) scale(.985) } }
1111
+ @keyframes bc-launcher-in { from { opacity: 0; transform: translateY(12px) scale(.86) } to { opacity: 1; transform: none } }
1112
+ @keyframes bc-item-in { from { opacity: 0; transform: translateY(8px) scale(.92) } to { opacity: 1; transform: none } }
1113
+ @keyframes bc-pulse { 0%, 100% { opacity: 1; transform: scale(1) } 50% { opacity: .3; transform: scale(.78) } }
1114
+ @keyframes bc-spin { to { transform: rotate(360deg) } }
1115
+ @keyframes bc-pop { 0% { transform: scale(.5); opacity: 0 } 65% { transform: scale(1.1) } 100% { transform: scale(1); opacity: 1 } }
1116
+ @keyframes bc-nudge { 25% { transform: translateX(-3px) } 75% { transform: translateX(3px) } }
1117
+
1118
+ /* ---------- dock: theme switch stacked above the launcher ---------- */
1119
+ .dock {
1120
+ position: fixed; z-index: 2147483000;
1121
+ display: flex; flex-direction: column; gap: 10px;
1122
+ }
1123
+ .dock[hidden] { display: none; }
1124
+ /* While the panel is open the switch rises above the scrim so it stays usable
1125
+ — the panel shares the shadow root, so a change re-themes the open form. */
1126
+ .dock.over { z-index: 2147483002; }
1127
+ .dock.at-right { align-items: flex-end; }
1128
+ .dock.at-left { align-items: flex-start; }
1129
+ /* Anchored to the top, "above the launcher" would be off-screen, so the
1130
+ switch goes under it and the launcher stays the outermost thing. */
1131
+ .dock.at-top { flex-direction: column-reverse; }
1132
+
1133
+ .themer {
1134
+ display: inline-flex; align-items: center; gap: 2px;
1135
+ padding: 3px; border-radius: 999px;
1136
+ background: var(--bc-elevated); border: 1px solid var(--bc-border);
1137
+ box-shadow: 0 6px 18px rgba(10,12,16,.16), 0 1px 3px rgba(10,12,16,.10);
1138
+ animation: bc-launcher-in 420ms var(--bc-ease) both;
1139
+ }
1140
+ .themer button {
1141
+ display: grid; place-items: center;
1142
+ width: 28px; height: 28px; padding: 0;
1143
+ border: none; border-radius: 999px; background: transparent;
1144
+ color: var(--bc-muted); cursor: pointer;
1145
+ transition: background var(--bc-dur) var(--bc-ease), color var(--bc-dur) var(--bc-ease), transform 140ms var(--bc-ease);
1146
+ }
1147
+ .themer button svg { font-size: 15px; }
1148
+ .themer button:hover { background: var(--bc-subtle); color: var(--bc-fg); }
1149
+ .themer button:active { transform: scale(.92); }
1150
+ .themer button[aria-pressed="true"] { background: var(--bc-accent); color: var(--bc-on-accent); }
1151
+ .themer button[aria-pressed="true"]:hover { background: var(--bc-accent); filter: brightness(1.06); }
1152
+ .themer button:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; }
1153
+
1154
+ /* ---------- launcher ---------- */
1155
+ .launcher {
1156
+ display: inline-flex; align-items: center; gap: 8px;
1157
+ padding: 11px 17px; border: none; border-radius: 999px;
1158
+ background: var(--bc-accent); color: var(--bc-on-accent);
1159
+ font-size: 14px; font-weight: 600; line-height: 1; letter-spacing: -.01em;
1160
+ cursor: pointer;
1161
+ box-shadow: 0 6px 18px rgba(10,12,16,.22), 0 1px 3px rgba(10,12,16,.16);
1162
+ transition: transform var(--bc-dur) var(--bc-ease), box-shadow var(--bc-dur) var(--bc-ease), filter 160ms linear;
1163
+ animation: bc-launcher-in 420ms var(--bc-ease) both;
1164
+ }
1165
+ .launcher svg { font-size: 17px; }
1166
+ .launcher:hover { transform: translateY(-2px); box-shadow: 0 12px 28px rgba(10,12,16,.26), 0 2px 6px rgba(10,12,16,.18); filter: brightness(1.06); }
1167
+ .launcher:active { transform: translateY(0) scale(.97); transition-duration: 90ms; }
1168
+ .launcher:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 3px; }
1169
+ .launcher[hidden] { display: none; }
1170
+
1171
+ /* ---------- scrim + panel ---------- */
1172
+ .scrim {
1173
+ position: fixed; inset: 0; z-index: 2147483001;
1174
+ background: rgba(10,12,16,.5);
1175
+ animation: bc-fade-in 200ms ease both;
1176
+ }
1177
+ .scrim.closing { animation: bc-fade-out 150ms ease both; }
1178
+
1179
+ /* The panel belongs to the launcher, so it opens out of that corner rather
1180
+ than in the middle of the screen. --bc-dock-h is measured at open time
1181
+ because the dock's height depends on whether the launcher is showing. */
1182
+ .panel {
1183
+ position: fixed;
1184
+ width: calc(100vw - 40px); max-width: 440px;
1185
+ max-height: calc(100vh - var(--bc-dock-h, 84px) - 52px);
1186
+ overflow-y: auto; overscroll-behavior: contain;
1187
+ background: var(--bc-bg); color: var(--bc-fg);
1188
+ border-radius: 16px; box-shadow: var(--bc-shadow);
1189
+ animation: bc-panel-in 320ms var(--bc-ease) both;
1190
+ }
1191
+ .scrim.at-right .panel { right: 20px; }
1192
+ .scrim.at-left .panel { left: 20px; }
1193
+ .scrim.at-bottom .panel { bottom: calc(20px + var(--bc-dock-h, 84px) + 12px); }
1194
+ .scrim.at-top .panel { top: calc(20px + var(--bc-dock-h, 84px) + 12px); }
1195
+ .scrim.closing .panel { animation: bc-panel-out 150ms ease both; }
1196
+
1197
+ .head { display: flex; align-items: flex-start; gap: 11px; padding: 18px 18px 0; }
1198
+ .head .mark {
1199
+ flex: none; display: grid; place-items: center;
1200
+ width: 32px; height: 32px; border-radius: 9px;
1201
+ background: color-mix(in srgb, var(--bc-accent) 13%, transparent);
1202
+ color: var(--bc-accent); font-size: 18px;
1203
+ }
1204
+ .head .titles { flex: 1; min-width: 0; }
1205
+ .head h2 { margin: 1px 0 3px; font-size: 16px; font-weight: 650; letter-spacing: -.015em; }
1206
+ .head p { margin: 0; font-size: 12.5px; line-height: 1.45; color: var(--bc-muted); }
1207
+ .icon-btn {
1208
+ flex: none; display: grid; place-items: center;
1209
+ width: 28px; height: 28px; padding: 0; font-size: 15px;
1210
+ border: none; border-radius: 8px; background: none; color: var(--bc-faint);
1211
+ cursor: pointer; transition: background 160ms, color 160ms;
1212
+ }
1213
+ .icon-btn:hover { background: var(--bc-subtle); color: var(--bc-fg); }
1214
+ .icon-btn:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 1px; }
1215
+
1216
+ .body { padding: 16px 18px 0; }
1217
+
1218
+
1219
+ label { display: block; font-size: 12px; font-weight: 600; color: var(--bc-fg); margin-bottom: 6px; }
1220
+ input[type=text], input[type=email], textarea {
1221
+ width: 100%; padding: 10px 12px; font-size: 14px; line-height: 1.45;
1222
+ color: var(--bc-fg); background: var(--bc-elevated);
1223
+ border: 1px solid var(--bc-border); border-radius: 10px;
1224
+ transition: border-color 160ms, box-shadow 160ms;
1225
+ }
1226
+ input[type=text]::placeholder, input[type=email]::placeholder, textarea::placeholder { color: var(--bc-faint); }
1227
+ input[type=text]:focus, input[type=email]:focus, textarea:focus {
1228
+ outline: none; border-color: var(--bc-accent);
1229
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--bc-accent) 18%, transparent);
1230
+ }
1231
+ input.invalid { border-color: var(--bc-danger); animation: bc-nudge 240ms ease; }
1232
+ textarea { min-height: 84px; resize: vertical; }
1233
+ .field { margin-bottom: 14px; }
1234
+
1235
+ /* ---------- what is already known ----------
1236
+ A help surface in front of the form, not a step in it. Everything here is
1237
+ dismissible by ignoring it: the reporter can read what is already known and
1238
+ leave, tap "this is happening to me too" and leave, or type straight past all
1239
+ of it and file the report they came to file. Nothing is ever in their way,
1240
+ because a reporter who feels handled rather than helped stops reporting. */
1241
+ .known { margin-bottom: 14px; }
1242
+ .known .known-h {
1243
+ margin: 0 0 8px; font-size: 12px; font-weight: 650; letter-spacing: .02em;
1244
+ text-transform: uppercase; color: var(--bc-faint);
1245
+ }
1246
+ .known .issue {
1247
+ display: flex; align-items: flex-start; gap: 10px;
1248
+ padding: 10px 12px; margin-bottom: 6px;
1249
+ background: var(--bc-subtle); border: 1px solid transparent; border-radius: 10px;
1250
+ }
1251
+ .known .issue.is-joined { border-color: var(--bc-ok); }
1252
+ .known .issue .who { flex: 1; min-width: 0; }
1253
+ .known .issue .t {
1254
+ display: block; font-size: 13px; font-weight: 620; line-height: 1.4;
1255
+ color: var(--bc-fg); overflow-wrap: anywhere;
1256
+ }
1257
+ .known .issue .meta {
1258
+ display: block; margin-top: 3px; font-size: 12px; line-height: 1.45; color: var(--bc-muted);
1259
+ }
1260
+ .known .issue .join {
1261
+ flex: none; padding: 6px 10px; font-size: 12px; font-weight: 620;
1262
+ color: var(--bc-accent); background: var(--bc-bg);
1263
+ border: 1px solid var(--bc-border); border-radius: 8px; cursor: pointer;
1264
+ }
1265
+ .known .issue .join:hover { border-color: var(--bc-accent); }
1266
+ .known .issue .join:disabled { color: var(--bc-ok); border-color: var(--bc-border); cursor: default; }
1267
+ .known .issue .join:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; }
1268
+ .known .ask { display: flex; gap: 8px; margin-top: 8px; }
1269
+ .known .ask input { flex: 1; min-width: 0; }
1270
+ .known .ask button { flex: none; }
1271
+ .known .err { margin: 6px 0 0; font-size: 12px; line-height: 1.45; color: var(--bc-danger); }
1272
+
1273
+ /* ---------- who is reporting ----------
1274
+ A strip above the form rather than a screen in front of it: where signing in
1275
+ is optional this is a line the reporter may ignore, and it should read as
1276
+ one. Where it is required the composer below is not rendered at all and this
1277
+ strip is the whole panel. */
1278
+ .whoami {
1279
+ display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
1280
+ margin-bottom: 14px; padding: 10px 12px;
1281
+ font-size: 12.5px; line-height: 1.45; color: var(--bc-muted);
1282
+ background: var(--bc-subtle); border-radius: 10px;
1283
+ }
1284
+ .whoami.asking { display: block; }
1285
+ .whoami .who { flex: 1; min-width: 0; }
1286
+ .whoami .who strong { color: var(--bc-fg); font-weight: 620; word-break: break-all; }
1287
+ .whoami .link {
1288
+ padding: 0; font-size: 12.5px; font-weight: 600; line-height: 1.45;
1289
+ color: var(--bc-accent); background: none; border: none; cursor: pointer;
1290
+ }
1291
+ .whoami .link:hover { text-decoration: underline; }
1292
+ .whoami .link:disabled { color: var(--bc-faint); cursor: default; text-decoration: none; }
1293
+ .whoami .link:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; border-radius: 4px; }
1294
+ .whoami .why { margin: 0 0 10px; font-size: 12.5px; line-height: 1.5; color: var(--bc-muted); }
1295
+ .whoami .why strong { color: var(--bc-fg); font-weight: 620; word-break: break-all; }
1296
+ .whoami .row { display: flex; gap: 8px; align-items: stretch; }
1297
+ .whoami .row input { flex: 1; min-width: 0; }
1298
+ .whoami .row button { flex: none; }
1299
+ .whoami .after { display: flex; gap: 12px; align-items: center; margin-top: 9px; }
1300
+ .whoami .err { color: var(--bc-danger); }
1301
+ .whoami .code { letter-spacing: .28em; font-variant-numeric: tabular-nums; }
1302
+
1303
+ /* ---------- severity ---------- */
1304
+ .seg { display: grid; grid-template-columns: repeat(4, 1fr); gap: 3px; padding: 3px; background: var(--bc-subtle); border-radius: 10px; }
1305
+ .seg button {
1306
+ padding: 7px 4px; font-size: 12.5px; font-weight: 550; line-height: 1;
1307
+ color: var(--bc-muted); background: none; border: none; border-radius: 7px;
1308
+ cursor: pointer; transition: background 180ms var(--bc-ease), color 180ms, box-shadow 180ms, font-weight 180ms;
1309
+ }
1310
+ .seg button:hover { color: var(--bc-fg); }
1311
+ .seg button[aria-pressed=true] {
1312
+ background: var(--bc-raised); color: var(--bc-accent); font-weight: 680;
1313
+ box-shadow: 0 1px 3px rgba(10,12,16,.14);
1314
+ }
1315
+ .seg button:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: -1px; }
1316
+
1317
+ /* ---------- attachments ---------- */
1318
+ .tools { display: flex; flex-wrap: wrap; gap: 6px; }
1319
+ .tool {
1320
+ display: inline-flex; align-items: center; gap: 6px;
1321
+ padding: 8px 12px; font-size: 12.5px; font-weight: 550; line-height: 1;
1322
+ color: var(--bc-fg); background: var(--bc-elevated);
1323
+ border: 1px solid var(--bc-border); border-radius: 9px; cursor: pointer;
1324
+ transition: border-color 160ms, background 160ms, transform 160ms var(--bc-ease);
1325
+ }
1326
+ .tool svg { font-size: 15px; color: var(--bc-muted); transition: color 160ms; }
1327
+ .tool:hover:not(:disabled) { border-color: var(--bc-accent); transform: translateY(-1px); }
1328
+ .tool:hover:not(:disabled) svg { color: var(--bc-accent); }
1329
+ .tool:active:not(:disabled) { transform: translateY(0) scale(.98); }
1330
+ .tool:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 1px; }
1331
+ .tool:disabled { opacity: .45; cursor: not-allowed; }
1332
+
1333
+ .thumbs { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
1334
+ .thumb {
1335
+ position: relative; width: 74px; height: 74px; flex: none;
1336
+ border-radius: 10px; overflow: hidden;
1337
+ border: 1px solid var(--bc-border); background: var(--bc-subtle);
1338
+ animation: bc-item-in 300ms var(--bc-ease) both;
1339
+ }
1340
+ .thumb img, .thumb video { width: 100%; height: 100%; object-fit: cover; display: block; }
1341
+ .thumb .kind {
1342
+ position: absolute; left: 4px; bottom: 4px;
1343
+ display: inline-flex; align-items: center; gap: 3px;
1344
+ padding: 2px 5px; border-radius: 5px;
1345
+ background: rgba(10,12,16,.72); color: #fff; font-size: 9.5px; font-weight: 600; line-height: 1.3;
1346
+ }
1347
+ .thumb .drop {
1348
+ position: absolute; top: 3px; right: 3px;
1349
+ width: 19px; height: 19px; font-size: 11px; border-radius: 6px;
1350
+ background: rgba(10,12,16,.68); color: #fff; border: none;
1351
+ display: grid; place-items: center; cursor: pointer;
1352
+ opacity: 0; transition: opacity 160ms;
1353
+ }
1354
+ .thumb:hover .drop, .thumb:focus-within .drop { opacity: 1; }
1355
+ .thumb .drop:hover { background: var(--bc-danger); }
1356
+ .thumb .veil {
1357
+ position: absolute; inset: 0; display: grid; place-items: center; gap: 6px;
1358
+ background: rgba(10,12,16,.62); color: #fff; font-size: 10.5px; font-weight: 600;
1359
+ align-content: center;
1360
+ }
1361
+ .thumb .veil.err { background: color-mix(in srgb, var(--bc-danger) 82%, transparent); }
1362
+ .spinner {
1363
+ width: 17px; height: 17px; border-radius: 50%;
1364
+ border: 2px solid rgba(255,255,255,.35); border-top-color: #fff;
1365
+ animation: bc-spin 720ms linear infinite;
1366
+ }
1367
+ .bar { width: 46px; height: 3px; border-radius: 2px; background: rgba(255,255,255,.3); overflow: hidden; }
1368
+ .bar i { display: block; height: 100%; width: 0; background: #fff; transition: width 180ms linear; }
1369
+ .veil button {
1370
+ background: none; border: none; color: #fff; font-size: 14px; padding: 2px;
1371
+ cursor: pointer; display: grid; place-items: center;
1372
+ }
1373
+
1374
+ /* ---------- messages ---------- */
1375
+ .msg {
1376
+ display: flex; align-items: flex-start; gap: 7px;
1377
+ font-size: 12.5px; line-height: 1.45; padding: 9px 11px; border-radius: 9px; margin-bottom: 12px;
1378
+ animation: bc-item-in 240ms var(--bc-ease) both;
1379
+ }
1380
+ .msg.err { background: color-mix(in srgb, var(--bc-danger) 11%, transparent); color: var(--bc-danger); }
1381
+ .msg.info { background: var(--bc-subtle); color: var(--bc-muted); }
1382
+
1383
+ /* ---------- footer ---------- */
1384
+ .foot {
1385
+ display: flex; align-items: center; gap: 9px;
1386
+ margin-top: 16px; padding: 14px 18px; border-top: 1px solid var(--bc-border);
1387
+ background: var(--bc-subtle);
1388
+ }
1389
+ .foot .spacer { flex: 1; }
1390
+ .foot .note { font-size: 11.5px; color: var(--bc-faint); }
1391
+ /* Quiet, but still legible: this sits in someone else's product, so it identifies
1392
+ where the report goes without competing with their interface. Quiet is bought
1393
+ with colour (--bc-brand-ink sits a step below body text) rather than with a
1394
+ blanket opacity — the BUGCATCH half is only about 3px of cap height at this
1395
+ size, and dimming it on top of that left it as a smudge in both themes. */
1396
+ .brand-credit { display: flex; align-items: center; transition: color 0.15s; }
1397
+ .bc-brand { height: 18px; width: auto; display: block; color: var(--bc-brand-ink); }
1398
+ .brand-credit:hover .bc-brand { color: var(--bc-fg); }
1399
+ .brand-credit:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 3px; border-radius: 4px; }
1400
+ button.ghost, button.primary {
1401
+ padding: 9px 15px; font-size: 13px; font-weight: 600; line-height: 1;
1402
+ border-radius: 9px; cursor: pointer;
1403
+ transition: transform 160ms var(--bc-ease), filter 160ms, background 160ms, border-color 160ms;
1404
+ }
1405
+ button.ghost { background: none; border: 1px solid var(--bc-border); color: var(--bc-muted); }
1406
+ button.ghost:hover { color: var(--bc-fg); border-color: var(--bc-faint); }
1407
+ button.primary {
1408
+ display: inline-flex; align-items: center; gap: 7px;
1409
+ background: var(--bc-accent); border: 1px solid var(--bc-accent); color: var(--bc-on-accent);
1410
+ }
1411
+ button.primary:hover:not(:disabled) { transform: translateY(-1px); filter: brightness(1.07); }
1412
+ button.primary:active:not(:disabled) { transform: none; }
1413
+ button.ghost:focus-visible, button.primary:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: 2px; }
1414
+ button:disabled { opacity: .5; cursor: not-allowed; }
1415
+ button.primary .spinner { border-color: rgba(255,255,255,.4); border-top-color: #fff; width: 13px; height: 13px; }
1416
+
1417
+ /* ---------- success ---------- */
1418
+ /* ---------- your reports ---------- */
1419
+ .history { padding: 6px 20px 18px; max-height: 340px; overflow-y: auto; }
1420
+ /* On the conversation screen the inner thread does the scrolling, so this
1421
+ wrapper must not — two nested scrollers means the composer drifts out of
1422
+ reach exactly when someone reaches for it. */
1423
+ .history.chatting {
1424
+ max-height: none; overflow: visible; display: flex; flex-direction: column;
1425
+ }
1426
+ .history .row {
1427
+ display: flex; align-items: flex-start; gap: 10px;
1428
+ padding: 11px 0; border-bottom: 1px solid var(--bc-border);
1429
+ }
1430
+ .history .row:last-child { border-bottom: 0; }
1431
+ .history .who { flex: 1; min-width: 0; }
1432
+ .history .ttl {
1433
+ display: block; font-size: 13px; font-weight: 550; color: var(--bc-fg);
1434
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1435
+ }
1436
+ .history .sub { display: block; margin-top: 2px; font-size: 11.5px; color: var(--bc-faint); }
1437
+ .history .empty { padding: 26px 0; text-align: center; font-size: 12.5px; color: var(--bc-muted); }
1438
+ /* A history row is a button now — it opens the report — so the UA styling has
1439
+ to go, and the hit area has to read as one. */
1440
+ .history button.row {
1441
+ width: 100%; background: none; border: 0; border-bottom: 1px solid var(--bc-border);
1442
+ font: inherit; color: inherit; text-align: left; cursor: pointer;
1443
+ border-radius: 8px; padding: 11px 8px; margin: 0 -8px; width: calc(100% + 16px);
1444
+ }
1445
+ .history button.row:hover { background: var(--bc-subtle); }
1446
+ .history button.row:focus-visible { outline: 2px solid var(--bc-accent); outline-offset: -2px; }
1447
+
1448
+ /* ---------- one report, and what happened to it ---------- */
1449
+ .report-head {
1450
+ display: grid; grid-template-columns: 1fr auto; gap: 2px 10px;
1451
+ padding: 4px 0 14px; border-bottom: 1px solid var(--bc-border);
1452
+ }
1453
+ .report-head .ttl { font-size: 13.5px; font-weight: 600; color: var(--bc-fg); }
1454
+ .report-head .sub { font-size: 11.5px; color: var(--bc-faint); }
1455
+ .report-head .pill { grid-row: 1 / span 2; align-self: center; }
1456
+
1457
+ /* The thread scrolls; the composer under it does not. Both live inside the
1458
+ panel, which stops scrolling as a whole on this screen — a composer that
1459
+ scrolls away is a composer people think isn't there. */
1460
+ .thread {
1461
+ display: flex; flex-direction: column; gap: 10px;
1462
+ padding: 14px 0 8px; overflow-y: auto; overscroll-behavior: contain;
1463
+ max-height: min(46vh, 330px);
1464
+ }
1465
+
1466
+ /* A status change is not a message and is not drawn as one: no side, no
1467
+ bubble, no author. Centred and quiet, the way every chat marks the things
1468
+ that happened rather than the things that were said. */
1469
+ .sys {
1470
+ align-self: center; max-width: 90%; text-align: center;
1471
+ font-size: 11.5px; line-height: 1.45; color: var(--bc-faint);
1472
+ }
1473
+ .sys .dot {
1474
+ display: inline-block; width: 6px; height: 6px; margin-right: 6px;
1475
+ border-radius: 50%; background: var(--bc-muted); vertical-align: middle;
1476
+ }
1477
+ .sys.k-status .dot { background: #4da3ff; }
1478
+ .sys.k-severity .dot { background: #e8912a; }
1479
+ .sys.k-assignment .dot { background: #8b7dff; }
1480
+ .sys.k-filed .dot { background: var(--bc-faint); }
1481
+ .icon-btn.back { margin-right: 2px; }
1482
+
1483
+ /* ---------- the conversation ----------
1484
+
1485
+ A status change and a message are different things and are drawn as such: a
1486
+ change is a line on the rail above, a message is a bubble with a side. The
1487
+ reporter's own words sit right and accented, the team's left and neutral —
1488
+ the arrangement every chat has taught people to read without a legend. */
1489
+ .msgs { display: flex; flex-direction: column; gap: 10px; padding: 4px 0 6px; }
1490
+ .msg-row { display: flex; flex-direction: column; max-width: 84%; gap: 4px; }
1491
+ .msg-row.mine { align-self: flex-end; align-items: flex-end; }
1492
+ .msg-row.theirs { align-self: flex-start; }
1493
+ .msg-who { font-size: 11px; font-weight: 600; color: var(--bc-faint); padding: 0 2px; }
1494
+ .bubble {
1495
+ padding: 8px 11px; border-radius: 12px; font-size: 12.5px; line-height: 1.5;
1496
+ background: var(--bc-subtle); color: var(--bc-fg);
1497
+ border: 1px solid var(--bc-border);
1498
+ }
1499
+ /* pre-wrap belongs to the text, not the box — on the box it would also render
1500
+ the markup's own indentation. */
1501
+ .msg-text { display: block; white-space: pre-wrap; overflow-wrap: anywhere; }
1502
+ .msg-row.mine .bubble {
1503
+ background: var(--bc-accent); border-color: var(--bc-accent); color: var(--bc-on-accent);
1504
+ border-bottom-right-radius: 4px;
1505
+ }
1506
+ .msg-row.theirs .bubble { border-bottom-left-radius: 4px; }
1507
+ /* A reply nobody wrote. Outlined rather than recoloured — it is still the
1508
+ team's side of the conversation — with the label above carrying the meaning,
1509
+ so it reads the same to someone who cannot see the border at all. */
1510
+ .msg-row.auto .bubble { border-style: dashed; }
1511
+ .msg-who.auto { color: var(--bc-accent); }
1512
+ /* An attachment-only message has no text to wrap, so the bubble would be an
1513
+ empty coloured box around it. */
1514
+ .bubble.silent { padding: 0; background: none; border: 0; }
1515
+ .msg-when { font-size: 10.5px; color: var(--bc-faint); padding: 0 2px; }
1516
+ .msg-row.pending { opacity: .55; }
1517
+ .msg-row.failed .bubble { border: 1px solid var(--bc-danger); }
1518
+
1519
+ .att { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
1520
+ .bubble.silent .att { margin-top: 0; }
1521
+ .att img {
1522
+ display: block; max-width: 100%; max-height: 190px; border-radius: 9px;
1523
+ border: 1px solid var(--bc-border); object-fit: cover; cursor: zoom-in;
1524
+ }
1525
+ .att audio { width: 232px; max-width: 100%; height: 34px; }
1526
+ .att video { max-width: 100%; max-height: 190px; border-radius: 9px; }
1527
+ .att .file {
1528
+ display: flex; align-items: center; gap: 8px; text-decoration: none;
1529
+ padding: 8px 10px; border-radius: 9px; font-size: 12px;
1530
+ background: var(--bc-elevated); border: 1px solid var(--bc-border); color: var(--bc-fg);
1531
+ }
1532
+ .att .file:hover { border-color: var(--bc-accent); }
1533
+ .att .file .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1534
+ .att .file .sz { flex: none; color: var(--bc-faint); font-size: 11px; }
1535
+
1536
+ /* ---------- the composer ---------- */
1537
+ .composer {
1538
+ position: sticky; bottom: 0; margin-top: auto;
1539
+ padding: 10px 0 2px;
1540
+ /* The panel's own background, not --bc-elevated: the two are the same white
1541
+ in light mode but differ in dark, where a raised composer reads as a band
1542
+ laid over the panel rather than the foot of it. */
1543
+ background: var(--bc-bg);
1544
+ border-top: 1px solid var(--bc-border);
1545
+ }
1546
+ .composer .row { display: flex; align-items: flex-end; gap: 8px; }
1547
+ .composer textarea {
1548
+ flex: 1; min-height: 38px; max-height: 120px; resize: none; padding: 9px 11px;
1549
+ font: inherit; font-size: 12.5px; line-height: 1.45;
1550
+ color: var(--bc-fg); background: var(--bc-subtle);
1551
+ border: 1px solid var(--bc-border); border-radius: 10px;
1552
+ }
1553
+ .composer textarea:focus { outline: none; border-color: var(--bc-accent); }
1554
+ .composer .tools { display: flex; gap: 4px; }
1555
+ .composer .tool {
1556
+ width: 34px; height: 34px; display: grid; place-items: center; cursor: pointer;
1557
+ border-radius: 9px; border: 1px solid transparent; background: none;
1558
+ color: var(--bc-muted);
1559
+ }
1560
+ .composer .tool:hover { background: var(--bc-subtle); color: var(--bc-fg); }
1561
+ .composer .tool:disabled { opacity: .4; cursor: default; }
1562
+ .composer .tool.recording { color: var(--bc-danger); background: color-mix(in srgb, var(--bc-danger) 12%, transparent); }
1563
+ .composer .send {
1564
+ width: 34px; height: 34px; flex: none; border-radius: 9px; cursor: pointer;
1565
+ background: var(--bc-accent); border: 1px solid var(--bc-accent); color: #fff;
1566
+ display: grid; place-items: center;
1567
+ }
1568
+ .composer .send:disabled { opacity: .4; cursor: default; }
1569
+ .composer .staged { display: flex; flex-wrap: wrap; gap: 6px; padding: 0 0 8px; }
1570
+ .composer .chip {
1571
+ display: flex; align-items: center; gap: 6px; max-width: 190px;
1572
+ padding: 5px 7px; border-radius: 8px; font-size: 11.5px;
1573
+ background: var(--bc-subtle); border: 1px solid var(--bc-border); color: var(--bc-muted);
1574
+ }
1575
+ .composer .chip .nm { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1576
+ .composer .chip button {
1577
+ flex: none; width: 16px; height: 16px; display: grid; place-items: center;
1578
+ border: 0; border-radius: 4px; background: none; color: var(--bc-faint); cursor: pointer;
1579
+ }
1580
+ .composer .chip button:hover { color: var(--bc-danger); }
1581
+ .composer .chip.uploading { border-style: dashed; }
1582
+ .composer .note { padding: 6px 2px 0; font-size: 11.5px; color: var(--bc-faint); }
1583
+ .composer .note.err { color: var(--bc-danger); }
1584
+ .reccount { font-variant-numeric: tabular-nums; }
1585
+
1586
+ /* Status carries the only colour in this list, so it is the thing the eye
1587
+ lands on — which is what someone opening this screen came to check. */
1588
+ .pill {
1589
+ flex: none; font-size: 10.5px; font-weight: 650; letter-spacing: .03em;
1590
+ padding: 3px 8px; border-radius: 999px; text-transform: uppercase;
1591
+ border: 1px solid currentColor;
1592
+ }
1593
+ .pill.open { color: #e8912a; }
1594
+ .pill.in_progress { color: #4da3ff; }
1595
+ .pill.resolved { color: #35b87f; }
1596
+ .pill.closed { color: var(--bc-faint); }
1597
+
1598
+ /* A call in progress, in place of the form.
1599
+ ---------------------------------------------------------------------------
1600
+ Deliberately not styled as a chat. What is on screen is a transcript of
1601
+ something being said out loud, and bubbles would invite the reporter to type
1602
+ into it — which is the one thing this surface cannot accept. */
1603
+ .call { padding: 20px 22px 14px; animation: bc-fade-in 240ms ease both; }
1604
+ .call-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
1605
+ .call-head strong { font-size: 14.5px; font-weight: 650; }
1606
+ .call-dot {
1607
+ width: 8px; height: 8px; border-radius: 50%; background: var(--bc-accent);
1608
+ animation: bc-call-pulse 1.6s ease-in-out infinite;
1609
+ }
1610
+ .call-dot.off { background: var(--bc-faint); animation: none; }
1611
+ @keyframes bc-call-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.25; } }
1612
+ /* Said before the first word is, because a voice answering is exactly when
1613
+ somebody assumes they have reached a person. */
1614
+ .call-note { margin: 0 0 12px; font-size: 12px; color: var(--bc-muted); line-height: 1.5; }
1615
+
1616
+ /* ---------- a call to a person, on top of the report it is about ---------- */
1617
+ /* A bar rather than its own screen, unlike the agent call. There is nothing to
1618
+ read during this one — no transcript arrives, because nobody is transcribing
1619
+ it — and the thread underneath is exactly what both people are talking about,
1620
+ so taking it off screen would be taking away the only shared reference. */
1621
+ .livecall {
1622
+ display: flex; align-items: center; gap: 9px;
1623
+ margin: 12px 0 0; padding: 9px 11px;
1624
+ border: 1px solid var(--bc-border); border-radius: 10px;
1625
+ background: var(--bc-subtle); font-size: 12.5px; color: var(--bc-fg);
1626
+ }
1627
+ .livecall .who { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1628
+ .livecall .secs { font-variant-numeric: tabular-nums; color: var(--bc-muted); }
1629
+ .livecall .act {
1630
+ padding: 5px 10px; border-radius: 999px; border: 1px solid var(--bc-border);
1631
+ background: var(--bc-elevated); color: var(--bc-fg);
1632
+ font: inherit; font-size: 12px; cursor: pointer;
1633
+ }
1634
+ .livecall .act:hover { background: var(--bc-subtle); }
1635
+ .livecall .act.danger { border-color: transparent; background: var(--bc-danger); color: #fff; }
1636
+ .livecall.over { color: var(--bc-muted); }
1637
+ .call-note code { font-size: 11px; }
1638
+ .call-lines {
1639
+ max-height: 240px; overflow-y: auto; display: flex; flex-direction: column; gap: 7px;
1640
+ padding: 10px 12px; border: 1px solid var(--bc-line); border-radius: 8px;
1641
+ background: var(--bc-surface-2, transparent);
1642
+ }
1643
+ .call-line { font-size: 13px; line-height: 1.55; overflow-wrap: anywhere; }
1644
+ .call-line.agent { color: var(--bc-muted); }
1645
+ .call-line.you { color: var(--bc-ink); font-weight: 500; }
1646
+ .call-line.you::before { content: 'You: '; color: var(--bc-faint); font-weight: 400; }
1647
+ @media (prefers-reduced-motion: reduce) { .call-dot { animation: none; } }
1648
+
1649
+ .done { padding: 34px 24px 38px; text-align: center; animation: bc-fade-in 240ms ease both; }
1650
+ .done .tick {
1651
+ width: 46px; height: 46px; margin: 0 auto 14px; border-radius: 50%;
1652
+ display: grid; place-items: center; font-size: 24px;
1653
+ background: color-mix(in srgb, var(--bc-ok) 14%, transparent); color: var(--bc-ok);
1654
+ animation: bc-pop 420ms var(--bc-ease) both;
1655
+ }
1656
+ .done h3 { margin: 0 0 5px; font-size: 15.5px; font-weight: 650; }
1657
+ .done p { margin: 0; font-size: 12.5px; color: var(--bc-muted); }
1658
+ .done code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
1659
+ /* The likely wait. Set apart from the reference line above it by a rule rather
1660
+ than by weight: one is bookkeeping and the other is the answer to the
1661
+ question the reporter actually has, and they should not read as one
1662
+ paragraph. */
1663
+ .done .eta {
1664
+ margin: 12px auto 0;
1665
+ padding-top: 11px;
1666
+ border-top: 1px solid var(--bc-line);
1667
+ max-width: 30ch;
1668
+ line-height: 1.5;
1669
+ }
1670
+ .done .eta strong { color: var(--bc-ink); font-weight: 600; }
1671
+ /* The same figure on the report the reporter came back to. Sits under the
1672
+ status pill in the header block, muted — it is context, not the headline the
1673
+ status is. */
1674
+ .report-head .eta-line {
1675
+ grid-column: 1 / -1;
1676
+ font-size: 11.5px;
1677
+ color: var(--bc-muted);
1678
+ margin-top: 2px;
1679
+ }
1680
+
1681
+ /* ---------- recording bar ---------- */
1682
+ .recbar {
1683
+ position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
1684
+ z-index: 2147483002;
1685
+ display: flex; align-items: center; gap: 12px;
1686
+ padding: 10px 12px 10px 15px; border-radius: 999px;
1687
+ background: var(--bc-elevated); color: var(--bc-fg);
1688
+ border: 1px solid var(--bc-border); box-shadow: var(--bc-shadow);
1689
+ animation: bc-item-in 300ms var(--bc-ease) both;
1690
+ }
1691
+ .recbar .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--bc-danger); animation: bc-pulse 1.4s ease-in-out infinite; }
1692
+ .recbar .time { font-variant-numeric: tabular-nums; font-size: 13px; font-weight: 600; min-width: 38px; }
1693
+ .recbar .hint { font-size: 12px; color: var(--bc-muted); }
1694
+ .recbar button { padding: 7px 13px; font-size: 12.5px; font-weight: 600; border-radius: 999px; cursor: pointer; }
1695
+ .recbar .stop { background: var(--bc-danger); border: 1px solid var(--bc-danger); color: #fff; }
1696
+ .recbar .stop:hover { filter: brightness(1.08); }
1697
+
1698
+ @media (max-width: 520px) {
1699
+ .panel { width: calc(100vw - 24px); max-width: none; }
1700
+ .scrim.at-right .panel, .scrim.at-left .panel { left: 12px; right: 12px; }
1701
+ .recbar .hint { display: none; }
1702
+ }
1703
+
1704
+ /* ---------- phones ----------------------------------------------------------
1705
+ This widget runs inside a shadow root on somebody else's site, so its own
1706
+ stylesheet is the whole story: the host page cannot correct any of this, and
1707
+ anything that goes wrong here goes wrong on their site, in front of their
1708
+ visitor, at the moment that visitor is already annoyed enough to file a bug.
1709
+ -------------------------------------------------------------------------- */
1710
+
1711
+ /* 100vh on a phone is the viewport with the browser chrome hidden, so the
1712
+ panel could be taller than the space it actually has and put "Send report"
1713
+ under the address bar. */
1714
+ @supports (height: 100dvh) {
1715
+ .panel { max-height: calc(100dvh - var(--bc-dock-h, 84px) - 52px); }
1716
+ }
1717
+
1718
+ /* The dock sits in a corner of a page the widget does not own. On a notched
1719
+ phone that corner is under the home indicator, and in landscape it is under
1720
+ the camera housing. env() is 0 everywhere else, so the offsets are unchanged
1721
+ on hardware that has no insets. */
1722
+ .dock {
1723
+ padding-bottom: env(safe-area-inset-bottom, 0px);
1724
+ padding-right: env(safe-area-inset-right, 0px);
1725
+ padding-left: env(safe-area-inset-left, 0px);
1726
+ }
1727
+
1728
+ @media (pointer: coarse) {
1729
+ /* iOS zooms the whole page when a field it focuses is under 16px, and does
1730
+ not zoom back out. On the host's site that is not the widget's page to
1731
+ zoom — the visitor is left on a magnified checkout wondering what they
1732
+ touched. !important because this is a property of the device, not a
1733
+ preference about type. */
1734
+ input, textarea, select { font-size: 16px !important; }
1735
+
1736
+ /* 44px is the floor a thumb hits reliably. Every control in this form was
1737
+ under it, on the one surface in the product designed for someone who is
1738
+ not an operator and gets no second attempt. */
1739
+ .launcher { min-height: 48px; padding: 14px 20px; }
1740
+ .seg button { min-height: 44px; padding: 12px 8px; }
1741
+ .tool { min-height: 44px; padding: 12px 14px; }
1742
+ button.primary, .foot button { min-height: 44px; padding: 12px 18px; }
1743
+ textarea { min-height: 104px; }
1744
+
1745
+ /* The theme switch and the close button are 28px glyphs whose boxes cannot
1746
+ grow without moving the header text beside them, so each keeps its size
1747
+ and gets an invisible target drawn around it instead. */
1748
+ .themer button, .head .icon-btn, .thumb .x, .brand-credit { position: relative; }
1749
+ .themer button::after, .head .icon-btn::after, .thumb .x::after, .brand-credit::after {
1750
+ content: '';
1751
+ position: absolute;
1752
+ inset: -9px;
1753
+ }
1754
+ /* The close button is the one people reach for in a hurry, and it sits in
1755
+ the corner where a thumb is least accurate. */
1756
+ .head .icon-btn::after { inset: -12px; }
1757
+ /* The themer's three buttons sit side by side, so their expanders would
1758
+ overlap each other without a gap to grow into. */
1759
+ .themer { gap: 4px; }
1760
+
1761
+ /* Thumb-sized controls make the form taller, which on a short phone pushed
1762
+ "Send report" below the fold of a panel that scrolls — so the one button
1763
+ the whole widget exists for was reachable only by scrolling past the
1764
+ attachment row. It stays on screen instead. */
1765
+ .foot {
1766
+ position: sticky;
1767
+ bottom: 0;
1768
+ margin-top: 16px;
1769
+ /* The panel scrolls under this, so the bar cannot be translucent. */
1770
+ background: var(--bc-subtle);
1771
+ padding-bottom: max(14px, env(safe-area-inset-bottom, 0px));
1772
+ }
1773
+ }
1774
+
1775
+ /* Hover is a lie on a touchscreen: it latches on tap and stays. The launcher's
1776
+ lift in particular was left applied after the panel closed. */
1777
+ @media (hover: none) {
1778
+ .launcher:hover { transform: none; filter: none; box-shadow: 0 6px 18px rgba(10,12,16,.22), 0 1px 3px rgba(10,12,16,.16); }
1779
+ .tool:hover, .themer button:hover { border-color: var(--bc-border); }
1780
+ }
1781
+ `;
1782
+ function Ca(t) {
1783
+ const e = document.createElement("div");
1784
+ e.setAttribute("data-bugcatch", "widget");
1785
+ let n = xa() || Ea(t.config.theme);
1786
+ e.className = qt(n);
1787
+ const a = e.attachShadow({ mode: "open" }), o = document.createElement("style");
1788
+ o.textContent = Sa(t.config.accent || "#4F46E5"), a.appendChild(o);
1789
+ const r = Ht[t.config.widgetPosition] ? t.config.widgetPosition : "bottom-right", s = document.createElement("div");
1790
+ s.className = `dock ${r.includes("right") ? "at-right" : "at-left"}` + (r.startsWith("top") ? " at-top" : ""), s.style.cssText = Ht[r];
1791
+ const l = document.createElement("div");
1792
+ l.className = "themer", l.setAttribute("role", "group"), l.setAttribute("aria-label", "Report widget theme"), l.innerHTML = an.map((b) => `<button type="button" data-theme="${b.value}" title="${b.label}" aria-label="${b.label}" aria-pressed="${b.value === n}">${b.icon}</button>`).join(""), l.addEventListener("click", (b) => {
1793
+ const d = b.target.closest("button[data-theme]");
1794
+ !d || d.dataset.theme === n || (n = d.dataset.theme, e.className = qt(n), ka(n), l.querySelectorAll("button").forEach((w) => {
1795
+ w.setAttribute("aria-pressed", String(w.dataset.theme === n));
1796
+ }));
1797
+ });
1798
+ const u = document.createElement("button");
1799
+ u.className = "launcher", u.type = "button", u.innerHTML = `${H.bug}<span>Report a bug</span>`, u.setAttribute("aria-haspopup", "dialog"), u.setAttribute("aria-expanded", "false");
1800
+ let g = null;
1801
+ return u.addEventListener("click", () => {
1802
+ if (g) {
1803
+ g.close();
1804
+ return;
1805
+ }
1806
+ s.classList.add("over"), u.setAttribute("aria-expanded", "true"), g = Ta({
1807
+ root: a,
1808
+ host: e,
1809
+ dock: s,
1810
+ client: t,
1811
+ onClose: () => {
1812
+ g = null, s.classList.remove("over"), u.setAttribute("aria-expanded", "false");
1813
+ }
1814
+ });
1815
+ }), s.append(l, u), a.appendChild(s), document.body.appendChild(e), () => {
1816
+ g == null || g.destroy(), e.remove();
1817
+ };
1818
+ }
1819
+ function Ea(t) {
1820
+ return t === "dark" || t === "light" ? t : "auto";
1821
+ }
1822
+ function qt(t) {
1823
+ return t === "dark" ? "bc-dark" : t === "light" ? "bc-light" : "bc-auto";
1824
+ }
1825
+ function Ta({ root: t, host: e, dock: n, client: a, onClose: o }) {
1826
+ var Ct, Et, Tt, $t, Bt;
1827
+ const r = a.config, s = r.attachments !== !1, l = s && Un(), u = s && qn(), g = [];
1828
+ let b = "MEDIUM", d = null, w = !1;
1829
+ const h = document.createElement("div");
1830
+ h.className = `scrim ${n.classList.contains("at-left") ? "at-left" : "at-right"} ${n.classList.contains("at-top") ? "at-top" : "at-bottom"}`, h.style.setProperty("--bc-dock-h", `${Math.round(n.getBoundingClientRect().height)}px`), h.innerHTML = `
1831
+ <div class="panel" role="dialog" aria-modal="true" aria-labelledby="bc-h">
1832
+ <div class="head">
1833
+ <div class="mark">${H.bug}</div>
1834
+ <div class="titles">
1835
+ <h2 id="bc-h">Report a bug</h2>
1836
+ <p>We'll attach what the page was doing, so you don't have to explain it.</p>
1837
+ </div>
1838
+ <button class="icon-btn" type="button" data-act="close" aria-label="Close">${H.close}</button>
1839
+ </div>
1840
+ <div class="body">
1841
+ <div class="slot"></div>
1842
+ <div class="whoami" hidden></div>
1843
+ <div class="known" hidden></div>
1844
+ <div class="compose">
1845
+ <div class="field">
1846
+ <label for="bc-title">What went wrong?</label>
1847
+ <input id="bc-title" type="text" placeholder="Checkout button does nothing" autocomplete="off" />
1848
+ </div>
1849
+ <div class="field">
1850
+ <label for="bc-desc">Any detail that helps <span style="font-weight:450;color:var(--bc-faint)">(optional)</span></label>
1851
+ <textarea id="bc-desc" placeholder="I clicked Pay and nothing happened."></textarea>
1852
+ </div>
1853
+ <div class="field">
1854
+ <label id="bc-prio-l">Severity</label>
1855
+ <div class="seg" role="group" aria-labelledby="bc-prio-l">
1856
+ ${ya.map((i) => `
1857
+ <button type="button" data-prio="${i.value}" aria-pressed="${i.value === b}">${i.label}</button>
1858
+ `).join("")}
1859
+ </div>
1860
+ </div>
1861
+ ${s ? `
1862
+ <div class="field">
1863
+ <label>Attach <span style="font-weight:450;color:var(--bc-faint)">(optional)</span></label>
1864
+ <div class="tools">
1865
+ ${l ? `<button class="tool" type="button" data-act="shot">${H.camera}Screenshot</button>` : ""}
1866
+ ${u ? `<button class="tool" type="button" data-act="rec">${H.video}Record</button>` : ""}
1867
+ <button class="tool" type="button" data-act="file">${H.paperclip}File</button>
1868
+ </div>
1869
+ <div class="thumbs"></div>
1870
+ <input type="file" accept="${Pe.join(",")}" multiple hidden />
1871
+ </div>` : ""}
1872
+ </div>
1873
+ </div>
1874
+ <div class="foot">
1875
+ <a class="brand-credit" href="https://bugcatch.sphoro.com" target="_blank" rel="noopener noreferrer" aria-label="BugCatch">${va}</a>
1876
+ <span class="note"></span>
1877
+ <span class="spacer"></span>
1878
+ ${a.hasReports() ? '<button class="ghost" type="button" data-act="history">Your reports</button>' : ""}
1879
+ <button class="ghost" type="button" data-act="cancel">Cancel</button>
1880
+ <!-- Hidden until the API confirms this project can take a call, which
1881
+ most cannot. Drawn here rather than as a second launcher because it
1882
+ is the same errand as Send report — the difference is only whether
1883
+ the reporter would rather say it than type it. -->
1884
+ <button class="ghost" type="button" data-act="call" hidden>Talk to us</button>
1885
+ <button class="primary" type="button" data-act="send">Send report</button>
1886
+ </div>
1887
+ </div>`;
1888
+ const S = (i) => h.querySelector(i), z = S("#bc-title"), F = S(".slot"), k = S(".thumbs"), y = h.querySelector("input[type=file]"), x = S('[data-act="send"]'), C = S('[data-act="call"]'), R = S(".note"), Y = S(".compose"), O = S(".whoami"), K = S(".known"), B = S('[data-act="history"]'), X = document.activeElement, me = document.documentElement.style, Xe = me.overflow;
1889
+ me.overflow = "hidden";
1890
+ let ve = !1, xe = null;
1891
+ function Ae() {
1892
+ var i;
1893
+ ve || (ve = !0, d == null || d.cancel(), xe == null || xe(), g.forEach((c) => c.previewUrl && URL.revokeObjectURL(c.previewUrl)), me.overflow = Xe, document.removeEventListener("keydown", re, !0), h.remove(), (i = t.querySelector(".recbar")) == null || i.remove(), n.hidden = !1, e.style.opacity = "", o == null || o());
1894
+ }
1895
+ function q() {
1896
+ var i;
1897
+ if (!ve) {
1898
+ h.classList.add("closing"), setTimeout(Ae, 160);
1899
+ try {
1900
+ (i = X == null ? void 0 : X.focus) == null || i.call(X);
1901
+ } catch {
1902
+ }
1903
+ }
1904
+ }
1905
+ function re(i) {
1906
+ if (i.key === "Escape") {
1907
+ if (i.stopPropagation(), d) {
1908
+ be({ keep: !1 });
1909
+ return;
1910
+ }
1911
+ w || q();
1912
+ return;
1913
+ }
1914
+ if (i.key !== "Tab" || d) return;
1915
+ const c = [...h.querySelectorAll(
1916
+ "button:not(:disabled), input:not([hidden]), textarea, select, [href]"
1917
+ )].filter((E) => E.offsetParent !== null || E === document.activeElement);
1918
+ if (!c.length) return;
1919
+ const p = c[0], m = c[c.length - 1], M = h.getRootNode().activeElement;
1920
+ i.shiftKey && M === p ? (i.preventDefault(), m.focus()) : !i.shiftKey && M === m && (i.preventDefault(), p.focus());
1921
+ }
1922
+ document.addEventListener("keydown", re, !0), h.addEventListener("click", (i) => {
1923
+ i.target === h && !w && q();
1924
+ }), S('[data-act="close"]').addEventListener("click", q), S('[data-act="cancel"]').addEventListener("click", q), h.querySelectorAll("[data-prio]").forEach((i) => {
1925
+ i.addEventListener("click", () => {
1926
+ b = i.dataset.prio, h.querySelectorAll("[data-prio]").forEach((c) => {
1927
+ c.setAttribute("aria-pressed", String(c === i));
1928
+ });
1929
+ });
1930
+ });
1931
+ function V(i, c = "err") {
1932
+ F.innerHTML = i ? `<div class="msg ${c}">${T(i)}</div>` : "";
1933
+ }
1934
+ function oe() {
1935
+ const i = g.some((c) => c.status === "uploading");
1936
+ x.disabled = w || i, R.textContent = i ? "Waiting for the upload to finish…" : "", h.querySelectorAll(".tool").forEach((c) => {
1937
+ c.disabled = w || g.length >= (r.maxAttachments ?? 5);
1938
+ });
1939
+ }
1940
+ function $e(i) {
1941
+ var E;
1942
+ const c = i.el, p = i.kind === "VIDEO" ? "Video" : "Shot", m = i.kind === "VIDEO" ? `<video src="${i.previewUrl}" muted playsinline preload="metadata"></video>` : `<img src="${i.previewUrl}" alt="" />`;
1943
+ let M = "";
1944
+ i.status === "uploading" ? M = `<div class="veil"><div class="spinner"></div><div class="bar"><i style="width:${Math.round(i.progress * 100)}%"></i></div></div>` : i.status === "failed" && (M = `<div class="veil err"><button type="button" data-act="retry" aria-label="Retry upload">${H.retry}</button>Failed</div>`), c.innerHTML = `
1945
+ ${m}
1946
+ <span class="kind">${p} · ${Te(i.size)}</span>
1947
+ <button class="drop" type="button" data-act="drop" aria-label="Remove ${p.toLowerCase()}">${H.close}</button>
1948
+ ${M}`, c.querySelector('[data-act="drop"]').addEventListener("click", () => Qe(i)), (E = c.querySelector('[data-act="retry"]')) == null || E.addEventListener("click", () => Be(i));
1949
+ }
1950
+ function Qe(i) {
1951
+ const c = g.indexOf(i);
1952
+ c >= 0 && g.splice(c, 1), i.previewUrl && URL.revokeObjectURL(i.previewUrl), i.el.remove(), oe();
1953
+ }
1954
+ async function Be(i) {
1955
+ i.status = "uploading", i.progress = 0, $e(i), oe();
1956
+ try {
1957
+ const { storageKey: c, mime: p } = await a.uploadAttachment(i.blob, i.mime, (m) => {
1958
+ i.progress = m;
1959
+ const M = i.el.querySelector(".bar i");
1960
+ M && (M.style.width = `${Math.round(m * 100)}%`);
1961
+ });
1962
+ i.storageKey = c, i.mime = p, i.status = "ready";
1963
+ } catch (c) {
1964
+ i.status = "failed", V(c.message.replace("[BugCatch] ", ""));
1965
+ }
1966
+ $e(i), oe();
1967
+ }
1968
+ function Me({ blob: i, mime: c, kind: p }) {
1969
+ const m = r.maxAttachmentBytes ?? 26214400;
1970
+ if (i.size > m) {
1971
+ V(`That file is ${Te(i.size)} — the limit is ${Te(m)}.`);
1972
+ return;
1973
+ }
1974
+ const M = Ke(c || i.type, Pe);
1975
+ if (!M) {
1976
+ V("Only PNG, JPEG, WebP, WebM and MP4 files can be attached to a report.");
1977
+ return;
1978
+ }
1979
+ const E = {
1980
+ blob: i,
1981
+ kind: p,
1982
+ mime: M,
1983
+ size: i.size,
1984
+ previewUrl: URL.createObjectURL(i),
1985
+ status: "uploading",
1986
+ progress: 0,
1987
+ storageKey: null,
1988
+ el: document.createElement("div")
1989
+ };
1990
+ E.el.className = "thumb", g.push(E), k.appendChild(E.el), V(""), Be(E);
1991
+ }
1992
+ async function Je(i) {
1993
+ e.style.opacity = "0", e.style.pointerEvents = "none", await new Promise((c) => requestAnimationFrame(() => requestAnimationFrame(c)));
1994
+ try {
1995
+ return await i();
1996
+ } finally {
1997
+ e.style.opacity = "", e.style.pointerEvents = "";
1998
+ }
1999
+ }
2000
+ (Ct = h.querySelector('[data-act="shot"]')) == null || Ct.addEventListener("click", async () => {
2001
+ try {
2002
+ const i = await Je(Dn);
2003
+ Me(i);
2004
+ } catch (i) {
2005
+ (i == null ? void 0 : i.name) !== "NotAllowedError" && V(`Screenshot failed: ${i.message}`);
2006
+ }
2007
+ }), (Et = h.querySelector('[data-act="file"]')) == null || Et.addEventListener("click", () => y.click()), y == null || y.addEventListener("change", () => {
2008
+ const i = (r.maxAttachments ?? 5) - g.length;
2009
+ [...y.files].slice(0, Math.max(i, 0)).forEach((c) => {
2010
+ Me({
2011
+ blob: c,
2012
+ mime: c.type,
2013
+ kind: c.type.startsWith("video/") ? "VIDEO" : "SCREENSHOT"
2014
+ });
2015
+ }), y.value = "";
2016
+ }), (Tt = h.querySelector('[data-act="rec"]')) == null || Tt.addEventListener("click", async () => {
2017
+ const i = r.maxRecordingMs ?? 12e4;
2018
+ let c;
2019
+ try {
2020
+ d = await Nn({
2021
+ maxMs: i,
2022
+ onTick: (p) => {
2023
+ c && (c.querySelector(".time").textContent = qe(p));
2024
+ },
2025
+ onAutoStop: () => {
2026
+ d && be({ keep: !0 });
2027
+ }
2028
+ });
2029
+ } catch (p) {
2030
+ (p == null ? void 0 : p.name) !== "NotAllowedError" && V(`Recording failed: ${p.message}`);
2031
+ return;
2032
+ }
2033
+ h.style.display = "none", n.hidden = !0, c = document.createElement("div"), c.className = "recbar", c.innerHTML = `
2034
+ <span class="dot"></span>
2035
+ <span class="time">0:00</span>
2036
+ <span class="hint">Recording — up to ${qe(Math.round(i / 1e3))}</span>
2037
+ <button class="stop" type="button">Stop</button>
2038
+ <button class="ghost" type="button">Discard</button>`, c.querySelector(".stop").addEventListener("click", () => be({ keep: !0 })), c.querySelector(".ghost").addEventListener("click", () => be({ keep: !1 })), t.appendChild(c), c.querySelector(".stop").focus();
2039
+ });
2040
+ async function be({ keep: i }) {
2041
+ var p;
2042
+ const c = d;
2043
+ if (c) {
2044
+ if (d = null, (p = t.querySelector(".recbar")) == null || p.remove(), h.style.display = "", n.hidden = !1, !i) {
2045
+ c.cancel(), z.focus();
2046
+ return;
2047
+ }
2048
+ try {
2049
+ const m = await c.stop();
2050
+ m.blob.size ? Me(m) : V("That recording came back empty — nothing was captured.");
2051
+ } catch (m) {
2052
+ V(`Recording failed: ${m.message}`);
2053
+ }
2054
+ z.focus();
2055
+ }
2056
+ }
2057
+ x.addEventListener("click", async () => {
2058
+ const i = z.value;
2059
+ if (!i.trim()) {
2060
+ V("Please describe what went wrong."), z.classList.add("invalid"), z.focus(), setTimeout(() => z.classList.remove("invalid"), 500);
2061
+ return;
2062
+ }
2063
+ w = !0, V(""), oe();
2064
+ const c = x.innerHTML;
2065
+ x.innerHTML = '<span class="spinner"></span>Sending…';
2066
+ try {
2067
+ const p = await a.report({
2068
+ title: i,
2069
+ description: S("#bc-desc").value,
2070
+ priority: b,
2071
+ attachments: g.filter((m) => m.status === "ready").map((m) => ({ type: m.kind, storageKey: m.storageKey, mime: m.mime }))
2072
+ });
2073
+ kt(p);
2074
+ } catch (p) {
2075
+ w = !1, x.innerHTML = c, V(p.message.replace("[BugCatch] ", "")), oe(), f.mode !== "off" && tt().catch(() => {
2076
+ });
2077
+ }
2078
+ }), ($t = h.querySelector('[data-act="history"]')) == null || $t.addEventListener("click", St);
2079
+ let ie = [], we = "", ye = null, le = null;
2080
+ function Re() {
2081
+ r.knownIssues !== !1 && a.knownIssues().then((i) => {
2082
+ ie = i, de();
2083
+ }).catch(() => {
2084
+ });
2085
+ }
2086
+ let je = null;
2087
+ function Ge(i) {
2088
+ if (r.knownIssues === !1) return;
2089
+ clearTimeout(je), le == null || le.abort();
2090
+ const c = (i || "").trim();
2091
+ if (c.length < 3) {
2092
+ Re();
2093
+ return;
2094
+ }
2095
+ je = setTimeout(() => {
2096
+ le = new AbortController(), a.matchKnownIssues(c, { signal: le.signal }).then((p) => {
2097
+ p.length && (ie = p, de());
2098
+ }).catch(() => {
2099
+ });
2100
+ }, 250);
2101
+ }
2102
+ function de() {
2103
+ if (!ie.length) {
2104
+ K.hidden = !0, K.innerHTML = "";
2105
+ return;
2106
+ }
2107
+ K.hidden = !1, K.innerHTML = `
2108
+ <p class="known-h">Already reported</p>
2109
+ ${ie.map((i) => `
2110
+ <div class="issue ${i.joined ? "is-joined" : ""}">
2111
+ <span class="who">
2112
+ <span class="t">${T(i.title)}</span>
2113
+ <span class="meta">${T(ze(i))}</span>
2114
+ </span>
2115
+ <button class="join" type="button" data-join="${T(i.id)}"
2116
+ ${i.joined ? "disabled" : ""}>${i.joined ? "Following" : "Me too"}</button>
2117
+ </div>
2118
+ ${ye === i.id ? `
2119
+ <div class="ask">
2120
+ <input type="email" class="join-email" placeholder="you@example.com"
2121
+ aria-label="Your email, so we can tell you when this is fixed" />
2122
+ <button class="ghost" type="button" data-join-confirm="${T(i.id)}">Follow</button>
2123
+ </div>` : ""}
2124
+ `).join("")}
2125
+ ${we ? `<p class="err">${T(we)}</p>` : ""}`;
2126
+ }
2127
+ function ze(i) {
2128
+ const c = i.status === "IN_PROGRESS" ? "Being worked on now" : "Not fixed yet", p = i.affectedCount === 0 ? "" : ` · ${i.affectedCount} ${i.affectedCount === 1 ? "person has" : "people have"} said this is happening to them`;
2129
+ return `${c} · reported ${We(i.since)}${p}`;
2130
+ }
2131
+ K.addEventListener("click", async (i) => {
2132
+ var E, se, P, ee, te;
2133
+ const c = i.target.closest("[data-join]"), p = i.target.closest("[data-join-confirm]");
2134
+ if (!c && !p) return;
2135
+ const m = (p || c).getAttribute(p ? "data-join-confirm" : "data-join");
2136
+ we = "";
2137
+ const M = ((se = (E = K.querySelector(".join-email")) == null ? void 0 : E.value) == null ? void 0 : se.trim()) || ((P = a.signedInAs()) == null ? void 0 : P.email) || ((ee = a.config.user) == null ? void 0 : ee.email);
2138
+ if (!M) {
2139
+ ye = m, de(), (te = K.querySelector(".join-email")) == null || te.focus();
2140
+ return;
2141
+ }
2142
+ try {
2143
+ const Q = await a.meToo(m, { email: M });
2144
+ ye = null, ie = ie.map((D) => D.id === m ? { ...D, joined: !0, affectedCount: Q.affectedCount ?? D.affectedCount } : D), de();
2145
+ } catch (Q) {
2146
+ we = Q.message.replace("[BugCatch] ", ""), de();
2147
+ }
2148
+ }), Re(), z.addEventListener("input", () => Ge(z.value));
2149
+ let ke = null;
2150
+ function Oe() {
2151
+ a.callKind().then((i) => {
2152
+ i && (ke = i, C.textContent = i === "live" ? "Talk to the team" : "Talk to us", C.hidden = Se());
2153
+ }).catch(() => {
2154
+ });
2155
+ }
2156
+ Oe();
2157
+ let f = { mode: a.lastLoginMode || "off", signedIn: null }, v = "idle", $ = ((Bt = a.config.user) == null ? void 0 : Bt.email) || "", _ = "", I = !1, G = "";
2158
+ const Se = () => f.mode === "required" && !f.signedIn;
2159
+ function Ce() {
2160
+ const i = Se();
2161
+ Y.hidden = i, x.hidden = i, B && (B.hidden = i), i ? C.hidden = !0 : ke && (C.hidden = !1);
2162
+ }
2163
+ function Ee() {
2164
+ var c;
2165
+ if (f.mode === "off") {
2166
+ O.hidden = !0, O.innerHTML = "";
2167
+ return;
2168
+ }
2169
+ O.hidden = !1, O.classList.toggle("asking", v !== "idle");
2170
+ const i = G ? `<div class="after err">${T(G)}</div>` : "";
2171
+ if (v === "idle") {
2172
+ if (f.signedIn) {
2173
+ O.innerHTML = `<span class="who">Signed in as <strong>${T(f.signedIn.email)}</strong></span><button type="button" class="link" data-act="signout">Sign out</button>`;
2174
+ return;
2175
+ }
2176
+ const p = (c = a.config.user) == null ? void 0 : c.email;
2177
+ O.innerHTML = `<span class="who">${f.mode === "required" ? "Sign in to report a bug here." : p ? `Reporting as <strong>${T(p)}</strong>.` : "Reporting anonymously — sign in and we can tell you what happens next."}</span><button type="button" class="link" data-act="signin">Sign in</button>`;
2178
+ return;
2179
+ }
2180
+ if (v === "email") {
2181
+ O.innerHTML = `
2182
+ <p class="why">Sign in with any email address — we'll send you a code.
2183
+ It's how you hear back about this report${f.mode === "required" ? ", and this project asks for it" : ""}.</p>
2184
+ <div class="row">
2185
+ <input type="email" data-fld="email" value="${T($)}"
2186
+ placeholder="you@example.com" autocomplete="email" spellcheck="false"
2187
+ aria-label="Your email address" ${I ? "disabled" : ""} />
2188
+ <button type="button" class="tool" data-act="code" ${I ? "disabled" : ""}>
2189
+ ${I ? "Sending…" : "Send code"}
2190
+ </button>
2191
+ </div>
2192
+ ${i}
2193
+ ${f.mode === "optional" ? '<div class="after"><button type="button" class="link" data-act="skip">Not now</button></div>' : ""}`;
2194
+ return;
2195
+ }
2196
+ O.innerHTML = `
2197
+ <p class="why">We sent a code to <strong>${T($)}</strong>. It expires in ten minutes.</p>
2198
+ <div class="row">
2199
+ <input type="text" class="code" data-fld="code" value="${T(_)}"
2200
+ placeholder="123456" inputmode="numeric" autocomplete="one-time-code"
2201
+ maxlength="8" aria-label="The code we emailed you" ${I ? "disabled" : ""} />
2202
+ <button type="button" class="tool" data-act="confirm" ${I ? "disabled" : ""}>
2203
+ ${I ? "Checking…" : "Sign in"}
2204
+ </button>
2205
+ </div>
2206
+ ${i}
2207
+ <div class="after">
2208
+ <button type="button" class="link" data-act="resend" ${I ? "disabled" : ""}>Send it again</button>
2209
+ <button type="button" class="link" data-act="signin-back" ${I ? "disabled" : ""}>Use another address</button>
2210
+ </div>`;
2211
+ }
2212
+ function ae({ focus: i = !1 } = {}) {
2213
+ var p;
2214
+ if (Ee(), Ce(), !i || I) return;
2215
+ const c = O.querySelector("[data-fld]");
2216
+ c && (c.focus(), (p = c.setSelectionRange) == null || p.call(c, c.value.length, c.value.length));
2217
+ }
2218
+ O.addEventListener("input", (i) => {
2219
+ const c = i.target.closest("[data-fld]");
2220
+ c && (c.dataset.fld === "email" ? $ = c.value : _ = c.value);
2221
+ }), O.addEventListener("keydown", (i) => {
2222
+ i.key !== "Enter" || !i.target.closest("[data-fld]") || (i.preventDefault(), v === "email" ? et() : xt());
2223
+ });
2224
+ async function et({ resent: i = !1 } = {}) {
2225
+ const c = $.trim();
2226
+ if (!c) {
2227
+ G = "Enter the address to send the code to.", ae({ focus: !0 });
2228
+ return;
2229
+ }
2230
+ I = !0, G = "", ae();
2231
+ try {
2232
+ await a.signIn(c), $ = c, i || (_ = ""), v = "code";
2233
+ } catch (p) {
2234
+ G = p.message;
2235
+ } finally {
2236
+ I = !1, ae({ focus: !0 });
2237
+ }
2238
+ }
2239
+ async function xt() {
2240
+ const i = _.trim();
2241
+ if (!i) {
2242
+ G = "Enter the code we emailed you.", ae({ focus: !0 });
2243
+ return;
2244
+ }
2245
+ I = !0, G = "", ae();
2246
+ try {
2247
+ const c = await a.confirmSignIn($.trim(), i);
2248
+ f.signedIn = c, v = "idle", _ = "", V(""), Oe();
2249
+ } catch (c) {
2250
+ G = c.message;
2251
+ } finally {
2252
+ I = !1, ae({ focus: v !== "idle" });
2253
+ }
2254
+ }
2255
+ O.addEventListener("click", async (i) => {
2256
+ const c = i.target.closest("button[data-act]");
2257
+ if (!c) return;
2258
+ const p = c.dataset.act;
2259
+ if (p === "signin") {
2260
+ v = "email", G = "", ae({ focus: !0 });
2261
+ return;
2262
+ }
2263
+ if (p === "skip") {
2264
+ v = "idle", G = "", ae();
2265
+ return;
2266
+ }
2267
+ if (p === "signin-back") {
2268
+ v = "email", G = "", _ = "", ae({ focus: !0 });
2269
+ return;
2270
+ }
2271
+ if (p === "code") {
2272
+ et();
2273
+ return;
2274
+ }
2275
+ if (p === "resend") {
2276
+ et({ resent: !0 });
2277
+ return;
2278
+ }
2279
+ if (p === "confirm") {
2280
+ xt();
2281
+ return;
2282
+ }
2283
+ if (p === "signout") {
2284
+ I = !0, ae();
2285
+ try {
2286
+ await a.signOut();
2287
+ } finally {
2288
+ f.signedIn = null, I = !1, v = f.mode === "required" ? "email" : "idle", ae({ focus: v !== "idle" });
2289
+ }
2290
+ }
2291
+ });
2292
+ async function tt({ announce: i = !1 } = {}) {
2293
+ const c = await a.loginState();
2294
+ f = { mode: c.mode, signedIn: c.signedIn }, a.lastLoginMode = c.mode, Se() && v === "idle" && (v = "email"), f.signedIn && (v = "idle"), i && c.signedOutReason && V(c.signedOutReason, "info"), ae();
2295
+ }
2296
+ tt({ announce: !0 }).catch(() => {
2297
+ }), C.addEventListener("click", async () => {
2298
+ w = !0, V(""), oe(), C.disabled = !0, C.innerHTML = `<span class="spinner"></span>${ke === "live" ? "Ringing…" : "Connecting…"}`;
2299
+ try {
2300
+ const i = await a.startCall();
2301
+ bn(i);
2302
+ } catch (i) {
2303
+ w = !1, C.disabled = !1, C.textContent = ke === "live" ? "Talk to the team" : "Talk to us", oe(), i.filed ? kt(i.filed) : (V(i.message.replace("[BugCatch] ", "")), f.mode !== "off" && tt().catch(() => {
2304
+ }));
2305
+ }
2306
+ });
2307
+ function bn(i) {
2308
+ const c = h.querySelector(".panel"), p = i != null && i.bugId ? String(i.bugId).slice(0, 8) : null, m = !!(i != null && i.live);
2309
+ c.innerHTML = `
2310
+ <div class="call">
2311
+ <div class="call-head">
2312
+ <span class="call-dot" aria-hidden="true"></span>
2313
+ <strong class="call-state">${m ? "Ringing…" : "Connecting…"}</strong>
2314
+ </div>
2315
+ <p class="call-note">
2316
+ ${m ? `We're ringing the team now. Someone will pick up if they're free — your report${p ? ` <code>${T(p)}</code>` : ""} is already filed either way, so nothing is lost if nobody answers.` : `You're talking to our automated assistant, not a person. What you say is
2317
+ added to your report${p ? ` <code>${T(p)}</code>` : ""} for the team to read.`}
2318
+ </p>
2319
+ <div class="call-lines" role="log" aria-live="polite"></div>
2320
+ <div class="foot">
2321
+ <span class="spacer"></span>
2322
+ <button class="primary" type="button" data-act="hangup">${m ? "Cancel" : "Hang up"}</button>
2323
+ </div>
2324
+ </div>`;
2325
+ const M = c.querySelector(".call-state"), E = c.querySelector(".call-lines"), se = c.querySelector(".call-dot"), P = m ? {
2326
+ ringing: "Ringing the team…",
2327
+ connecting: "Someone answered — connecting…",
2328
+ connected: "Connected",
2329
+ missed: "Nobody was free",
2330
+ ended: "Call ended",
2331
+ failed: "The call dropped"
2332
+ } : {
2333
+ connecting: "Connecting…",
2334
+ listening: "Listening",
2335
+ speaking: "Assistant is speaking",
2336
+ ended: "Call ended",
2337
+ failed: "The call dropped"
2338
+ }, ee = ["ended", "failed", "missed"], te = a.on("call", ({ state: J }) => {
2339
+ if (M.textContent = P[J] || J, se.classList.toggle("off", ee.includes(J)), ee.includes(J) && (c.querySelector('[data-act="hangup"]').textContent = "Close", J === "missed")) {
2340
+ const j = c.querySelector(".call-note");
2341
+ j && (j.textContent = "Nobody was free to pick up just now. Your report is filed and the team will reply in the chat.");
2342
+ }
2343
+ });
2344
+ let Q = null;
2345
+ const D = a.on("call-transcript", ({ role: J, text: j, final: W }) => {
2346
+ !j && !W || (J === "agent" ? ((!Q || Q.dataset.role !== "agent") && (Q = document.createElement("div"), Q.className = "call-line agent", Q.dataset.role = "agent", E.appendChild(Q)), j && (Q.textContent += j), W && (Q = null)) : ((!Q || Q.dataset.role !== "reporter") && (Q = document.createElement("div"), Q.className = "call-line you", Q.dataset.role = "reporter", E.appendChild(Q)), Q.textContent = j, W && (Q = null)), E.scrollTop = E.scrollHeight);
2347
+ });
2348
+ c.querySelector('[data-act="hangup"]').addEventListener("click", () => {
2349
+ te(), D(), a.endCall(), q();
2350
+ });
2351
+ }
2352
+ function kt(i) {
2353
+ var m;
2354
+ const c = i != null && i.id ? String(i.id).slice(0, 8) : null, p = (m = i == null ? void 0 : i.estimate) != null && m.label ? String(i.estimate.label) : null;
2355
+ h.querySelector(".panel").innerHTML = `
2356
+ <div class="done">
2357
+ <div class="tick">${H.check}</div>
2358
+ <h3>Thanks — that's on its way.</h3>
2359
+ <p>${c ? `Filed as <code>${T(c)}</code>.` : "The team has what they need to reproduce it."}</p>
2360
+ ${p ? `<p class="eta">Reports like this usually take <strong>${T(p)}</strong>. We'll let you know when it moves.</p>` : ""}
2361
+ </div>`, setTimeout(q, p ? 4e3 : 2e3);
2362
+ }
2363
+ async function St() {
2364
+ const i = h.querySelector(".panel");
2365
+ i.innerHTML = `
2366
+ <div class="head">
2367
+ <div class="mark">${H.bug}</div>
2368
+ <div class="titles">
2369
+ <h2>Your reports</h2>
2370
+ <p>Everything you've reported, and where each one got to.</p>
2371
+ </div>
2372
+ <button class="icon-btn" type="button" data-act="close" aria-label="Close">${H.close}</button>
2373
+ </div>
2374
+ <div class="history"><div class="empty">Loading…</div></div>`, i.querySelector('[data-act="close"]').addEventListener("click", q);
2375
+ let c = [];
2376
+ try {
2377
+ ({ reports: c } = await a.myReports());
2378
+ } catch {
2379
+ i.querySelector(".history").innerHTML = `<div class="empty">Couldn't load your reports just now.</div>`;
2380
+ return;
2381
+ }
2382
+ const p = i.querySelector(".history");
2383
+ if (p) {
2384
+ p.innerHTML = c.length ? c.map((m) => `
2385
+ <button class="row" type="button" data-report="${T(m.id)}">
2386
+ <span class="who">
2387
+ <span class="ttl">${T(m.title)}</span>
2388
+ <span class="sub">${T(m.reference)} · ${T(We(m.createdAt))}</span>
2389
+ </span>
2390
+ <span class="pill ${T(String(m.status).toLowerCase())}">${T(st(m.status))}</span>
2391
+ </button>`).join("") : `<div class="empty">You haven't reported anything yet.</div>`;
2392
+ for (const m of p.querySelectorAll("[data-report]"))
2393
+ m.addEventListener("click", () => wn(m.dataset.report));
2394
+ }
2395
+ }
2396
+ async function wn(i) {
2397
+ var E;
2398
+ const c = h.querySelector(".panel");
2399
+ c.innerHTML = `
2400
+ <div class="head">
2401
+ <button class="icon-btn back" type="button" data-act="back" aria-label="Back to your reports">${H.back}</button>
2402
+ <div class="titles">
2403
+ <h2>Your report</h2>
2404
+ <p>Everything that has happened, and a way to answer.</p>
2405
+ </div>
2406
+ <button class="icon-btn" type="button" data-act="close" aria-label="Close">${H.close}</button>
2407
+ </div>
2408
+ <div class="history"><div class="empty">Loading…</div></div>`, c.querySelector('[data-act="close"]').addEventListener("click", q), c.querySelector('[data-act="back"]').addEventListener("click", St);
2409
+ let p = null;
2410
+ try {
2411
+ p = await a.myReport(i);
2412
+ } catch {
2413
+ p = null;
2414
+ }
2415
+ const m = c.querySelector(".history");
2416
+ if (!m || !c.querySelector('[data-act="back"]')) return;
2417
+ if (!p) {
2418
+ m.innerHTML = `<div class="empty">Couldn't load that report just now.</div>`;
2419
+ return;
2420
+ }
2421
+ ue = i, a.markReportRead(i), m.classList.add("chatting"), m.innerHTML = `
2422
+ <div class="report-head">
2423
+ <span class="ttl">${T(p.title)}</span>
2424
+ <span class="sub">${T(p.reference)} · ${T(p.project || "")}</span>
2425
+ <span class="pill ${T(String(p.status).toLowerCase())}">${T(st(p.status))}</span>
2426
+ ${(E = p.estimate) != null && E.label ? `<span class="eta-line">Usually takes ${T(String(p.estimate.label))}</span>` : ""}
2427
+ </div>
2428
+ <div class="thread">${Ut(p.timeline)}</div>`;
2429
+ const M = m.querySelector(".thread");
2430
+ it(M), p.canReply && yn(m, M, i);
2431
+ }
2432
+ async function nt(i) {
2433
+ const c = h.querySelector(".history.chatting"), p = c == null ? void 0 : c.querySelector(".thread");
2434
+ if (!p || ue !== i) return;
2435
+ let m = null;
2436
+ try {
2437
+ m = await a.myReport(i);
2438
+ } catch {
2439
+ }
2440
+ if (!m || ue !== i) return;
2441
+ const M = Ma(p);
2442
+ p.innerHTML = Ut(m.timeline), M && it(p);
2443
+ const E = c.querySelector(".report-head .pill");
2444
+ E && (E.className = `pill ${String(m.status).toLowerCase()}`, E.textContent = st(m.status));
2445
+ }
2446
+ function yn(i, c, p) {
2447
+ const m = document.createElement("div");
2448
+ m.className = "composer", m.innerHTML = `
2449
+ <div class="staged" hidden></div>
2450
+ <div class="row">
2451
+ <div class="tools">
2452
+ <button class="tool" type="button" data-act="attach" title="Attach a file" aria-label="Attach a file">${H.paperclip}</button>
2453
+ <button class="tool" type="button" data-act="voice" title="Record a voice note" aria-label="Record a voice note">${H.mic}</button>
2454
+ <button class="tool" type="button" data-act="livecall" title="Call the team" aria-label="Call the team" hidden>${H.phone}</button>
2455
+ </div>
2456
+ <textarea rows="1" placeholder="Write a reply…" aria-label="Write a reply"></textarea>
2457
+ <button class="send" type="button" data-act="send" title="Send" aria-label="Send" disabled>${H.send}</button>
2458
+ </div>
2459
+ <div class="note" hidden></div>
2460
+ <input type="file" hidden multiple accept="${en.join(",")}">`, i.appendChild(m);
2461
+ const M = m.querySelector(".staged"), E = m.querySelector("textarea"), se = m.querySelector('[data-act="send"]'), P = m.querySelector('[data-act="voice"]'), ee = m.querySelector('[data-act="livecall"]'), te = m.querySelector("input[type=file]"), Q = m.querySelector(".note"), D = [];
2462
+ let J = null, j = !1;
2463
+ function W(L, U = "") {
2464
+ Q.hidden = !L, Q.className = `note ${U}`, Q.textContent = L || "";
2465
+ }
2466
+ function ce() {
2467
+ const L = D.filter((U) => U.storageKey);
2468
+ se.disabled = j || !E.value.trim() && !L.length, M.hidden = !D.length, M.innerHTML = D.map((U, pe) => `
2469
+ <span class="chip ${U.storageKey ? "" : "uploading"}">
2470
+ <span class="nm">${T(U.name)}</span>
2471
+ <span class="sz">${U.storageKey ? Te(U.size) : "sending…"}</span>
2472
+ <button type="button" data-drop="${pe}" aria-label="Remove ${T(U.name)}">${H.close}</button>
2473
+ </span>`).join("");
2474
+ for (const U of M.querySelectorAll("[data-drop]"))
2475
+ U.addEventListener("click", () => {
2476
+ D.splice(Number(U.dataset.drop), 1), ce();
2477
+ });
2478
+ }
2479
+ async function Mt(L, U, pe) {
2480
+ const Z = { name: pe, size: L.size, storageKey: null };
2481
+ D.push(Z), ce();
2482
+ try {
2483
+ const { storageKey: at, mime: xn } = await a.uploadAttachment(L, U);
2484
+ Z.storageKey = at, Z.mime = xn;
2485
+ } catch (at) {
2486
+ D.splice(D.indexOf(Z), 1), W(at.message || "That file could not be sent.", "err");
2487
+ }
2488
+ ce();
2489
+ }
2490
+ E.addEventListener("input", () => {
2491
+ E.style.height = "auto", E.style.height = `${Math.min(E.scrollHeight, 120)}px`, ce();
2492
+ }), E.addEventListener("keydown", (L) => {
2493
+ L.key === "Enter" && !L.shiftKey && !L.isComposing && (L.preventDefault(), Lt());
2494
+ }), m.querySelector('[data-act="attach"]').addEventListener("click", () => te.click()), te.addEventListener("change", () => {
2495
+ W("");
2496
+ for (const L of te.files) {
2497
+ if (L.size > a.config.maxAttachmentBytes) {
2498
+ W(`${L.name} is larger than ${Te(a.config.maxAttachmentBytes)}.`, "err");
2499
+ continue;
2500
+ }
2501
+ Mt(L, L.type, L.name);
2502
+ }
2503
+ te.value = "";
2504
+ }), Hn() || (P.disabled = !0, P.title = "This browser cannot record audio"), P.addEventListener("click", async () => {
2505
+ if (J) {
2506
+ const L = J;
2507
+ J = null, P.classList.remove("recording"), P.innerHTML = H.mic, W("");
2508
+ const { blob: U, mime: pe } = await L.stop();
2509
+ U.size && Mt(U, pe, "Voice note");
2510
+ return;
2511
+ }
2512
+ try {
2513
+ W(""), J = await Vn({
2514
+ maxMs: a.config.maxRecordingMs,
2515
+ onTick: (L) => W(`Recording — ${qe(L)}. Tap the mic to stop.`),
2516
+ onAutoStop: () => P.click()
2517
+ }), P.classList.add("recording"), P.innerHTML = H.stopSquare;
2518
+ } catch {
2519
+ J = null, W("The microphone is not available — check the browser permission.", "err");
2520
+ }
2521
+ }), a.canCallTeam(p).then((L) => {
2522
+ L && (ee.hidden = !1);
2523
+ }).catch(() => {
2524
+ }), ee.addEventListener("click", async () => {
2525
+ ee.disabled = !0, W("");
2526
+ try {
2527
+ const L = await a.startLiveCall(p);
2528
+ vn(i, c, L);
2529
+ } catch (L) {
2530
+ W(L.message.replace("[BugCatch] ", ""), "err");
2531
+ } finally {
2532
+ ee.disabled = !1;
2533
+ }
2534
+ }), se.addEventListener("click", Lt);
2535
+ async function Lt() {
2536
+ var pe;
2537
+ const L = E.value.trim(), U = D.filter((Z) => Z.storageKey);
2538
+ if (!(j || !L && !U.length)) {
2539
+ j = !0, ce(), W(""), c.insertAdjacentHTML("beforeend", rn({
2540
+ from: "reporter",
2541
+ body: L,
2542
+ at: (/* @__PURE__ */ new Date()).toISOString(),
2543
+ attachments: U.map((Z) => ({ type: "FILE", name: Z.name, size: Z.size }))
2544
+ }, "pending")), it(c);
2545
+ try {
2546
+ await nn({
2547
+ apiBase: a.config.apiBase,
2548
+ bugId: p,
2549
+ body: L,
2550
+ attachments: U.map((Z) => ({
2551
+ storageKey: Z.storageKey,
2552
+ mime: Z.mime,
2553
+ name: Z.name,
2554
+ size: Z.size
2555
+ }))
2556
+ }), E.value = "", E.style.height = "auto", D.length = 0, await nt(p);
2557
+ } catch (Z) {
2558
+ (pe = c.querySelector(".msg-row.pending")) == null || pe.classList.replace("pending", "failed"), W(Z.message || "That could not be sent — try again.", "err");
2559
+ } finally {
2560
+ j = !1, ce();
2561
+ }
2562
+ }
2563
+ }
2564
+ ce();
2565
+ }
2566
+ function vn(i, c, p) {
2567
+ const m = document.createElement("div");
2568
+ m.className = "livecall", m.innerHTML = `
2569
+ <span class="call-dot" aria-hidden="true"></span>
2570
+ <span class="who" role="status" aria-live="polite">Calling the team…</span>
2571
+ <span class="secs" hidden></span>
2572
+ <button class="act" type="button" data-act="mute" hidden>Mute</button>
2573
+ <button class="act danger" type="button" data-act="hangup">Cancel</button>`, i.insertBefore(m, c);
2574
+ const M = m.querySelector(".who"), E = m.querySelector(".secs"), se = m.querySelector(".call-dot"), P = m.querySelector('[data-act="mute"]'), ee = m.querySelector('[data-act="hangup"]');
2575
+ let te = null;
2576
+ const Q = () => {
2577
+ te && (clearInterval(te), te = null);
2578
+ }, D = (j) => {
2579
+ Q(), se.classList.add("off"), m.classList.add("over"), M.textContent = j, E.hidden = !0, P.hidden = !0, ee.textContent = "Dismiss", ee.classList.remove("danger");
2580
+ }, J = a.on("call", ({ state: j, live: W }) => {
2581
+ if (W) {
2582
+ if (j === "ringing" && (M.textContent = "Calling the team…"), j === "connecting" && (M.textContent = "Someone answered — connecting…"), j === "connected") {
2583
+ M.textContent = "You're on a call with the team", P.hidden = !1, E.hidden = !1;
2584
+ const ce = Date.now();
2585
+ Q(), te = setInterval(() => {
2586
+ E.textContent = qe(Math.floor((Date.now() - ce) / 1e3));
2587
+ }, 1e3), E.textContent = "0s";
2588
+ }
2589
+ j === "missed" && D("Nobody was free. Send a message instead and the team will pick it up."), j === "ended" && D("Call ended."), j === "failed" && D("The call dropped.");
2590
+ }
2591
+ });
2592
+ P.addEventListener("click", () => {
2593
+ const j = p.setMuted(!p.isMuted());
2594
+ P.textContent = j ? "Unmute" : "Mute", M.textContent = j ? "You're muted" : "You're on a call with the team";
2595
+ }), ee.addEventListener("click", () => {
2596
+ J(), Q(), a.endCall(), m.remove(), nt(ue);
2597
+ });
2598
+ }
2599
+ let ue = null;
2600
+ return xe = a.on("update", (i) => {
2601
+ !ue || (i == null ? void 0 : i.bugId) !== ue || (a.markReportRead(ue), nt(ue));
2602
+ }), t.appendChild(h), oe(), z.focus(), { destroy: Ae, close: q };
2603
+ }
2604
+ function Ut(t) {
2605
+ const e = t || [];
2606
+ return e.length ? e.map((n) => {
2607
+ var a, o, r, s;
2608
+ return n.from ? rn({
2609
+ from: n.from,
2610
+ author: (a = n.reply) == null ? void 0 : a.author,
2611
+ automated: (o = n.reply) == null ? void 0 : o.automated,
2612
+ body: (r = n.reply) == null ? void 0 : r.body,
2613
+ at: n.at,
2614
+ attachments: (s = n.reply) == null ? void 0 : s.attachments
2615
+ }) : $a(n);
2616
+ }).join("") : "";
2617
+ }
2618
+ function $a(t) {
2619
+ return `
2620
+ <div class="sys k-${T(t.kind)}">
2621
+ <span class="dot" aria-hidden="true"></span>${T(t.summary || "")}
2622
+ · ${T(We(t.at))}
2623
+ </div>`;
2624
+ }
2625
+ function rn(t, e = "") {
2626
+ const n = t.from === "reporter", a = t.body || "", o = t.attachments || [], r = !n && !!t.automated, s = a ? `<span class="msg-text">${T(a)}</span>` : "", l = o.length ? `<div class="att">${o.map(Ba).join("")}</div>` : "";
2627
+ let u = "";
2628
+ return r ? u = '<span class="msg-who auto">Automatic reply</span>' : !n && t.author && (u = `<span class="msg-who">${T(t.author)}</span>`), `
2629
+ <div class="msg-row ${n ? "mine" : "theirs"} ${r ? "auto" : ""} ${e}">
2630
+ ${u}
2631
+ <div class="bubble ${a ? "" : "silent"}">${s}${l}</div>
2632
+ <span class="msg-when">${T(We(t.at))}</span>
2633
+ </div>`;
2634
+ }
2635
+ function Ba(t) {
2636
+ const e = T(t.name || "attachment");
2637
+ if (!t.url) return `<span class="file"><span class="nm">${e}</span></span>`;
2638
+ const n = T(t.url);
2639
+ return t.type === "SCREENSHOT" ? `<a href="${n}" target="_blank" rel="noopener noreferrer"><img src="${n}" alt="${e}" loading="lazy"></a>` : t.type === "AUDIO" ? `<audio controls preload="metadata" src="${n}"></audio>` : t.type === "VIDEO" ? `<video controls preload="metadata" src="${n}"></video>` : `
2640
+ <a class="file" href="${n}" target="_blank" rel="noopener noreferrer" download>
2641
+ <span class="nm">${e}</span>
2642
+ ${t.size ? `<span class="sz">${T(Te(t.size))}</span>` : ""}
2643
+ </a>`;
2644
+ }
2645
+ function Ma(t) {
2646
+ return t.scrollHeight - t.scrollTop - t.clientHeight < 40;
2647
+ }
2648
+ function it(t) {
2649
+ t.scrollTop = t.scrollHeight;
2650
+ }
2651
+ function st(t) {
2652
+ return {
2653
+ OPEN: "Open",
2654
+ IN_PROGRESS: "In progress",
2655
+ RESOLVED: "Fixed",
2656
+ CLOSED: "Closed"
2657
+ }[t] || String(t || "").toLowerCase();
2658
+ }
2659
+ function We(t) {
2660
+ const e = new Date(t).getTime();
2661
+ if (!Number.isFinite(e)) return "";
2662
+ const n = Math.floor((Date.now() - e) / 864e5);
2663
+ if (n <= 0) return "today";
2664
+ if (n === 1) return "yesterday";
2665
+ if (n < 30) return `${n} days ago`;
2666
+ const a = Math.floor(n / 30);
2667
+ return a === 1 ? "a month ago" : `${a} months ago`;
2668
+ }
2669
+ function T(t) {
2670
+ return String(t).replace(/[&<>"']/g, (e) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[e]);
2671
+ }
2672
+ const La = "bugcatch.session.", yt = (t) => `${La}${t}`, vt = /* @__PURE__ */ new Map();
2673
+ function he(t) {
2674
+ var e;
2675
+ if (!t) return null;
2676
+ try {
2677
+ const n = (e = window.localStorage) == null ? void 0 : e.getItem(yt(t));
2678
+ if (n) {
2679
+ const a = JSON.parse(n);
2680
+ if (a != null && a.token) return a;
2681
+ }
2682
+ } catch {
2683
+ }
2684
+ return vt.get(t) || null;
2685
+ }
2686
+ function Ia(t, e) {
2687
+ var a;
2688
+ if (!t || !(e != null && e.token)) return;
2689
+ const n = {
2690
+ token: e.token,
2691
+ email: e.email || null,
2692
+ name: e.name || null
2693
+ };
2694
+ vt.set(t, n);
2695
+ try {
2696
+ (a = window.localStorage) == null || a.setItem(yt(t), JSON.stringify(n));
2697
+ } catch {
2698
+ }
2699
+ }
2700
+ function dt(t) {
2701
+ var e;
2702
+ if (t) {
2703
+ vt.delete(t);
2704
+ try {
2705
+ (e = window.localStorage) == null || e.removeItem(yt(t));
2706
+ } catch {
2707
+ }
2708
+ }
2709
+ }
2710
+ function Ze({ apiKey: t, token: e }) {
2711
+ return {
2712
+ "Content-Type": "application/json",
2713
+ "X-BugCatch-Key": t,
2714
+ // Its own header rather than Authorization, which on these routes already
2715
+ // carries the API key. The two answer different questions and a request
2716
+ // carries both.
2717
+ ...e ? { "X-BugCatch-Session": e } : {}
2718
+ };
2719
+ }
2720
+ async function sn(t) {
2721
+ try {
2722
+ const e = await t.json(), n = Array.isArray(e.message) ? e.message.join(", ") : e.message;
2723
+ if (n) return n;
2724
+ } catch {
2725
+ }
2726
+ return `Request failed (HTTP ${t.status})`;
2727
+ }
2728
+ async function Aa({ apiBase: t, apiKey: e, projectId: n, token: a }) {
2729
+ const o = { mode: "off", signedIn: null, signedOutReason: null };
2730
+ try {
2731
+ const r = await fetch(
2732
+ `${t}/ingest/login?projectId=${encodeURIComponent(n)}`,
2733
+ { headers: Ze({ apiKey: e, token: a }) }
2734
+ );
2735
+ if (!r.ok) return o;
2736
+ const s = await r.json();
2737
+ return {
2738
+ mode: s.mode === "optional" || s.mode === "required" ? s.mode : "off",
2739
+ signedIn: s.signedIn || null,
2740
+ signedOutReason: s.signedOutReason || null
2741
+ };
2742
+ } catch {
2743
+ return o;
2744
+ }
2745
+ }
2746
+ async function Qa({ apiBase: t, apiKey: e, projectId: n, email: a, name: o }) {
2747
+ const r = await fetch(`${t}/ingest/login/start`, {
2748
+ method: "POST",
2749
+ headers: Ze({ apiKey: e }),
2750
+ body: JSON.stringify({ projectId: n, email: a, ...o ? { name: o } : {} })
2751
+ });
2752
+ if (!r.ok) throw new Error(await sn(r));
2753
+ return r.json();
2754
+ }
2755
+ async function Ra({ apiBase: t, apiKey: e, projectId: n, email: a, code: o }) {
2756
+ const r = await fetch(`${t}/ingest/login`, {
2757
+ method: "POST",
2758
+ headers: Ze({ apiKey: e }),
2759
+ body: JSON.stringify({ projectId: n, email: a, code: o })
2760
+ });
2761
+ if (!r.ok) throw new Error(await sn(r));
2762
+ const s = await r.json(), l = { token: s.sessionToken, email: s.email, name: s.name };
2763
+ return Ia(n, l), l;
2764
+ }
2765
+ async function ja({ apiBase: t, apiKey: e, projectId: n, token: a }) {
2766
+ if (dt(n), !a) return { signedOut: !0 };
2767
+ try {
2768
+ await fetch(`${t}/ingest/logout`, {
2769
+ method: "POST",
2770
+ headers: Ze({ apiKey: e, token: a }),
2771
+ body: JSON.stringify({ projectId: n })
2772
+ });
2773
+ } catch {
2774
+ }
2775
+ return { signedOut: !0 };
2776
+ }
2777
+ const Le = 8e3;
2778
+ function cn(t = Le) {
2779
+ const e = window.AudioContext || window.webkitAudioContext;
2780
+ try {
2781
+ return new e({ sampleRate: t });
2782
+ } catch {
2783
+ return new e();
2784
+ }
2785
+ }
2786
+ async function ut(t) {
2787
+ if (t.state === "suspended")
2788
+ try {
2789
+ await t.resume();
2790
+ } catch {
2791
+ }
2792
+ }
2793
+ function ln(t, e, n) {
2794
+ const a = e / n, o = new Float32Array(Math.floor(t.length / a));
2795
+ for (let r = 0; r < o.length; r++) {
2796
+ const s = Math.floor(r * a), l = Math.min(Math.floor((r + 1) * a), t.length);
2797
+ let u = 0;
2798
+ for (let g = s; g < l; g++) u += t[g];
2799
+ o[r] = l > s ? u / (l - s) : 0;
2800
+ }
2801
+ return o;
2802
+ }
2803
+ const pt = 132, Dt = 32635;
2804
+ function dn(t) {
2805
+ const e = new Uint8Array(t.length);
2806
+ for (let n = 0; n < t.length; n++) {
2807
+ const a = Math.max(-1, Math.min(1, t[n]));
2808
+ let o = Math.round(a * 32767);
2809
+ const r = o < 0 ? 128 : 0;
2810
+ r && (o = -o), o > Dt && (o = Dt), o += pt;
2811
+ let s = 7;
2812
+ for (let u = 16384; !(o & u) && s > 0; s--, u >>= 1) ;
2813
+ const l = o >> s + 3 & 15;
2814
+ e[n] = ~(r | s << 4 | l) & 255;
2815
+ }
2816
+ return e;
2817
+ }
2818
+ function za(t) {
2819
+ const e = new Float32Array(t.length);
2820
+ for (let n = 0; n < t.length; n++) {
2821
+ const a = ~t[n] & 255, o = a & 128, r = a >> 4 & 7, l = (((a & 15) << 3) + pt << r) - pt;
2822
+ e[n] = (o ? -l : l) / 32768;
2823
+ }
2824
+ return e;
2825
+ }
2826
+ const un = 512;
2827
+ function Oa(t, e, n, a = Le) {
2828
+ const o = t.createMediaStreamSource(e), r = t.createScriptProcessor(un, 1, 1);
2829
+ r.onaudioprocess = (l) => {
2830
+ const u = l.inputBuffer.getChannelData(0), g = t.sampleRate === a ? u : ln(u, t.sampleRate, a);
2831
+ n(dn(g));
2832
+ }, o.connect(r);
2833
+ const s = t.createGain();
2834
+ return s.gain.value = 0, r.connect(s), s.connect(t.destination), {
2835
+ stop() {
2836
+ r.onaudioprocess = null;
2837
+ try {
2838
+ o.disconnect();
2839
+ } catch {
2840
+ }
2841
+ try {
2842
+ r.disconnect();
2843
+ } catch {
2844
+ }
2845
+ try {
2846
+ s.disconnect();
2847
+ } catch {
2848
+ }
2849
+ }
2850
+ };
2851
+ }
2852
+ const Ha = "voiceai.realtime.v1";
2853
+ async function qa({ realtimeUrl: t, onState: e, onTranscript: n }) {
2854
+ const a = (y, x) => {
2855
+ try {
2856
+ e == null || e(y, x);
2857
+ } catch {
2858
+ }
2859
+ }, o = (y) => {
2860
+ try {
2861
+ n == null || n(y);
2862
+ } catch {
2863
+ }
2864
+ };
2865
+ a("connecting");
2866
+ let r;
2867
+ try {
2868
+ r = await navigator.mediaDevices.getUserMedia({
2869
+ audio: {
2870
+ // A speakerphone conversation with no echo cancellation is one where
2871
+ // the agent hears itself and answers its own greeting.
2872
+ echoCancellation: !0,
2873
+ noiseSuppression: !0,
2874
+ autoGainControl: !0,
2875
+ channelCount: 1
2876
+ }
2877
+ });
2878
+ } catch (y) {
2879
+ throw a("failed", { reason: "microphone", error: String((y == null ? void 0 : y.name) || y) }), y;
2880
+ }
2881
+ const s = cn();
2882
+ await ut(s);
2883
+ const l = s.createMediaStreamSource(r);
2884
+ let u;
2885
+ try {
2886
+ u = await Ua(t);
2887
+ } catch (y) {
2888
+ throw Nt(r, s, null, null), a("failed", { reason: "connect" }), y;
2889
+ }
2890
+ const g = s.createScriptProcessor(un, 1, 1);
2891
+ let b = !0;
2892
+ g.onaudioprocess = (y) => {
2893
+ if (!b || u.readyState !== WebSocket.OPEN) return;
2894
+ const x = y.inputBuffer.getChannelData(0), C = s.sampleRate === Le ? x : ln(x, s.sampleRate, Le);
2895
+ u.send(dn(C));
2896
+ }, l.connect(g);
2897
+ const d = s.createGain();
2898
+ d.gain.value = 0, g.connect(d), d.connect(s.destination);
2899
+ let w = 0;
2900
+ const h = /* @__PURE__ */ new Set(), S = (y) => {
2901
+ s.state === "suspended" && ut(s);
2902
+ const x = za(y), C = s.createBuffer(1, x.length, Le);
2903
+ C.getChannelData(0).set(x);
2904
+ const R = s.createBufferSource();
2905
+ R.buffer = C, R.connect(s.destination), R.onended = () => h.delete(R);
2906
+ const Y = s.currentTime;
2907
+ w < Y && (w = Y), R.start(w), w += C.duration, h.add(R);
2908
+ }, z = () => {
2909
+ for (const y of h)
2910
+ try {
2911
+ y.stop();
2912
+ } catch {
2913
+ }
2914
+ h.clear(), w = 0;
2915
+ };
2916
+ u.addEventListener("message", (y) => {
2917
+ var C, R;
2918
+ if (typeof y.data != "string") return;
2919
+ let x;
2920
+ try {
2921
+ x = JSON.parse(y.data);
2922
+ } catch {
2923
+ return;
2924
+ }
2925
+ switch (x.type) {
2926
+ case "session.created":
2927
+ a("listening");
2928
+ break;
2929
+ case "transcript.partial":
2930
+ ct(x.text) && o({ role: "reporter", text: x.text, final: !1 });
2931
+ break;
2932
+ case "transcript.final":
2933
+ ct(x.text) && o({ role: "reporter", text: x.text, final: !0 });
2934
+ break;
2935
+ case "response.started":
2936
+ a("speaking");
2937
+ break;
2938
+ case "response.audio.delta":
2939
+ x.audio && S(Da(x.audio));
2940
+ break;
2941
+ case "response.text.delta":
2942
+ ct(x.text) && o({ role: "agent", text: x.text, final: !1 });
2943
+ break;
2944
+ case "response.done":
2945
+ o({ role: "agent", text: "", final: !0 }), a("listening");
2946
+ break;
2947
+ case "state.changed":
2948
+ (x.state === "listening" || x.state === "interrupted") && z(), x.state && a(x.state);
2949
+ break;
2950
+ case "session.ended":
2951
+ k("ended", { reason: ((C = x.data) == null ? void 0 : C.reason) || null });
2952
+ break;
2953
+ case "error":
2954
+ k("failed", { reason: ((R = x.error) == null ? void 0 : R.detail) || "the call failed" });
2955
+ break;
2956
+ }
2957
+ }), u.addEventListener("close", () => k("ended")), u.addEventListener("error", () => k("failed", { reason: "transport" }));
2958
+ let F = !1;
2959
+ function k(y, x) {
2960
+ F || (F = !0, b = !1, z(), Nt(r, s, g, u), a(y, x));
2961
+ }
2962
+ return {
2963
+ /** End the call. Safe to call twice, and after it has already ended. */
2964
+ hangUp() {
2965
+ if (!F && u.readyState === WebSocket.OPEN)
2966
+ try {
2967
+ u.send(JSON.stringify({ type: "session.end" }));
2968
+ } catch {
2969
+ }
2970
+ k("ended", { reason: "hung_up" });
2971
+ }
2972
+ };
2973
+ }
2974
+ function Ua(t) {
2975
+ return new Promise((e, n) => {
2976
+ let a;
2977
+ try {
2978
+ a = new WebSocket(t, Ha);
2979
+ } catch (l) {
2980
+ n(l);
2981
+ return;
2982
+ }
2983
+ a.binaryType = "arraybuffer";
2984
+ const o = () => {
2985
+ s(), e(a);
2986
+ }, r = () => {
2987
+ s(), n(new Error("the call could not be connected"));
2988
+ }, s = () => {
2989
+ a.removeEventListener("open", o), a.removeEventListener("error", r), a.removeEventListener("close", r);
2990
+ };
2991
+ a.addEventListener("open", o), a.addEventListener("error", r), a.addEventListener("close", r);
2992
+ });
2993
+ }
2994
+ function Nt(t, e, n, a) {
2995
+ try {
2996
+ n == null || n.disconnect();
2997
+ } catch {
2998
+ }
2999
+ n && (n.onaudioprocess = null);
3000
+ for (const o of (t == null ? void 0 : t.getTracks()) ?? [])
3001
+ try {
3002
+ o.stop();
3003
+ } catch {
3004
+ }
3005
+ try {
3006
+ e == null || e.close();
3007
+ } catch {
3008
+ }
3009
+ try {
3010
+ a == null || a.close();
3011
+ } catch {
3012
+ }
3013
+ }
3014
+ function ct(t) {
3015
+ return typeof t != "string" || !t || t.includes("\0") ? !1 : (t.match(/[\u0001-\u0008\u000e-\u001f]/g) || []).length <= t.length / 10;
3016
+ }
3017
+ function Da(t) {
3018
+ const e = window.atob(t), n = new Uint8Array(e.length);
3019
+ for (let a = 0; a < e.length; a++) n[a] = e.charCodeAt(a);
3020
+ return n;
3021
+ }
3022
+ const Na = "voiceai.realtime.v1", Va = [{ urls: "stun:stun.l.google.com:19302" }];
3023
+ function ht() {
3024
+ return navigator.mediaDevices.getUserMedia({
3025
+ audio: {
3026
+ // Both ends are on speakers as often as not. Without cancellation each
3027
+ // hears themselves back through the other, half a second late, which is
3028
+ // the single most effective way to make a conversation impossible.
3029
+ echoCancellation: !0,
3030
+ noiseSuppression: !0,
3031
+ autoGainControl: !0,
3032
+ channelCount: 1
3033
+ }
3034
+ });
3035
+ }
3036
+ function Ne() {
3037
+ var t;
3038
+ return typeof window < "u" && typeof window.RTCPeerConnection == "function" && !!((t = navigator.mediaDevices) != null && t.getUserMedia);
3039
+ }
3040
+ async function Vt({
3041
+ realtimeUrl: t,
3042
+ stream: e,
3043
+ ringsUntil: n,
3044
+ onState: a,
3045
+ onAudioBlocked: o,
3046
+ onMicrophone: r
3047
+ }) {
3048
+ const s = (f, v) => {
3049
+ try {
3050
+ a == null || a(f, v);
3051
+ } catch {
3052
+ }
3053
+ }, l = (f) => {
3054
+ try {
3055
+ o == null || o(f);
3056
+ } catch {
3057
+ }
3058
+ }, u = (f) => {
3059
+ try {
3060
+ r == null || r(f);
3061
+ } catch {
3062
+ }
3063
+ };
3064
+ if (!Ne()) {
3065
+ const f = new Error("[BugCatch] this browser cannot make live calls.");
3066
+ throw s("failed", { reason: "unsupported" }), f;
3067
+ }
3068
+ let g = e;
3069
+ if (!g)
3070
+ try {
3071
+ g = await ht();
3072
+ } catch (f) {
3073
+ throw s("failed", { reason: "microphone", error: String((f == null ? void 0 : f.name) || f) }), f;
3074
+ }
3075
+ let b;
3076
+ try {
3077
+ b = await Pa(t);
3078
+ } catch (f) {
3079
+ throw Kt(g), s("failed", { reason: "connect" }), f;
3080
+ }
3081
+ let d = null, w = null, h = !1, S = null, z = null, F = null, k = !1, y = null, x = null, C = !1;
3082
+ const R = [];
3083
+ let Y = !1, O = !1, K = !1, B = null, X = null, me = 0;
3084
+ const Xe = 2e3, ve = 5e3, xe = 3e4, Ae = 4, q = (f, v) => {
3085
+ if (!h) {
3086
+ h = !0, S && (clearTimeout(S), S = null), we(), x == null || x(), x = null, oe(), y == null || y(), y = null;
3087
+ try {
3088
+ d == null || d.close();
3089
+ } catch {
3090
+ }
3091
+ Kt(g), w && (w.srcObject = null, w.remove(), w = null);
3092
+ try {
3093
+ b.readyState === WebSocket.OPEN && b.close(1e3, "call over");
3094
+ } catch {
3095
+ }
3096
+ s(f, v);
3097
+ }
3098
+ }, re = (f) => {
3099
+ b.readyState === WebSocket.OPEN && b.send(JSON.stringify(f));
3100
+ }, V = async () => {
3101
+ if (C || F || h || !z || !z.audio && !z.transcript) return;
3102
+ C = !0;
3103
+ const f = z.encoding || "mulaw";
3104
+ if (f !== "mulaw") {
3105
+ Pt(`this call is kept as ${f}, which this SDK cannot produce.`);
3106
+ return;
3107
+ }
3108
+ const v = Number(z.sample_rate) || 8e3;
3109
+ try {
3110
+ const $ = cn(v);
3111
+ if (await ut($), h) {
3112
+ try {
3113
+ $.close();
3114
+ } catch {
3115
+ }
3116
+ return;
3117
+ }
3118
+ const _ = Oa($, g, (I) => {
3119
+ b.readyState === WebSocket.OPEN && b.send(I);
3120
+ }, v);
3121
+ F = { context: $, tap: _ };
3122
+ } catch ($) {
3123
+ Pt(`could not send the copy this call is keeping: ${String(($ == null ? void 0 : $.message) || $)}`);
3124
+ }
3125
+ };
3126
+ function oe() {
3127
+ if (C = !1, !F) return;
3128
+ const { context: f, tap: v } = F;
3129
+ F = null;
3130
+ try {
3131
+ v.stop();
3132
+ } catch {
3133
+ }
3134
+ try {
3135
+ f.close();
3136
+ } catch {
3137
+ }
3138
+ }
3139
+ function $e() {
3140
+ var v;
3141
+ if (!w || h) return;
3142
+ let f;
3143
+ try {
3144
+ f = (v = w.play) == null ? void 0 : v.call(w);
3145
+ } catch {
3146
+ Be();
3147
+ return;
3148
+ }
3149
+ if (!f || typeof f.then != "function") {
3150
+ Qe();
3151
+ return;
3152
+ }
3153
+ f.then(Qe, Be);
3154
+ }
3155
+ function Qe() {
3156
+ y == null || y(), y = null, k && (k = !1, l(!1));
3157
+ }
3158
+ function Be() {
3159
+ h || (k || (k = !0, l(!0)), Me());
3160
+ }
3161
+ function Me() {
3162
+ if (y || typeof window > "u" || typeof window.addEventListener != "function") return;
3163
+ const f = () => $e(), v = ["pointerdown", "keydown", "touchstart"];
3164
+ for (const $ of v)
3165
+ window.addEventListener($, f, { capture: !0, passive: !0 });
3166
+ y = () => {
3167
+ for (const $ of v)
3168
+ try {
3169
+ window.removeEventListener($, f, { capture: !0 });
3170
+ } catch {
3171
+ }
3172
+ };
3173
+ }
3174
+ const Je = (f) => {
3175
+ d = new RTCPeerConnection({ iceServers: f != null && f.length ? f : Va });
3176
+ for (const v of g.getAudioTracks()) d.addTrack(v, g);
3177
+ x = Re(), d.onicecandidate = (v) => {
3178
+ v.candidate && re({ type: "signal", signal: { kind: "candidate", payload: v.candidate.toJSON() } });
3179
+ }, d.ontrack = (v) => {
3180
+ var _;
3181
+ const [$] = v.streams;
3182
+ $ && (w || (w = document.createElement("audio"), w.autoplay = !0, w.playsInline = !0, (_ = w.setAttribute) == null || _.call(w, "playsinline", ""), w.style.cssText = "position:fixed;width:0;height:0;opacity:0;pointer-events:none", document.body.appendChild(w)), w.srcObject = $, $e());
3183
+ }, d.onconnectionstatechange = be, d.oniceconnectionstatechange = be;
3184
+ };
3185
+ function be() {
3186
+ if (!d || h) return;
3187
+ const f = d.connectionState || d.iceConnectionState;
3188
+ if (f === "connected" || f === "completed") {
3189
+ O = !0, me = 0, we(), K = !1, s("connected");
3190
+ return;
3191
+ }
3192
+ if (f === "disconnected") {
3193
+ if (!O) return;
3194
+ ie(Xe);
3195
+ return;
3196
+ }
3197
+ if (f === "failed") {
3198
+ if (!O) {
3199
+ q("failed", { reason: "ice" });
3200
+ return;
3201
+ }
3202
+ ie(0);
3203
+ }
3204
+ }
3205
+ function ie(f) {
3206
+ h || (K || (K = !0, s("reconnecting"), X = setTimeout(() => q("failed", { reason: "network" }), xe)), !B && (B = setTimeout(ye, f)));
3207
+ }
3208
+ function we() {
3209
+ B && (clearTimeout(B), B = null), X && (clearTimeout(X), X = null);
3210
+ }
3211
+ async function ye() {
3212
+ if (B = null, h || !d) return;
3213
+ const f = d.connectionState || d.iceConnectionState;
3214
+ if (!(f === "connected" || f === "completed")) {
3215
+ if (!Y) {
3216
+ re({ type: "signal", signal: { kind: "restart", payload: {} } }), B = setTimeout(ye, ve);
3217
+ return;
3218
+ }
3219
+ me >= Ae || (me += 1, await le(), B = setTimeout(ye, ve));
3220
+ }
3221
+ }
3222
+ async function le() {
3223
+ if (!(h || !d))
3224
+ try {
3225
+ const f = await d.createOffer({ iceRestart: !0 });
3226
+ await d.setLocalDescription(f), re({ type: "signal", signal: { kind: "offer", payload: d.localDescription.toJSON() } });
3227
+ } catch {
3228
+ }
3229
+ }
3230
+ function Re() {
3231
+ let v = null, $ = !1;
3232
+ const _ = (I) => {
3233
+ if (v && (clearTimeout(v), v = null), I) {
3234
+ if (!$) return;
3235
+ $ = !1, u(!0);
3236
+ return;
3237
+ }
3238
+ $ || (v = setTimeout(() => {
3239
+ v = null, !h && ($ = !0, u(!1));
3240
+ }, 600));
3241
+ };
3242
+ for (const I of g.getAudioTracks())
3243
+ I.onmute = () => _(!1), I.onunmute = () => _(!0), I.onended = () => {
3244
+ v && (clearTimeout(v), v = null), !$ && ($ = !0, u(!1));
3245
+ };
3246
+ return () => {
3247
+ v && (clearTimeout(v), v = null);
3248
+ for (const I of g.getAudioTracks())
3249
+ I.onmute = null, I.onunmute = null, I.onended = null;
3250
+ };
3251
+ }
3252
+ const je = async () => {
3253
+ try {
3254
+ const f = await d.createOffer();
3255
+ await d.setLocalDescription(f), re({ type: "signal", signal: { kind: "offer", payload: d.localDescription.toJSON() } });
3256
+ } catch {
3257
+ q("failed", { reason: "negotiation" });
3258
+ }
3259
+ }, Ge = async (f) => {
3260
+ try {
3261
+ await d.setRemoteDescription(new RTCSessionDescription(f));
3262
+ const v = await d.createAnswer();
3263
+ await d.setLocalDescription(v), re({ type: "signal", signal: { kind: "answer", payload: d.localDescription.toJSON() } }), await de();
3264
+ } catch {
3265
+ q("failed", { reason: "negotiation" });
3266
+ }
3267
+ }, de = async () => {
3268
+ for (; R.length; ) {
3269
+ const f = R.shift();
3270
+ try {
3271
+ await d.addIceCandidate(new RTCIceCandidate(f));
3272
+ } catch {
3273
+ }
3274
+ }
3275
+ };
3276
+ let ze = Promise.resolve();
3277
+ b.onmessage = (f) => {
3278
+ ze = ze.then(() => ke(f)).catch(() => {
3279
+ });
3280
+ };
3281
+ const ke = async (f) => {
3282
+ var $, _, I, G, Se;
3283
+ if (typeof f.data != "string") return;
3284
+ let v;
3285
+ try {
3286
+ v = JSON.parse(f.data);
3287
+ } catch {
3288
+ return;
3289
+ }
3290
+ switch (v.type) {
3291
+ case "session.created": {
3292
+ if (Je(($ = v.data) == null ? void 0 : $.ice_servers), z = ((_ = v.data) == null ? void 0 : _.capture) || null, (I = v.data) != null && I.peer_present) {
3293
+ s("connecting"), V();
3294
+ break;
3295
+ }
3296
+ s("ringing"), Oe();
3297
+ break;
3298
+ }
3299
+ case "peer.joined": {
3300
+ S && (clearTimeout(S), S = null), s("connecting"), Y = !!((G = v.data) != null && G.should_offer), Y && await je(), V();
3301
+ break;
3302
+ }
3303
+ case "signal": {
3304
+ if (!d) break;
3305
+ const { kind: Ce, payload: Ee } = v.data || {};
3306
+ if (Ce === "offer")
3307
+ await Ge(Ee);
3308
+ else if (Ce === "answer")
3309
+ try {
3310
+ await d.setRemoteDescription(new RTCSessionDescription(Ee)), await de();
3311
+ } catch {
3312
+ q("failed", { reason: "negotiation" });
3313
+ }
3314
+ else if (Ce === "candidate")
3315
+ if (d.remoteDescription)
3316
+ try {
3317
+ await d.addIceCandidate(new RTCIceCandidate(Ee));
3318
+ } catch {
3319
+ }
3320
+ else
3321
+ R.push(Ee);
3322
+ else Ce === "restart" && Y && await le();
3323
+ break;
3324
+ }
3325
+ case "peer.left":
3326
+ q("ended", { reason: "peer_left" });
3327
+ break;
3328
+ case "error":
3329
+ q("failed", { reason: "server", detail: (Se = v.error) == null ? void 0 : Se.detail });
3330
+ break;
3331
+ case "session.ended":
3332
+ q("ended", { reason: "server" });
3333
+ break;
3334
+ }
3335
+ };
3336
+ b.onclose = () => q("ended", { reason: "disconnected" }), b.onerror = () => q("failed", { reason: "socket" });
3337
+ function Oe() {
3338
+ if (!n) return;
3339
+ const f = new Date(n).getTime() - Date.now();
3340
+ Number.isFinite(f) && (S = setTimeout(() => q("missed"), Math.max(0, f)));
3341
+ }
3342
+ return {
3343
+ hangUp() {
3344
+ re({ type: "session.end" }), q("ended", { reason: "hung_up" });
3345
+ },
3346
+ setMuted(f) {
3347
+ for (const v of g.getAudioTracks()) v.enabled = !f;
3348
+ return f;
3349
+ },
3350
+ isMuted() {
3351
+ return g.getAudioTracks().every((f) => !f.enabled);
3352
+ }
3353
+ };
3354
+ }
3355
+ function Pa(t) {
3356
+ return new Promise((e, n) => {
3357
+ let a;
3358
+ try {
3359
+ a = new WebSocket(t, Na);
3360
+ } catch (o) {
3361
+ n(o);
3362
+ return;
3363
+ }
3364
+ a.onopen = () => {
3365
+ a.onopen = null, a.onerror = null, e(a);
3366
+ }, a.onerror = () => {
3367
+ a.onopen = null, a.onerror = null, n(new Error("[BugCatch] could not connect the call."));
3368
+ };
3369
+ });
3370
+ }
3371
+ function Pt(t) {
3372
+ try {
3373
+ console.warn(`[BugCatch] ${t}`);
3374
+ } catch {
3375
+ }
3376
+ }
3377
+ function Kt(t) {
3378
+ var e;
3379
+ for (const n of ((e = t == null ? void 0 : t.getTracks) == null ? void 0 : e.call(t)) ?? [])
3380
+ try {
3381
+ n.stop();
3382
+ } catch {
3383
+ }
3384
+ }
3385
+ const Ue = {
3386
+ cycleMs: 3e3,
3387
+ peak: 0.045,
3388
+ bursts: [
3389
+ { at: 0, seconds: 0.4, hz: [440, 660] },
3390
+ { at: 0.55, seconds: 0.4, hz: [440, 660] }
3391
+ ]
3392
+ };
3393
+ let pn = !1;
3394
+ if (typeof window < "u" && window.addEventListener)
3395
+ for (const t of ["pointerdown", "keydown", "touchstart"])
3396
+ window.addEventListener(t, () => {
3397
+ pn = !0;
3398
+ }, {
3399
+ once: !0,
3400
+ capture: !0,
3401
+ passive: !0
3402
+ });
3403
+ function Ka() {
3404
+ var t;
3405
+ try {
3406
+ return !!((t = navigator.userActivation) != null && t.hasBeenActive);
3407
+ } catch {
3408
+ return !1;
3409
+ }
3410
+ }
3411
+ function _a() {
3412
+ if (!pn && !Ka()) return () => {
3413
+ };
3414
+ const t = typeof window < "u" && (window.AudioContext || window.webkitAudioContext);
3415
+ if (!t) return () => {
3416
+ };
3417
+ let e;
3418
+ try {
3419
+ e = new t();
3420
+ } catch {
3421
+ return () => {
3422
+ };
3423
+ }
3424
+ let n = !1, a = null;
3425
+ const o = [], r = ({ at: u, seconds: g, hz: b }) => {
3426
+ const d = e.currentTime + u, w = e.createGain();
3427
+ w.connect(e.destination), w.gain.setValueAtTime(0, d), w.gain.linearRampToValueAtTime(Ue.peak, d + 0.02), w.gain.setValueAtTime(Ue.peak, d + g - 0.06), w.gain.linearRampToValueAtTime(0, d + g);
3428
+ for (const h of b) {
3429
+ const S = e.createOscillator();
3430
+ S.type = "sine", S.frequency.value = h, S.connect(w), S.start(d), S.stop(d + g + 0.02), o.push(S);
3431
+ }
3432
+ }, s = () => {
3433
+ if (!n)
3434
+ try {
3435
+ Ue.bursts.forEach(r);
3436
+ } catch {
3437
+ }
3438
+ }, l = () => {
3439
+ n || (s(), a = setInterval(s, Ue.cycleMs));
3440
+ };
3441
+ return e.state === "running" ? l() : e.resume().then(l).catch(() => {
3442
+ }), () => {
3443
+ n = !0, a && clearInterval(a);
3444
+ for (const u of o)
3445
+ try {
3446
+ u.stop();
3447
+ } catch {
3448
+ }
3449
+ try {
3450
+ e.close();
3451
+ } catch {
3452
+ }
3453
+ };
3454
+ }
3455
+ function Wa(t) {
3456
+ const e = Date.parse(t || "");
3457
+ return Number.isNaN(e) ? 0 : Math.max(0, Math.round((e - Date.now()) / 1e3));
3458
+ }
3459
+ const _t = [
3460
+ "flex:1",
3461
+ "padding:9px 12px",
3462
+ "border:0",
3463
+ "border-radius:8px",
3464
+ "font:600 13px/1.4 inherit",
3465
+ "cursor:pointer"
3466
+ ].join(";");
3467
+ function Fa({
3468
+ call: t,
3469
+ accent: e = "#5b6ef5",
3470
+ position: n = "bottom-right",
3471
+ onAnswer: a,
3472
+ onDecline: o,
3473
+ onExpire: r
3474
+ }) {
3475
+ if (typeof document > "u" || !document.body) return () => {
3476
+ };
3477
+ const [s, l] = String(n).split("-"), u = document.createElement("div");
3478
+ u.setAttribute("role", "alertdialog"), u.setAttribute("aria-live", "assertive"), u.setAttribute("aria-label", `${t.from || "Support"} is calling about ${t.reference}`), u.style.cssText = [
3479
+ "position:fixed",
3480
+ `${s === "top" ? "top" : "bottom"}:20px`,
3481
+ `${l === "left" ? "left" : "right"}:20px`,
3482
+ // Above the update toast, which uses the maximum and may be on screen at
3483
+ // the same moment — a status line must not sit on top of a ringing phone.
3484
+ "z-index:2147483647",
3485
+ "width:300px",
3486
+ "max-width:calc(100vw - 40px)",
3487
+ "padding:14px",
3488
+ "border-radius:12px",
3489
+ "background:#1b1e26",
3490
+ "color:#eceef2",
3491
+ 'font:13px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif',
3492
+ "box-shadow:0 18px 44px rgba(0,0,0,0.5)",
3493
+ `border:1px solid ${e}`,
3494
+ "opacity:0",
3495
+ "transform:translateY(8px)",
3496
+ "transition:opacity .2s ease,transform .2s ease"
3497
+ ].join(";");
3498
+ const g = document.createElement("div");
3499
+ g.style.cssText = "font-weight:600;font-size:14px;margin-bottom:2px", g.textContent = `${t.from || "Support"} is calling`;
3500
+ const b = document.createElement("div");
3501
+ b.style.cssText = "color:#a4abba;margin-bottom:10px", b.textContent = t.project ? `About your report ${t.reference} · ${t.project}` : `About your report ${t.reference}`;
3502
+ const d = document.createElement("div");
3503
+ d.style.cssText = "color:#7d8598;font-size:12px;margin-bottom:10px";
3504
+ const w = document.createElement("button");
3505
+ w.type = "button", w.textContent = "Answer", w.style.cssText = `${_t};background:${e};color:#fff`;
3506
+ const h = document.createElement("button");
3507
+ h.type = "button", h.textContent = "Not now", h.style.cssText = `${_t};background:#2b2f3a;color:#c8cedb`;
3508
+ const S = document.createElement("div");
3509
+ if (S.style.cssText = "display:flex;gap:8px", S.append(w, h), u.append(g, b, d), t.recorded) {
3510
+ const C = document.createElement("div");
3511
+ C.style.cssText = "color:#7d8598;font-size:12px;margin-bottom:10px", C.textContent = "This call will be recorded.", u.append(C);
3512
+ }
3513
+ u.append(S), document.body.appendChild(u);
3514
+ const z = _a();
3515
+ let F = !1, k = null;
3516
+ const y = () => {
3517
+ F || (F = !0, k && clearInterval(k), z(), u.style.opacity = "0", u.style.transform = "translateY(8px)", setTimeout(() => u.remove(), 250));
3518
+ }, x = () => {
3519
+ const C = Wa(t.ringsUntil);
3520
+ if (d.textContent = C ? `Ringing — ${C}s` : "The call has stopped ringing.", !C) {
3521
+ y();
3522
+ try {
3523
+ r == null || r();
3524
+ } catch {
3525
+ }
3526
+ }
3527
+ };
3528
+ return x(), k = setInterval(x, 1e3), w.addEventListener("click", () => {
3529
+ y();
3530
+ try {
3531
+ a == null || a();
3532
+ } catch {
3533
+ }
3534
+ }), h.addEventListener("click", () => {
3535
+ y();
3536
+ try {
3537
+ o == null || o();
3538
+ } catch {
3539
+ }
3540
+ }), requestAnimationFrame(() => {
3541
+ u.style.opacity = "1", u.style.transform = "translateY(0)";
3542
+ }), y;
3543
+ }
3544
+ const Wt = {
3545
+ name: "@sphoro/bugcatch-web-sdk",
3546
+ version: "0.13.1"
3547
+ }, De = {
3548
+ apiBase: "http://localhost:3000",
3549
+ apiKey: null,
3550
+ projectId: null,
3551
+ consoleLimit: 100,
3552
+ networkLimit: 50,
3553
+ captureConsole: !0,
3554
+ captureNetwork: !0,
3555
+ captureErrors: !0,
3556
+ // Shows the floating "Report a bug" button. Turn off to drive it yourself.
3557
+ widget: !0,
3558
+ widgetPosition: "bottom-right",
3559
+ // 'auto' follows the visitor's system setting; 'light' / 'dark' pin it.
3560
+ theme: "auto",
3561
+ accent: "#4F46E5",
3562
+ // Screenshot / screen recording / file attach. Capture needs getDisplayMedia,
3563
+ // so it is offered only where the browser has it — file attach always works.
3564
+ attachments: !0,
3565
+ maxAttachments: 5,
3566
+ maxAttachmentBytes: 25 * 1024 * 1024,
3567
+ maxRecordingMs: 12e4,
3568
+ // Called before send; return false to cancel, or mutate the payload to scrub.
3569
+ beforeSend: null,
3570
+ user: null,
3571
+ // Tell the reporter when a bug they filed changes status — live, in your
3572
+ // app, where they already are. Off means no connection is opened at all.
3573
+ updates: !0,
3574
+ // Show the built-in notification when an update arrives. It stays until the
3575
+ // reporter marks it read — including across a reload — so an update is not
3576
+ // lost to whoever happened to be looking at the tab. Turn it off and drive
3577
+ // the notification yourself from onUpdate and unreadUpdates().
3578
+ updateToast: !0,
3579
+ // Called with { id, bugId, title, status, at } whenever one of this browser's
3580
+ // reports moves. Runs whether or not the built-in notification is on; `id` is
3581
+ // what markUpdateRead() takes.
3582
+ onUpdate: null,
3583
+ // Offer a voice call when a report is picked up, if the workspace has
3584
+ // sphoro.voice configured. Nothing is dialled and no permission is asked for
3585
+ // until somebody presses the button — see startCall().
3586
+ voice: !0,
3587
+ // Called as a call moves: 'connecting', 'listening', 'speaking', 'ended',
3588
+ // 'failed'. The SDK shows nothing of its own beyond the offer, so an
3589
+ // integration that wants a call UI builds it from this and onCallTranscript.
3590
+ onCallState: null,
3591
+ // Called with { role, text, final } for each line, as it is recognised.
3592
+ onCallTranscript: null,
3593
+ // Called with { bugId, callId, from, project, reference, ringsUntil,
3594
+ // recorded } when somebody on the team rings this page about a report filed
3595
+ // from it. The SDK has already drawn the card by then — this is for a host
3596
+ // application that wants to duck its own audio or pause a video, not a hook
3597
+ // that can suppress a call the person can see ringing. Set `voice: false` to
3598
+ // switch the whole thing off.
3599
+ onIncomingCall: null
3600
+ };
3601
+ var A, fe, ft, gt, fn, mt, gn, Ve, mn;
3602
+ class hn {
3603
+ constructor() {
3604
+ At(this, A);
3605
+ this.config = { ...De }, this.started = !1, this.teardown = [], this.consoleBuffer = He(De.consoleLimit), this.networkBuffer = He(De.networkLimit), this.stopWatching = null, this.notifier = null, this.call = null, this.callStarting = !1, this.ringing = null, this.listeners = /* @__PURE__ */ new Map();
3606
+ }
3607
+ init(e = {}) {
3608
+ if (this.started)
3609
+ return console.warn("[BugCatch] init() called twice — ignoring the second call."), this;
3610
+ if (this.config = { ...De, ...e }, !this.config.apiKey) throw new Error("[BugCatch] apiKey is required.");
3611
+ if (!this.config.projectId) throw new Error("[BugCatch] projectId is required.");
3612
+ return this.consoleBuffer = He(this.config.consoleLimit), this.networkBuffer = He(this.config.networkLimit), this.config.captureConsole && this.teardown.push(An(this.consoleBuffer)), this.config.captureNetwork && this.teardown.push(Qn(this.networkBuffer)), this.config.captureErrors && this.teardown.push(Rn(this.consoleBuffer)), this.config.widget && this.teardown.push(Ca(this)), this.config.updates && ne() && (this.startWatching(), queueMicrotask(() => N(this, A, fe).call(this))), this.started = !0, this;
3613
+ }
3614
+ /**
3615
+ * Open the update stream. Idempotent — init() and a first report both reach
3616
+ * here, and a second connection would double every notification.
3617
+ */
3618
+ startWatching() {
3619
+ if (this.stopWatching) return this;
3620
+ this.stopWatching = ca({
3621
+ apiBase: this.config.apiBase,
3622
+ onUpdate: (n) => {
3623
+ if (n != null && n.incomingCall) {
3624
+ N(this, A, gn).call(this, n);
3625
+ return;
3626
+ }
3627
+ if (n != null && n.callWithdrawn) {
3628
+ N(this, A, Ve).call(this, n.callWithdrawn.callId);
3629
+ return;
3630
+ }
3631
+ const a = Fn(n);
3632
+ typeof this.config.onUpdate == "function" && this.config.onUpdate(a), this.emit("update", a), N(this, A, fe).call(this);
3633
+ }
3634
+ }), this.teardown.push(() => {
3635
+ var n;
3636
+ return (n = this.stopWatching) == null ? void 0 : n.call(this);
3637
+ }), this.teardown.push(() => N(this, A, Ve).call(this));
3638
+ const e = (n) => {
3639
+ (n.key === _e || n.key === null) && N(this, A, fe).call(this);
3640
+ };
3641
+ return window.addEventListener("storage", e), this.teardown.push(() => window.removeEventListener("storage", e)), this;
3642
+ }
3643
+ /**
3644
+ * Subscribe to updates on this browser's reports. Returns an unsubscribe.
3645
+ *
3646
+ * const off = BugCatch.on('update', (u) => console.log(u.summary));
3647
+ *
3648
+ * Alongside the `onUpdate` config option rather than replacing it: that one
3649
+ * is the single handler an integration sets at init, this one lets several
3650
+ * parts of an app listen independently — a badge, a toast of your own, an
3651
+ * open report view — without them having to agree on one callback.
3652
+ */
3653
+ on(e, n) {
3654
+ if (typeof n != "function") return () => {
3655
+ };
3656
+ const a = this.listeners.get(e) || /* @__PURE__ */ new Set();
3657
+ return a.add(n), this.listeners.set(e, a), () => this.off(e, n);
3658
+ }
3659
+ off(e, n) {
3660
+ var a;
3661
+ return (a = this.listeners.get(e)) == null || a.delete(n), this;
3662
+ }
3663
+ /** Notify listeners; one that throws must not stop the rest. */
3664
+ emit(e, n) {
3665
+ for (const a of this.listeners.get(e) || [])
3666
+ try {
3667
+ a(n);
3668
+ } catch (o) {
3669
+ console.error("[BugCatch] an update listener threw:", o);
3670
+ }
3671
+ }
3672
+ /**
3673
+ * Updates this browser has been told about and nobody has read yet, oldest
3674
+ * first.
3675
+ *
3676
+ * BugCatch.unreadUpdates().length // a badge on your own bell
3677
+ *
3678
+ * Survives a reload: the stream replays nothing when it reconnects, so an
3679
+ * update that arrived while the tab was closed would otherwise be gone.
3680
+ * Each carries the `id` the mark-read calls take.
3681
+ */
3682
+ unreadUpdates() {
3683
+ return Ie();
3684
+ }
3685
+ /**
3686
+ * Mark one update read. It stops being shown, here and on the next load.
3687
+ *
3688
+ * BugCatch.on('update', (u) => myBell.add(u, () => BugCatch.markUpdateRead(u.id)));
3689
+ *
3690
+ * Read is something the reporter says, not something a timer decides — which
3691
+ * is the whole reason these are held rather than shown for eight seconds and
3692
+ * dropped. An integration rendering its own notifications is the one that
3693
+ * knows when its reader has seen one, so it has to say.
3694
+ */
3695
+ markUpdateRead(e) {
3696
+ return Yn(e), N(this, A, fe).call(this), this;
3697
+ }
3698
+ /**
3699
+ * Mark everything about one report read.
3700
+ *
3701
+ * Opening the report is reading its updates, so the widget calls this when
3702
+ * the thread opens; an integration with its own report view should too.
3703
+ */
3704
+ markReportRead(e) {
3705
+ return Zn(e), N(this, A, fe).call(this), this;
3706
+ }
3707
+ /** Mark everything read. The "clear all" behind your own notification list. */
3708
+ markUpdatesRead() {
3709
+ return tn(), N(this, A, fe).call(this), this;
3710
+ }
3711
+ /**
3712
+ * One report in full, with its timeline — every step from filing to now.
3713
+ *
3714
+ * const report = await BugCatch.myReport(id);
3715
+ * report.timeline // [{ kind: 'filed' | 'status' | 'severity' | ... , summary, at }]
3716
+ *
3717
+ * Resolves to null when this browser cannot prove it filed that report.
3718
+ */
3719
+ myReport(e) {
3720
+ return Gn({ apiBase: this.config.apiBase, bugId: e });
3721
+ }
3722
+ /**
3723
+ * Write back on a report this browser filed.
3724
+ *
3725
+ * const key = await BugCatch.uploadAttachment(file, file.type);
3726
+ * await BugCatch.replyToReport(id, {
3727
+ * body: 'Still happening on Safari',
3728
+ * attachments: [{ ...key, name: file.name, size: file.size }],
3729
+ * });
3730
+ *
3731
+ * The widget's own conversation view calls this; it is public so an
3732
+ * integration rendering its own can too. Resolves to the stored message, so
3733
+ * the caller renders what the server kept rather than what it hoped it sent.
3734
+ */
3735
+ replyToReport(e, { body: n, attachments: a } = {}) {
3736
+ return nn({ apiBase: this.config.apiBase, bugId: e, body: n, attachments: a });
3737
+ }
3738
+ /**
3739
+ * Whether this visitor can report something by talking about it instead.
3740
+ *
3741
+ * if (await BugCatch.canCallNow()) showYourOwnCallButton();
3742
+ *
3743
+ * Two conditions, and both have to hold before anything is drawn. The project
3744
+ * has to have calling configured — false on most deployments — and this
3745
+ * browser has to know who the visitor is, because a call with nobody to
3746
+ * attribute it to has no watch token to follow it with and nobody to send the
3747
+ * transcript to afterwards. That is the same limit every other follow-up
3748
+ * feature in this SDK has: it all hangs off `identify()`.
3749
+ */
3750
+ async canCallNow() {
3751
+ return !!await this.callKind();
3752
+ }
3753
+ /**
3754
+ * Which kind of call this visitor would get: 'agent', 'live', or null.
3755
+ *
3756
+ * const kind = await BugCatch.callKind();
3757
+ * // 'agent' — a model answers and asks the questions the form did not
3758
+ * // 'live' — the team's dashboards ring and a colleague picks up
3759
+ *
3760
+ * A deployment offers one or the other, never both: a sphoro.voice agent has
3761
+ * exactly one mode. Worth asking rather than assuming, because the two are
3762
+ * wholly different promises to make to somebody — and the wording next to the
3763
+ * button has to match which one it is.
3764
+ *
3765
+ * The answer is remembered for the life of the page. It is a property of the
3766
+ * deployment's configuration rather than of this visitor, and re-asking on
3767
+ * every dialog open is a request per page view for something that changes
3768
+ * about once a year.
3769
+ */
3770
+ async callKind() {
3771
+ var e;
3772
+ return !this.config.voice || !((e = this.config.user) != null && e.email) ? null : this.kind !== void 0 ? this.kind : (this.kind = await ea({
3773
+ apiBase: this.config.apiBase,
3774
+ apiKey: this.config.apiKey,
3775
+ projectId: this.config.projectId
3776
+ }), this.kind);
3777
+ }
3778
+ /**
3779
+ * Whether a report can be talked through right now.
3780
+ *
3781
+ * if (await BugCatch.canCall(id)) showYourOwnButton();
3782
+ *
3783
+ * False on every deployment without sphoro.voice configured, on every
3784
+ * workspace that has not switched calling on, and on a report that has had
3785
+ * its allowance of calls — so an integration rendering its own affordance can
3786
+ * ask rather than guess.
3787
+ */
3788
+ canCall(e) {
3789
+ return this.config.voice ? na({ apiBase: this.config.apiBase, bugId: e }) : Promise.resolve(!1);
3790
+ }
3791
+ /**
3792
+ * Talk to the triage agent.
3793
+ *
3794
+ * await BugCatch.startCall(id); // about a report already filed
3795
+ * await BugCatch.startCall(); // instead of filling the form in
3796
+ *
3797
+ * With a report id, it talks about that report. With none, it files one first
3798
+ * — carrying the console, network and environment this SDK is already holding
3799
+ * — and then calls about that. The report is written before the call starts
3800
+ * either way, so a call that drops still leaves the context behind; the
3801
+ * handle carries `bugId` so the caller can say what it was filed as.
3802
+ *
3803
+ * Asks for the microphone, opens the call, and resolves once it is connected.
3804
+ * What is said reaches the team as a message in the report's thread when the
3805
+ * call ends — nobody from the team is on the line, and the agent says so.
3806
+ *
3807
+ * Rejects if the microphone is refused, if calling is not available, or if
3808
+ * one is already running. There is deliberately no way to start a second: two
3809
+ * microphones on one page talk over each other, and the second call would be
3810
+ * transcribing the first. A rejection carrying `.filed` means the report was
3811
+ * written and only the call failed.
3812
+ */
3813
+ async startCall(e) {
3814
+ if (!this.config.voice) throw new Error("[BugCatch] calling is switched off.");
3815
+ if (this.call || this.callStarting)
3816
+ throw new Error("[BugCatch] a call is already running.");
3817
+ this.callStarting = !0;
3818
+ try {
3819
+ return await N(this, A, fn).call(this, e);
3820
+ } finally {
3821
+ this.callStarting = !1;
3822
+ }
3823
+ }
3824
+ /**
3825
+ * Talk to a person on the team.
3826
+ *
3827
+ * await BugCatch.startLiveCall(id);
3828
+ *
3829
+ * The other kind of call. `startCall` reaches the triage agent, which answers
3830
+ * instantly and asks the three questions the form did not; this rings the
3831
+ * team, and whoever is free picks it up. What comes back is a conversation
3832
+ * with a colleague — the audio travels directly between the two browsers over
3833
+ * WebRTC and never reaches bugcatch or sphoro.voice, so it is not recorded and
3834
+ * not transcribed. The thread gets a note saying it happened and who took it.
3835
+ *
3836
+ * Resolves once the call is *ringing*, not once somebody answers — the
3837
+ * handle's `onState` is how the caller follows what happens next:
3838
+ *
3839
+ * 'ringing' the team's dashboards are lighting up
3840
+ * 'connecting' somebody answered; the two browsers are finding each other
3841
+ * 'connected' they can hear each other
3842
+ * 'missed' nobody picked up before the ringing stopped
3843
+ * 'ended' it is over
3844
+ *
3845
+ * A report id is required, unlike `startCall`. Ringing a room about a report
3846
+ * nobody can read would ask somebody to answer a call they know nothing
3847
+ * about — and the agent call's trick of filing first and taking a title from
3848
+ * the transcript is not available here, because there is no transcript.
3849
+ *
3850
+ * Rejects if the microphone is refused, if live calling is not available on
3851
+ * this report, or if a call of either kind is already running: this page has
3852
+ * one microphone, and two calls on it would talk over each other.
3853
+ */
3854
+ async startLiveCall(e) {
3855
+ if (!this.config.voice) throw new Error("[BugCatch] calling is switched off.");
3856
+ if (!e) throw new Error("[BugCatch] startLiveCall needs the id of a filed report.");
3857
+ if (!Ne())
3858
+ throw new Error("[BugCatch] this browser cannot make live calls.");
3859
+ if (this.call || this.callStarting)
3860
+ throw new Error("[BugCatch] a call is already running.");
3861
+ this.callStarting = !0;
3862
+ try {
3863
+ return await N(this, A, mt).call(this, e);
3864
+ } finally {
3865
+ this.callStarting = !1;
3866
+ }
3867
+ }
3868
+ /**
3869
+ * Whether this report can be talked through with a person.
3870
+ *
3871
+ * For a host application drawing its own button. The widget asks this itself.
3872
+ */
3873
+ canCallTeam(e) {
3874
+ return this.config.voice ? oa({ apiBase: this.config.apiBase, bugId: e }) : Promise.resolve(!1);
3875
+ }
3876
+ /**
3877
+ * End the call in progress, of either kind. Does nothing when there is not
3878
+ * one — there is a single slot, because there is a single microphone.
3879
+ */
3880
+ endCall() {
3881
+ var e;
3882
+ return (e = this.call) == null || e.hangUp(), this.call = null, this;
3883
+ }
3884
+ /**
3885
+ * Everything this browser has reported, newest first, with the status of
3886
+ * each — the history a reporter normally has no way to see.
3887
+ *
3888
+ * const { reports } = await BugCatch.myReports();
3889
+ * // [{ reference: '3F2A9B01', title, status, priority, createdAt, ... }]
3890
+ *
3891
+ * Resolves to an empty history rather than throwing when nothing has been
3892
+ * filed from this browser, or when identify() supplied no email — there is
3893
+ * nothing to look up in either case.
3894
+ */
3895
+ myReports() {
3896
+ return Jn({ apiBase: this.config.apiBase });
3897
+ }
3898
+ /** True when this browser has filed something, so a "your reports" entry
3899
+ * point can be hidden until it would show anything. */
3900
+ hasReports() {
3901
+ return !!ne();
3902
+ }
3903
+ /**
3904
+ * Forget this browser's reports — call on sign-out, or the next person at
3905
+ * this machine inherits the previous one's notifications.
3906
+ */
3907
+ forgetReports() {
3908
+ var e;
3909
+ return (e = this.stopWatching) == null || e.call(this), this.stopWatching = null, this.endCall(), ge(), N(this, A, fe).call(this), this;
3910
+ }
3911
+ /**
3912
+ * Attach the signed-in user so reports say who hit the bug.
3913
+ *
3914
+ * `email` is the field that matters beyond display: it threads every report
3915
+ * from the same person together in the dashboard, and it is where the team's
3916
+ * "your issue is resolved" email goes. Because your app has already
3917
+ * authenticated this person, an address sent here is treated as reachable —
3918
+ * unlike one typed into a public recording-link form, which stays unusable
3919
+ * for outbound mail until the reporter answers a confirmation code.
3920
+ *
3921
+ * BugCatch.identify({ id: 'u_42', email: 'alex@acme.com', name: 'Alex' });
3922
+ *
3923
+ * Pass null to forget them again on sign-out.
3924
+ */
3925
+ identify(e) {
3926
+ return this.config.user = e, this;
3927
+ }
3928
+ /**
3929
+ * Upload a screenshot, recording or file and get back the storage key that
3930
+ * `report({ attachments })` references.
3931
+ *
3932
+ * The blob goes straight to storage on a presigned URL — it never passes
3933
+ * through the API — so this is also what you call when you drive the SDK
3934
+ * yourself with `widget: false`.
3935
+ */
3936
+ async uploadAttachment(e, n, a) {
3937
+ if (!this.started) throw new Error("[BugCatch] call init() first.");
3938
+ return Pn({
3939
+ apiBase: this.config.apiBase,
3940
+ apiKey: this.config.apiKey,
3941
+ blob: e,
3942
+ mime: n || (e == null ? void 0 : e.type),
3943
+ onProgress: a
3944
+ });
3945
+ }
3946
+ /** File a bug with everything captured so far. */
3947
+ async report({
3948
+ title: e,
3949
+ description: n = "",
3950
+ priority: a = "MEDIUM",
3951
+ metadata: o = {},
3952
+ attachments: r = []
3953
+ } = {}) {
3954
+ if (!this.started) throw new Error("[BugCatch] call init() first.");
3955
+ if (!e || !e.trim()) throw new Error("[BugCatch] a title is required.");
3956
+ const s = (r || []).filter((d) => (d == null ? void 0 : d.storageKey) && Ke(d.mime, Pe)).map((d) => ({
3957
+ type: d.type === "VIDEO" ? "VIDEO" : "SCREENSHOT",
3958
+ storageKey: d.storageKey,
3959
+ mime: Ke(d.mime, Pe)
3960
+ }));
3961
+ let l = await N(this, A, ft).call(this, { title: e.trim(), description: n, priority: a, metadata: o, media: s });
3962
+ if (typeof this.config.beforeSend == "function") {
3963
+ const d = this.config.beforeSend(l);
3964
+ if (d === !1) return { cancelled: !0 };
3965
+ d && typeof d == "object" && (l = d);
3966
+ }
3967
+ const u = he(this.config.projectId), g = await fetch(`${this.config.apiBase}/ingest/bugs`, {
3968
+ method: "POST",
3969
+ headers: {
3970
+ "Content-Type": "application/json",
3971
+ "X-BugCatch-Key": this.config.apiKey,
3972
+ // Present only where the reporter signed in. The server takes the
3973
+ // address from here rather than from the payload when it is — on a
3974
+ // project that requires sign-in, letting the page name a different
3975
+ // reporter would undo the requirement.
3976
+ ...u != null && u.token ? { "X-BugCatch-Session": u.token } : {}
3977
+ },
3978
+ body: JSON.stringify(l)
3979
+ });
3980
+ if (g.status === 401 && u && dt(this.config.projectId), !g.ok) {
3981
+ let d = "";
3982
+ try {
3983
+ const w = await g.json();
3984
+ d = Array.isArray(w.message) ? w.message.join(", ") : w.message || "";
3985
+ } catch {
3986
+ }
3987
+ throw new Error(`[BugCatch] report failed (HTTP ${g.status})${d ? ": " + d : ""}`);
3988
+ }
3989
+ const b = await g.json();
3990
+ return b.watchToken && (lt(b.watchToken), this.config.updates && this.startWatching()), b;
3991
+ }
3992
+ /**
3993
+ * What this project asks of the person reporting, and who this browser is
3994
+ * signed in as right now.
3995
+ *
3996
+ * const { mode, signedIn } = await BugCatch.loginState();
3997
+ * // mode: 'off' | 'optional' | 'required'
3998
+ *
3999
+ * The widget calls this itself every time it opens. It is here for
4000
+ * integrations running with `widget: false`, which have to draw their own
4001
+ * sign-in and need the same answer to know whether to draw one at all.
4002
+ *
4003
+ * The mode is the project's setting, changed in BugCatch under Project
4004
+ * Settings → General, not in this config. A widget that could opt out of a
4005
+ * required sign-in would not be a requirement.
4006
+ */
4007
+ loginState() {
4008
+ var e;
4009
+ return Aa({
4010
+ apiBase: this.config.apiBase,
4011
+ apiKey: this.config.apiKey,
4012
+ projectId: this.config.projectId,
4013
+ token: (e = he(this.config.projectId)) == null ? void 0 : e.token
4014
+ });
4015
+ }
4016
+ /**
4017
+ * Who this browser is signed in as on this project, from storage alone.
4018
+ *
4019
+ * BugCatch.signedInAs() // { email, name } | null
4020
+ *
4021
+ * Synchronous, and therefore only as current as the last round trip: a
4022
+ * session ended elsewhere still reads as signed in here until something asks
4023
+ * the server. Use loginState() where that matters.
4024
+ */
4025
+ signedInAs() {
4026
+ const e = he(this.config.projectId);
4027
+ return e ? { email: e.email, name: e.name } : null;
4028
+ }
4029
+ /**
4030
+ * Step one of signing in: mail a code to any address.
4031
+ *
4032
+ * await BugCatch.signIn('someone@example.com');
4033
+ * await BugCatch.confirmSignIn('someone@example.com', '481920');
4034
+ *
4035
+ * Any address, deliberately — there is no list of people who may report a
4036
+ * bug in somebody's product, and asking for one would be asking the customer
4037
+ * to maintain it. What the code proves is that whoever typed it can read the
4038
+ * address, which is all a reply-to has ever needed to be worth.
4039
+ */
4040
+ signIn(e, { name: n } = {}) {
4041
+ var a;
4042
+ return Qa({
4043
+ apiBase: this.config.apiBase,
4044
+ apiKey: this.config.apiKey,
4045
+ projectId: this.config.projectId,
4046
+ email: e,
4047
+ // The name the host app already knows beats asking for one again.
4048
+ name: n ?? ((a = this.config.user) == null ? void 0 : a.name) ?? null
4049
+ });
4050
+ }
4051
+ /**
4052
+ * Step two: spend the code and hold the session.
4053
+ *
4054
+ * Signing in signs this address out of this project in every other browser —
4055
+ * one live session per address per project. Somebody who signs in on their
4056
+ * phone has signed their laptop out, on purpose.
4057
+ */
4058
+ async confirmSignIn(e, n) {
4059
+ var o;
4060
+ const a = await Ra({
4061
+ apiBase: this.config.apiBase,
4062
+ apiKey: this.config.apiKey,
4063
+ projectId: this.config.projectId,
4064
+ email: e,
4065
+ code: n
4066
+ });
4067
+ return (o = this.config.user) != null && o.email || (this.config.user = { email: a.email, name: a.name || void 0 }), this.emit("signin", { email: a.email, name: a.name }), { email: a.email, name: a.name };
4068
+ }
4069
+ /**
4070
+ * Sign out of this project.
4071
+ *
4072
+ * Ends the session on the server too, so the token cannot be replayed from a
4073
+ * copy of this browser's storage. Local state goes whatever the network
4074
+ * says — sign-out on a shared machine has to mean signed out on it.
4075
+ */
4076
+ async signOut() {
4077
+ var a;
4078
+ const e = he(this.config.projectId), n = await ja({
4079
+ apiBase: this.config.apiBase,
4080
+ apiKey: this.config.apiKey,
4081
+ projectId: this.config.projectId,
4082
+ token: e == null ? void 0 : e.token
4083
+ });
4084
+ return e != null && e.email && ((a = this.config.user) == null ? void 0 : a.email) === e.email && (this.config.user = null), this.emit("signout", { email: (e == null ? void 0 : e.email) || null }), n;
4085
+ }
4086
+ /** What would be sent right now — useful for debugging integrations. */
4087
+ /**
4088
+ * What this project is already known to be having trouble with.
4089
+ *
4090
+ * The widget asks this itself when it opens; it is public for a host
4091
+ * application drawing its own help surface with `widget: false`. Resolves to
4092
+ * an empty list on every project that has not switched the feature on, which
4093
+ * is the default — draw nothing rather than an empty heading.
4094
+ */
4095
+ knownIssues() {
4096
+ var e;
4097
+ return ga({
4098
+ apiBase: this.config.apiBase,
4099
+ apiKey: this.config.apiKey,
4100
+ projectId: this.config.projectId,
4101
+ sessionToken: (e = he(this.config.projectId)) == null ? void 0 : e.token
4102
+ });
4103
+ }
4104
+ /**
4105
+ * The known issues that look like a description somebody is writing.
4106
+ *
4107
+ * Pass an AbortSignal when calling this on a keystroke, or a fast typist gets
4108
+ * the results of whichever request happens to return last.
4109
+ */
4110
+ matchKnownIssues(e, { signal: n } = {}) {
4111
+ var a;
4112
+ return ma({
4113
+ apiBase: this.config.apiBase,
4114
+ apiKey: this.config.apiKey,
4115
+ projectId: this.config.projectId,
4116
+ q: e || "",
4117
+ sessionToken: (a = he(this.config.projectId)) == null ? void 0 : a.token,
4118
+ signal: n
4119
+ });
4120
+ }
4121
+ /**
4122
+ * Say a known issue is happening to you too, instead of filing a second
4123
+ * report about it.
4124
+ *
4125
+ * Sends the same capture a report would — this browser's console, network and
4126
+ * environment — because that is what the team gets out of somebody joining
4127
+ * rather than filing: one report with everybody's environment on it.
4128
+ *
4129
+ * Needs an address. A signed-in widget has one, and identify() supplies one
4130
+ * otherwise; pass `email` for the case where neither did. Joining means "tell
4131
+ * me when this is fixed", and there is no way to keep that promise to
4132
+ * somebody anonymous.
4133
+ */
4134
+ async meToo(e, { email: n, name: a } = {}) {
4135
+ var s, l;
4136
+ const o = he(this.config.projectId), r = await Rt();
4137
+ return ba({
4138
+ apiBase: this.config.apiBase,
4139
+ apiKey: this.config.apiKey,
4140
+ projectId: this.config.projectId,
4141
+ bugId: e,
4142
+ reporterEmail: n || ((s = this.config.user) == null ? void 0 : s.email),
4143
+ reporterName: a || ((l = this.config.user) == null ? void 0 : l.name),
4144
+ metadata: { ...r, user: this.config.user || void 0, sdk: Wt },
4145
+ logs: {
4146
+ console: { entries: this.consoleBuffer.all() },
4147
+ network: { entries: this.networkBuffer.all() }
4148
+ },
4149
+ sessionToken: o == null ? void 0 : o.token
4150
+ });
4151
+ }
4152
+ snapshot() {
4153
+ return {
4154
+ console: this.consoleBuffer.all(),
4155
+ network: this.networkBuffer.all(),
4156
+ environment: Jt()
4157
+ };
4158
+ }
4159
+ clear() {
4160
+ this.consoleBuffer.clear(), this.networkBuffer.clear();
4161
+ }
4162
+ /** Restore every patched global and remove the widget. */
4163
+ destroy() {
4164
+ this.endCall(), this.teardown.forEach((e) => {
4165
+ try {
4166
+ e();
4167
+ } catch {
4168
+ }
4169
+ }), this.teardown = [], this.notifier = null, this.started = !1;
4170
+ }
4171
+ }
4172
+ A = new WeakSet(), /**
4173
+ * Redraw the built-in notifications, and tell anyone counting.
4174
+ *
4175
+ * The single path out of the unread store: every add and every mark-read ends
4176
+ * here, so the cards on screen and a host app's own badge cannot disagree
4177
+ * about what is outstanding.
4178
+ */
4179
+ fe = function() {
4180
+ const e = Ie();
4181
+ if (this.emit("unread", { count: e.length, updates: e }), !!this.config.updateToast) {
4182
+ if (!this.notifier) {
4183
+ if (!e.length) return;
4184
+ this.notifier = fa({
4185
+ accent: this.config.accent,
4186
+ position: this.config.widgetPosition,
4187
+ onRead: (n) => this.markUpdateRead(n),
4188
+ onReadAll: () => this.markUpdatesRead(),
4189
+ // The call button appears only where the API said the offer stands and
4190
+ // this page has calling switched on. Pressing it reads the update —
4191
+ // acting on a notification is the clearest way there is of having seen
4192
+ // it — and `startCall` does the rest.
4193
+ onCall: this.config.voice ? (n) => {
4194
+ this.markUpdateRead(n.id), this.startCall(n.bugId);
4195
+ } : null
4196
+ }), this.teardown.push(() => {
4197
+ var n;
4198
+ return (n = this.notifier) == null ? void 0 : n.destroy();
4199
+ });
4200
+ }
4201
+ this.notifier.render(e);
4202
+ }
4203
+ }, ft = async function({ title: e, description: n = "", priority: a = "MEDIUM", metadata: o = {}, media: r = [] }) {
4204
+ var s, l;
4205
+ return {
4206
+ projectId: this.config.projectId,
4207
+ ...e ? { title: e } : {},
4208
+ description: n,
4209
+ priority: a,
4210
+ url: window.location.href,
4211
+ reporterEmail: (s = this.config.user) == null ? void 0 : s.email,
4212
+ // Sent alongside the address so the dashboard can show a person rather
4213
+ // than a mailbox, and so the resolution email can open with their name.
4214
+ reporterName: (l = this.config.user) == null ? void 0 : l.name,
4215
+ metadata: {
4216
+ ...await Rt(),
4217
+ ...o,
4218
+ user: this.config.user || void 0,
4219
+ sdk: Wt
4220
+ },
4221
+ logs: {
4222
+ console: { entries: this.consoleBuffer.all() },
4223
+ network: { entries: this.networkBuffer.all() }
4224
+ },
4225
+ // Omitted rather than sent empty: the field is optional server-side, and
4226
+ // an empty array in every payload makes beforeSend hooks harder to read.
4227
+ ...r.length ? { attachments: r } : {}
4228
+ };
4229
+ }, gt = async function() {
4230
+ const e = he(this.config.projectId);
4231
+ let n;
4232
+ try {
4233
+ n = await ta({
4234
+ apiBase: this.config.apiBase,
4235
+ apiKey: this.config.apiKey,
4236
+ token: e == null ? void 0 : e.token,
4237
+ payload: await N(this, A, ft).call(this, {})
4238
+ });
4239
+ } catch (a) {
4240
+ throw a.status === 401 && e && dt(this.config.projectId), a;
4241
+ }
4242
+ return n.watchToken && (lt(n.watchToken), this.config.updates && this.startWatching()), n;
4243
+ }, fn = async function(e) {
4244
+ if (await this.callKind() === "live") return N(this, A, mt).call(this, e);
4245
+ let n, a = null;
4246
+ if (e)
4247
+ ({ realtime_url: n } = await aa({
4248
+ apiBase: this.config.apiBase,
4249
+ bugId: e
4250
+ }));
4251
+ else if (a = await N(this, A, gt).call(this), e = a.id, n = a.realtime_url, !n) {
4252
+ const r = new Error("[BugCatch] the report was filed, but no call could be started.");
4253
+ throw r.filed = a, r;
4254
+ }
4255
+ const o = await qa({
4256
+ realtimeUrl: n,
4257
+ onState: (r, s) => {
4258
+ (r === "ended" || r === "failed") && (this.call = null), typeof this.config.onCallState == "function" && this.config.onCallState(r, { bugId: e, ...s }), this.emit("call", { bugId: e, state: r, ...s });
4259
+ },
4260
+ onTranscript: (r) => {
4261
+ typeof this.config.onCallTranscript == "function" && this.config.onCallTranscript({ bugId: e, ...r }), this.emit("call-transcript", { bugId: e, ...r });
4262
+ }
4263
+ });
4264
+ return this.call = o, Object.assign(o, { bugId: e, filed: a });
4265
+ }, mt = async function(e) {
4266
+ if (!Ne())
4267
+ throw new Error("[BugCatch] this browser cannot make live calls.");
4268
+ const n = await ht();
4269
+ let a, o = null;
4270
+ try {
4271
+ if (e)
4272
+ a = await ra({ apiBase: this.config.apiBase, bugId: e });
4273
+ else if (o = await N(this, A, gt).call(this), e = o.id, a = o.live_call, !(a != null && a.realtime_url)) {
4274
+ const s = new Error("[BugCatch] the report was filed, but the team could not be rung.");
4275
+ throw s.filed = o, s;
4276
+ }
4277
+ } catch (s) {
4278
+ for (const l of n.getTracks()) l.stop();
4279
+ throw s;
4280
+ }
4281
+ const r = await Vt({
4282
+ realtimeUrl: a.realtime_url,
4283
+ stream: n,
4284
+ ringsUntil: a.rings_until,
4285
+ onState: (s, l) => {
4286
+ (s === "ended" || s === "failed" || s === "missed") && (this.call = null), s === "missed" && ia({ apiBase: this.config.apiBase, bugId: e, callId: a.call_id }), typeof this.config.onCallState == "function" && this.config.onCallState(s, { bugId: e, live: !0, ...l }), this.emit("call", { bugId: e, live: !0, state: s, ...l });
4287
+ }
4288
+ });
4289
+ return this.call = r, Object.assign(r, { bugId: e, callId: a.call_id, live: !0, filed: o });
4290
+ }, // --- being called ------------------------------------------------------------
4291
+ /**
4292
+ * Somebody on the team is ringing this page.
4293
+ *
4294
+ * The only thing in this SDK that starts without the person on the page
4295
+ * asking for it, which is why what it does is so much narrower than what the
4296
+ * outgoing calls do: it draws a card and makes a soft noise. No microphone is
4297
+ * opened and nothing is connected until Answer is pressed, and if nothing is
4298
+ * pressed the card takes itself away when the ringing stops.
4299
+ *
4300
+ * Silently ignored when calling is switched off in this integration's config
4301
+ * or the browser cannot hold a peer connection. A host application that has
4302
+ * said `voice: false` has said its customers are not to be rung, and a card
4303
+ * offering a call this page could not join would be worse than none.
4304
+ */
4305
+ gn = function(e) {
4306
+ var o;
4307
+ const n = e.incomingCall;
4308
+ if (!this.config.voice || !Ne() || ((o = this.ringing) == null ? void 0 : o.callId) === n.callId || this.call || this.callStarting) return;
4309
+ N(this, A, Ve).call(this);
4310
+ const a = Fa({
4311
+ call: n,
4312
+ accent: this.config.accent,
4313
+ position: this.config.widgetPosition,
4314
+ onAnswer: () => {
4315
+ N(this, A, mn).call(this, e.bugId, n);
4316
+ },
4317
+ onDecline: () => {
4318
+ this.ringing = null, zt({
4319
+ apiBase: this.config.apiBase,
4320
+ bugId: e.bugId,
4321
+ callId: n.callId
4322
+ }), this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "declined" });
4323
+ },
4324
+ // Nothing is reported here. The member who placed it is watching it ring
4325
+ // on their own screen and their browser tells the API when it stops —
4326
+ // the same rule as the other direction, where whoever placed the call is
4327
+ // the one that reports nobody answered.
4328
+ onExpire: () => {
4329
+ this.ringing = null, this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "missed" });
4330
+ }
4331
+ });
4332
+ if (this.ringing = { callId: n.callId, dismiss: a }, typeof this.config.onIncomingCall == "function")
4333
+ try {
4334
+ this.config.onIncomingCall({ bugId: e.bugId, ...n });
4335
+ } catch {
4336
+ }
4337
+ this.emit("incoming-call", { bugId: e.bugId, callId: n.callId, state: "ringing" });
4338
+ }, /** Take the card away, whatever it is showing. */
4339
+ Ve = function(e) {
4340
+ if (this.ringing && !(e && this.ringing.callId !== e)) {
4341
+ try {
4342
+ this.ringing.dismiss();
4343
+ } catch {
4344
+ }
4345
+ this.ringing = null;
4346
+ }
4347
+ }, mn = async function(e, n) {
4348
+ if (this.ringing = null, this.call || this.callStarting) return;
4349
+ this.callStarting = !0;
4350
+ let a;
4351
+ try {
4352
+ a = await ht();
4353
+ } catch (r) {
4354
+ this.callStarting = !1, zt({ apiBase: this.config.apiBase, bugId: e, callId: n.callId }), this.emit("incoming-call", { bugId: e, callId: n.callId, state: "failed", error: r });
4355
+ return;
4356
+ }
4357
+ let o;
4358
+ try {
4359
+ o = await sa({
4360
+ apiBase: this.config.apiBase,
4361
+ bugId: e,
4362
+ callId: n.callId
4363
+ });
4364
+ } catch (r) {
4365
+ for (const s of a.getTracks()) s.stop();
4366
+ this.callStarting = !1, this.emit("incoming-call", { bugId: e, callId: n.callId, state: "missed", error: r });
4367
+ return;
4368
+ }
4369
+ try {
4370
+ const r = await Vt({
4371
+ realtimeUrl: o.realtime_url,
4372
+ stream: a,
4373
+ // No deadline on this side. The ringing is over — it ended when this
4374
+ // browser claimed the call — and a countdown here would hang up on a
4375
+ // conversation that has already started.
4376
+ onState: (s, l) => {
4377
+ (s === "ended" || s === "failed" || s === "missed") && (this.call = null), typeof this.config.onCallState == "function" && this.config.onCallState(s, { bugId: e, live: !0, incoming: !0, ...l }), this.emit("call", { bugId: e, live: !0, incoming: !0, state: s, ...l });
4378
+ }
4379
+ });
4380
+ this.call = r, this.emit("incoming-call", { bugId: e, callId: n.callId, state: "answered" });
4381
+ } catch (r) {
4382
+ for (const s of a.getTracks()) s.stop();
4383
+ this.emit("incoming-call", { bugId: e, callId: n.callId, state: "failed", error: r });
4384
+ } finally {
4385
+ this.callStarting = !1;
4386
+ }
4387
+ };
4388
+ const Ya = new hn();
4389
+ Ya.Client = hn;
4390
+ export {
4391
+ Ya as default
4392
+ };
4393
+ //# sourceMappingURL=bugcatch.es.js.map