@fogg/bug-reporter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/chunk-6TCI6T2U.cjs +45 -0
  4. package/dist/chunk-6TCI6T2U.cjs.map +1 -0
  5. package/dist/chunk-S2YRP4GT.js +22 -0
  6. package/dist/chunk-S2YRP4GT.js.map +1 -0
  7. package/dist/index.cjs +1963 -0
  8. package/dist/index.cjs.map +1 -0
  9. package/dist/index.d.cts +331 -0
  10. package/dist/index.d.ts +331 -0
  11. package/dist/index.js +1956 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/recording-ML63ZQ6A.cjs +120 -0
  14. package/dist/recording-ML63ZQ6A.cjs.map +1 -0
  15. package/dist/recording-YSR6IORT.js +118 -0
  16. package/dist/recording-YSR6IORT.js.map +1 -0
  17. package/dist/screenshot-F4W72WRK.js +176 -0
  18. package/dist/screenshot-F4W72WRK.js.map +1 -0
  19. package/dist/screenshot-FRAZAS6B.cjs +178 -0
  20. package/dist/screenshot-FRAZAS6B.cjs.map +1 -0
  21. package/dist/styles/index.css +495 -0
  22. package/dist/styles/index.css.map +1 -0
  23. package/dist/styles/index.d.cts +2 -0
  24. package/dist/styles/index.d.ts +2 -0
  25. package/docs/backend-local.md +16 -0
  26. package/docs/backend-s3.md +31 -0
  27. package/docs/browser-compatibility.md +8 -0
  28. package/docs/framework-cra.md +10 -0
  29. package/docs/framework-nextjs.md +16 -0
  30. package/docs/framework-remix.md +6 -0
  31. package/docs/framework-vite.md +21 -0
  32. package/docs/known-limitations.md +6 -0
  33. package/docs/quickstart.md +26 -0
  34. package/docs/security.md +9 -0
  35. package/examples/backend-local/README.md +11 -0
  36. package/examples/backend-local/package.json +13 -0
  37. package/examples/backend-local/src/server.mjs +31 -0
  38. package/examples/backend-s3-presign/README.md +14 -0
  39. package/examples/backend-s3-presign/package.json +13 -0
  40. package/examples/backend-s3-presign/src/server.mjs +53 -0
  41. package/examples/sandbox-vite/README.md +25 -0
  42. package/examples/sandbox-vite/index.html +12 -0
  43. package/examples/sandbox-vite/package-lock.json +1880 -0
  44. package/examples/sandbox-vite/package.json +24 -0
  45. package/examples/sandbox-vite/src/App.tsx +200 -0
  46. package/examples/sandbox-vite/src/main.tsx +10 -0
  47. package/examples/sandbox-vite/src/sandbox.css +74 -0
  48. package/examples/sandbox-vite/tsconfig.json +14 -0
  49. package/examples/sandbox-vite/vite.config.ts +9 -0
  50. package/package.json +93 -0
package/dist/index.js ADDED
@@ -0,0 +1,1956 @@
1
+ import { React, BugReporterError, __publicField } from './chunk-S2YRP4GT.js';
2
+ export { BugReporterError } from './chunk-S2YRP4GT.js';
3
+ import { createContext, forwardRef, useRef, useState, useCallback, useEffect, useImperativeHandle, useMemo, useContext } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+
6
+ // src/diagnostics/ua.ts
7
+ function detectBrowserAndOS(userAgent) {
8
+ let browser = "Unknown";
9
+ let os = "Unknown";
10
+ if (/Edg\//.test(userAgent)) browser = "Edge";
11
+ else if (/Chrome\//.test(userAgent)) browser = "Chrome";
12
+ else if (/Firefox\//.test(userAgent)) browser = "Firefox";
13
+ else if (/Safari\//.test(userAgent) && !/Chrome\//.test(userAgent)) browser = "Safari";
14
+ if (/Windows NT/.test(userAgent)) os = "Windows";
15
+ else if (/Mac OS X/.test(userAgent)) os = "macOS";
16
+ else if (/Android/.test(userAgent)) os = "Android";
17
+ else if (/iPhone|iPad|iPod/.test(userAgent)) os = "iOS";
18
+ else if (/Linux/.test(userAgent)) os = "Linux";
19
+ return { browser, os };
20
+ }
21
+
22
+ // src/diagnostics/collect.ts
23
+ function getUserAgentDataSnapshot() {
24
+ const userAgentData = navigator.userAgentData;
25
+ if (!userAgentData) {
26
+ return void 0;
27
+ }
28
+ return {
29
+ brands: userAgentData.brands?.map((item) => ({
30
+ brand: item.brand,
31
+ version: item.version
32
+ })),
33
+ mobile: userAgentData.mobile,
34
+ platform: userAgentData.platform
35
+ };
36
+ }
37
+ function collectDiagnostics(config, options) {
38
+ const nav = performance.getEntriesByType("navigation")[0];
39
+ const { browser, os } = detectBrowserAndOS(navigator.userAgent);
40
+ return {
41
+ url: window.location.href,
42
+ referrer: document.referrer,
43
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
44
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
45
+ viewport: {
46
+ width: window.innerWidth,
47
+ height: window.innerHeight,
48
+ pixelRatio: window.devicePixelRatio || 1
49
+ },
50
+ browser,
51
+ os,
52
+ language: navigator.language,
53
+ userAgent: navigator.userAgent,
54
+ userAgentData: getUserAgentDataSnapshot(),
55
+ appVersion: config.appVersion,
56
+ environment: config.environment,
57
+ projectId: config.projectId,
58
+ logs: options?.logs,
59
+ requests: options?.requests,
60
+ navigationTiming: {
61
+ domComplete: nav?.domComplete,
62
+ loadEventEnd: nav?.loadEventEnd,
63
+ responseEnd: nav?.responseEnd
64
+ }
65
+ };
66
+ }
67
+
68
+ // src/diagnostics/console-buffer.ts
69
+ var ConsoleBuffer = class {
70
+ constructor(maxEntries) {
71
+ this.maxEntries = maxEntries;
72
+ __publicField(this, "entries", []);
73
+ __publicField(this, "originals", /* @__PURE__ */ new Map());
74
+ __publicField(this, "installed", false);
75
+ __publicField(this, "onWindowError", (event) => {
76
+ this.push("error", [event.message, event.error instanceof Error ? event.error.stack ?? "" : ""]);
77
+ });
78
+ __publicField(this, "onUnhandledRejection", (event) => {
79
+ this.push("error", ["Unhandled promise rejection", event.reason]);
80
+ });
81
+ }
82
+ install() {
83
+ if (this.installed) {
84
+ return;
85
+ }
86
+ const levels = ["log", "info", "warn", "error"];
87
+ levels.forEach((level) => {
88
+ const original = console[level];
89
+ this.originals.set(level, original.bind(console));
90
+ console[level] = ((...args) => {
91
+ this.push(level, args);
92
+ original(...args);
93
+ });
94
+ });
95
+ if (typeof window !== "undefined") {
96
+ window.addEventListener("error", this.onWindowError);
97
+ window.addEventListener("unhandledrejection", this.onUnhandledRejection);
98
+ }
99
+ this.installed = true;
100
+ }
101
+ uninstall() {
102
+ if (!this.installed) {
103
+ return;
104
+ }
105
+ this.originals.forEach((original, level) => {
106
+ console[level] = original;
107
+ });
108
+ this.originals.clear();
109
+ if (typeof window !== "undefined") {
110
+ window.removeEventListener("error", this.onWindowError);
111
+ window.removeEventListener("unhandledrejection", this.onUnhandledRejection);
112
+ }
113
+ this.installed = false;
114
+ }
115
+ clear() {
116
+ this.entries.length = 0;
117
+ }
118
+ snapshot() {
119
+ return [...this.entries];
120
+ }
121
+ push(level, args) {
122
+ const message = args.map((arg) => {
123
+ if (typeof arg === "string") {
124
+ return arg;
125
+ }
126
+ try {
127
+ return JSON.stringify(arg);
128
+ } catch {
129
+ return String(arg);
130
+ }
131
+ }).join(" ");
132
+ this.entries.push({
133
+ level,
134
+ message,
135
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
136
+ });
137
+ if (this.entries.length > this.maxEntries) {
138
+ this.entries.splice(0, this.entries.length - this.maxEntries);
139
+ }
140
+ }
141
+ };
142
+
143
+ // src/diagnostics/network-buffer.ts
144
+ var xhrMeta = /* @__PURE__ */ new WeakMap();
145
+ function extractUrl(input) {
146
+ if (typeof input === "string") {
147
+ return input;
148
+ }
149
+ if (input instanceof URL) {
150
+ return input.toString();
151
+ }
152
+ return input.url;
153
+ }
154
+ function extractMethod(input, init) {
155
+ if (init?.method) {
156
+ return String(init.method).toUpperCase();
157
+ }
158
+ if (typeof Request !== "undefined" && input instanceof Request) {
159
+ return input.method.toUpperCase();
160
+ }
161
+ return "GET";
162
+ }
163
+ var NetworkBuffer = class {
164
+ constructor(maxEntries) {
165
+ this.maxEntries = maxEntries;
166
+ __publicField(this, "entries", []);
167
+ __publicField(this, "installed", false);
168
+ __publicField(this, "originalFetch");
169
+ __publicField(this, "originalXhrOpen");
170
+ __publicField(this, "originalXhrSend");
171
+ }
172
+ install() {
173
+ if (this.installed) {
174
+ return;
175
+ }
176
+ this.installFetch();
177
+ this.installXhr();
178
+ this.installed = true;
179
+ }
180
+ uninstall() {
181
+ if (!this.installed) {
182
+ return;
183
+ }
184
+ if (this.originalFetch) {
185
+ window.fetch = this.originalFetch;
186
+ }
187
+ if (this.originalXhrOpen) {
188
+ XMLHttpRequest.prototype.open = this.originalXhrOpen;
189
+ }
190
+ if (this.originalXhrSend) {
191
+ XMLHttpRequest.prototype.send = this.originalXhrSend;
192
+ }
193
+ this.installed = false;
194
+ }
195
+ clear() {
196
+ this.entries.length = 0;
197
+ }
198
+ snapshot() {
199
+ return [...this.entries];
200
+ }
201
+ installFetch() {
202
+ if (typeof window === "undefined" || typeof window.fetch !== "function") {
203
+ return;
204
+ }
205
+ this.originalFetch = window.fetch.bind(window);
206
+ const originalFetch = this.originalFetch;
207
+ window.fetch = async (input, init) => {
208
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
209
+ const startedAt = performance.now();
210
+ const method = extractMethod(input, init);
211
+ const url = extractUrl(input);
212
+ try {
213
+ const response = await originalFetch(input, init);
214
+ this.push({
215
+ transport: "fetch",
216
+ method,
217
+ url,
218
+ status: response.status,
219
+ ok: response.ok,
220
+ durationMs: Math.round(performance.now() - startedAt),
221
+ timestamp
222
+ });
223
+ return response;
224
+ } catch (error) {
225
+ this.push({
226
+ transport: "fetch",
227
+ method,
228
+ url,
229
+ durationMs: Math.round(performance.now() - startedAt),
230
+ timestamp,
231
+ error: error instanceof Error ? error.message : String(error)
232
+ });
233
+ throw error;
234
+ }
235
+ };
236
+ }
237
+ installXhr() {
238
+ if (typeof XMLHttpRequest === "undefined") {
239
+ return;
240
+ }
241
+ this.originalXhrOpen = XMLHttpRequest.prototype.open;
242
+ this.originalXhrSend = XMLHttpRequest.prototype.send;
243
+ const buffer = this;
244
+ XMLHttpRequest.prototype.open = function patchedOpen(method, url, async, username, password) {
245
+ xhrMeta.set(this, {
246
+ method: String(method || "GET").toUpperCase(),
247
+ url: String(url),
248
+ startedAt: 0,
249
+ timestamp: ""
250
+ });
251
+ buffer.originalXhrOpen?.call(this, method, url, async ?? true, username ?? null, password ?? null);
252
+ };
253
+ XMLHttpRequest.prototype.send = function patchedSend(body) {
254
+ const existing = xhrMeta.get(this);
255
+ if (existing) {
256
+ existing.startedAt = performance.now();
257
+ existing.timestamp = (/* @__PURE__ */ new Date()).toISOString();
258
+ xhrMeta.set(this, existing);
259
+ const onLoadEnd = () => {
260
+ const meta = xhrMeta.get(this);
261
+ if (!meta) {
262
+ return;
263
+ }
264
+ buffer.push({
265
+ transport: "xhr",
266
+ method: meta.method,
267
+ url: meta.url,
268
+ status: this.status,
269
+ ok: this.status >= 200 && this.status < 300,
270
+ durationMs: Math.round(performance.now() - meta.startedAt),
271
+ timestamp: meta.timestamp
272
+ });
273
+ xhrMeta.delete(this);
274
+ this.removeEventListener("loadend", onLoadEnd);
275
+ this.removeEventListener("error", onError);
276
+ };
277
+ const onError = () => {
278
+ const meta = xhrMeta.get(this);
279
+ if (!meta) {
280
+ return;
281
+ }
282
+ buffer.push({
283
+ transport: "xhr",
284
+ method: meta.method,
285
+ url: meta.url,
286
+ status: this.status || void 0,
287
+ ok: false,
288
+ durationMs: Math.round(performance.now() - meta.startedAt),
289
+ timestamp: meta.timestamp,
290
+ error: "XMLHttpRequest failed"
291
+ });
292
+ xhrMeta.delete(this);
293
+ this.removeEventListener("loadend", onLoadEnd);
294
+ this.removeEventListener("error", onError);
295
+ };
296
+ this.addEventListener("loadend", onLoadEnd);
297
+ this.addEventListener("error", onError);
298
+ }
299
+ buffer.originalXhrSend?.call(this, body);
300
+ };
301
+ }
302
+ push(entry) {
303
+ this.entries.push(entry);
304
+ if (this.entries.length > this.maxEntries) {
305
+ this.entries.splice(0, this.entries.length - this.maxEntries);
306
+ }
307
+ }
308
+ };
309
+ var BugReporterContext = createContext(void 0);
310
+
311
+ // src/core/defaults.ts
312
+ var DEFAULT_MASK_SELECTORS = [
313
+ "input[type='password']",
314
+ "[data-bug-reporter-mask='true']"
315
+ ];
316
+ function withDefaults(config) {
317
+ return {
318
+ apiEndpoint: config.apiEndpoint,
319
+ projectId: config.projectId,
320
+ appVersion: config.appVersion,
321
+ environment: config.environment,
322
+ storage: {
323
+ mode: config.storage?.mode ?? "proxy",
324
+ s3: config.storage?.s3,
325
+ local: config.storage?.local,
326
+ proxy: config.storage?.proxy,
327
+ limits: {
328
+ maxVideoSeconds: config.storage?.limits?.maxVideoSeconds ?? 30,
329
+ maxVideoBytes: config.storage?.limits?.maxVideoBytes ?? 50 * 1024 * 1024,
330
+ maxScreenshotBytes: config.storage?.limits?.maxScreenshotBytes ?? 8 * 1024 * 1024
331
+ }
332
+ },
333
+ auth: {
334
+ headers: config.auth?.headers ?? {},
335
+ withCredentials: config.auth?.withCredentials ?? false
336
+ },
337
+ theme: {
338
+ primaryColor: config.theme?.primaryColor ?? "#1b74e4",
339
+ position: config.theme?.position ?? "bottom-right",
340
+ zIndex: config.theme?.zIndex ?? 2147483e3,
341
+ borderRadius: config.theme?.borderRadius ?? "999px"
342
+ },
343
+ features: {
344
+ screenshot: config.features?.screenshot ?? true,
345
+ recording: config.features?.recording ?? true,
346
+ annotations: config.features?.annotations ?? true,
347
+ consoleLogs: config.features?.consoleLogs ?? false,
348
+ networkInfo: config.features?.networkInfo ?? false
349
+ },
350
+ user: config.user,
351
+ attributes: config.attributes ?? {},
352
+ privacy: {
353
+ maskSelectors: config.privacy?.maskSelectors ?? DEFAULT_MASK_SELECTORS,
354
+ redactTextPatterns: config.privacy?.redactTextPatterns ?? []
355
+ },
356
+ diagnostics: {
357
+ consoleBufferSize: config.diagnostics?.consoleBufferSize ?? 100,
358
+ requestBufferSize: config.diagnostics?.requestBufferSize ?? 200
359
+ },
360
+ hooks: {
361
+ beforeSubmit: config.hooks?.beforeSubmit,
362
+ onSuccess: config.hooks?.onSuccess,
363
+ onError: config.hooks?.onError
364
+ }
365
+ };
366
+ }
367
+
368
+ // src/storage/local-public.ts
369
+ var LocalPublicProvider = class {
370
+ constructor(options) {
371
+ this.options = options;
372
+ }
373
+ async prepareUploads(files) {
374
+ return files.map((file) => ({
375
+ id: file.id,
376
+ method: "POST",
377
+ uploadUrl: this.options.uploadEndpoint,
378
+ headers: this.options.authHeaders,
379
+ type: file.type
380
+ }));
381
+ }
382
+ async upload(instruction, blob, onProgress) {
383
+ onProgress?.(0);
384
+ const form = new FormData();
385
+ form.append("file", blob, instruction.id);
386
+ form.append("id", instruction.id);
387
+ form.append("type", instruction.type);
388
+ const response = await fetch(instruction.uploadUrl, {
389
+ method: "POST",
390
+ headers: instruction.headers,
391
+ credentials: this.options.withCredentials ? "include" : "same-origin",
392
+ body: form
393
+ });
394
+ if (!response.ok) {
395
+ throw new BugReporterError("UPLOAD_ERROR", `Local upload failed (${response.status}).`);
396
+ }
397
+ const payload = await response.json();
398
+ onProgress?.(1);
399
+ return {
400
+ id: instruction.id,
401
+ type: instruction.type,
402
+ url: payload.url || (payload.key && this.options.publicBaseUrl ? `${this.options.publicBaseUrl.replace(/\/$/, "")}/${payload.key}` : instruction.uploadUrl),
403
+ key: payload.key,
404
+ mimeType: blob.type,
405
+ size: blob.size
406
+ };
407
+ }
408
+ };
409
+
410
+ // src/storage/proxy.ts
411
+ var ProxyProvider = class {
412
+ constructor(options) {
413
+ this.options = options;
414
+ }
415
+ async prepareUploads(files) {
416
+ return files.map((file) => ({
417
+ id: file.id,
418
+ method: "POST",
419
+ uploadUrl: this.options.uploadEndpoint,
420
+ headers: this.options.authHeaders,
421
+ type: file.type
422
+ }));
423
+ }
424
+ async upload(instruction, blob, onProgress) {
425
+ onProgress?.(0);
426
+ const response = await fetch(instruction.uploadUrl, {
427
+ method: "POST",
428
+ headers: {
429
+ "content-type": blob.type || "application/octet-stream",
430
+ "x-bug-reporter-asset-id": instruction.id,
431
+ "x-bug-reporter-asset-type": instruction.type,
432
+ ...instruction.headers
433
+ },
434
+ credentials: this.options.withCredentials ? "include" : "same-origin",
435
+ body: blob
436
+ });
437
+ if (!response.ok) {
438
+ throw new BugReporterError("UPLOAD_ERROR", `Proxy upload failed (${response.status}).`);
439
+ }
440
+ const payload = await response.json();
441
+ onProgress?.(1);
442
+ return {
443
+ id: instruction.id,
444
+ type: instruction.type,
445
+ url: payload.url,
446
+ key: payload.key,
447
+ mimeType: blob.type,
448
+ size: blob.size
449
+ };
450
+ }
451
+ };
452
+
453
+ // src/storage/s3-presigned.ts
454
+ var S3PresignedProvider = class {
455
+ constructor(options) {
456
+ this.options = options;
457
+ }
458
+ async prepareUploads(files) {
459
+ const response = await fetch(this.options.presignEndpoint, {
460
+ method: "POST",
461
+ headers: {
462
+ "content-type": "application/json",
463
+ ...this.options.authHeaders
464
+ },
465
+ credentials: this.options.withCredentials ? "include" : "same-origin",
466
+ body: JSON.stringify({ files })
467
+ });
468
+ if (!response.ok) {
469
+ throw new BugReporterError("UPLOAD_ERROR", `Failed to prepare uploads (${response.status}).`);
470
+ }
471
+ const payload = await response.json();
472
+ if (!payload.uploads?.length) {
473
+ throw new BugReporterError("UPLOAD_ERROR", "Presign endpoint did not return upload instructions.");
474
+ }
475
+ return payload.uploads;
476
+ }
477
+ async upload(instruction, blob, onProgress) {
478
+ onProgress?.(0);
479
+ if (instruction.method === "POST" && instruction.fields) {
480
+ const formData = new FormData();
481
+ Object.entries(instruction.fields).forEach(([key, value]) => formData.append(key, value));
482
+ formData.append("file", blob);
483
+ const response = await fetch(instruction.uploadUrl, {
484
+ method: "POST",
485
+ body: formData
486
+ });
487
+ if (!response.ok) {
488
+ throw new BugReporterError("UPLOAD_ERROR", `S3 form upload failed (${response.status}).`);
489
+ }
490
+ } else {
491
+ const response = await fetch(instruction.uploadUrl, {
492
+ method: instruction.method,
493
+ headers: instruction.headers,
494
+ body: blob
495
+ });
496
+ if (!response.ok) {
497
+ throw new BugReporterError("UPLOAD_ERROR", `S3 upload failed (${response.status}).`);
498
+ }
499
+ }
500
+ onProgress?.(1);
501
+ const publicUrl = instruction.publicUrl ?? (this.options.publicBaseUrl && instruction.key ? `${this.options.publicBaseUrl.replace(/\/$/, "")}/${instruction.key}` : instruction.uploadUrl);
502
+ return {
503
+ id: instruction.id,
504
+ type: instruction.type,
505
+ key: instruction.key,
506
+ url: publicUrl,
507
+ mimeType: blob.type,
508
+ size: blob.size
509
+ };
510
+ }
511
+ };
512
+
513
+ // src/storage/factory.ts
514
+ function createStorageProvider(config) {
515
+ if (config.storage.mode === "s3-presigned") {
516
+ const presignEndpoint = config.storage.s3?.presignEndpoint;
517
+ if (!presignEndpoint) {
518
+ throw new Error("storage.s3.presignEndpoint is required for s3-presigned mode.");
519
+ }
520
+ return new S3PresignedProvider({
521
+ presignEndpoint,
522
+ publicBaseUrl: config.storage.s3?.publicBaseUrl,
523
+ authHeaders: config.auth.headers,
524
+ withCredentials: config.auth.withCredentials
525
+ });
526
+ }
527
+ if (config.storage.mode === "local-public") {
528
+ const uploadEndpoint2 = config.storage.local?.uploadEndpoint;
529
+ if (!uploadEndpoint2) {
530
+ throw new Error("storage.local.uploadEndpoint is required for local-public mode.");
531
+ }
532
+ return new LocalPublicProvider({
533
+ uploadEndpoint: uploadEndpoint2,
534
+ publicBaseUrl: config.storage.local?.publicBaseUrl,
535
+ authHeaders: config.auth.headers,
536
+ withCredentials: config.auth.withCredentials
537
+ });
538
+ }
539
+ const uploadEndpoint = config.storage.proxy?.uploadEndpoint;
540
+ if (!uploadEndpoint) {
541
+ throw new Error("storage.proxy.uploadEndpoint is required for proxy mode.");
542
+ }
543
+ return new ProxyProvider({
544
+ uploadEndpoint,
545
+ authHeaders: config.auth.headers,
546
+ withCredentials: config.auth.withCredentials
547
+ });
548
+ }
549
+
550
+ // src/core/utils.ts
551
+ function uid(prefix = "br") {
552
+ return `${prefix}_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
553
+ }
554
+ function blobToObjectUrl(blob) {
555
+ return URL.createObjectURL(blob);
556
+ }
557
+ function revokeObjectUrl(url) {
558
+ if (!url) {
559
+ return;
560
+ }
561
+ URL.revokeObjectURL(url);
562
+ }
563
+ async function sleep(ms) {
564
+ await new Promise((resolve) => setTimeout(resolve, ms));
565
+ }
566
+
567
+ // src/core/upload.ts
568
+ async function withRetry(fn, retries) {
569
+ let lastError;
570
+ for (let attempt = 0; attempt <= retries; attempt += 1) {
571
+ try {
572
+ return await fn();
573
+ } catch (error) {
574
+ lastError = error;
575
+ if (attempt < retries) {
576
+ await sleep(300 * (attempt + 1));
577
+ }
578
+ }
579
+ }
580
+ throw lastError;
581
+ }
582
+ async function uploadAssets(options) {
583
+ const files = options.assets.map((asset) => ({
584
+ id: asset.id,
585
+ name: asset.filename,
586
+ type: asset.type,
587
+ mimeType: asset.mimeType,
588
+ size: asset.size
589
+ }));
590
+ const instructions = await options.provider.prepareUploads(files);
591
+ const byId = new Map(instructions.map((instruction) => [instruction.id, instruction]));
592
+ const refs = [];
593
+ let completed = 0;
594
+ for (const asset of options.assets) {
595
+ const instruction = byId.get(asset.id);
596
+ if (!instruction) {
597
+ throw new BugReporterError("UPLOAD_ERROR", `No upload instruction for asset ${asset.id}.`);
598
+ }
599
+ const ref = await withRetry(
600
+ () => options.provider.upload(instruction, asset.blob, (inner) => {
601
+ const aggregate = (completed + inner) / options.assets.length;
602
+ options.onProgress?.(aggregate);
603
+ }),
604
+ options.retries ?? 2
605
+ );
606
+ refs.push(ref);
607
+ completed += 1;
608
+ options.onProgress?.(completed / options.assets.length);
609
+ }
610
+ return refs;
611
+ }
612
+
613
+ // src/core/submit.ts
614
+ async function submitReport(options) {
615
+ const provider = createStorageProvider(options.config);
616
+ const assetReferences = await uploadAssets({
617
+ provider,
618
+ assets: options.assets,
619
+ retries: 2,
620
+ onProgress: options.onUploadProgress
621
+ });
622
+ const payloadBase = {
623
+ issue: {
624
+ title: options.draft.title,
625
+ description: options.draft.description,
626
+ projectId: options.config.projectId,
627
+ environment: options.config.environment,
628
+ appVersion: options.config.appVersion,
629
+ assets: assetReferences
630
+ },
631
+ context: {
632
+ url: options.diagnostics.url,
633
+ referrer: options.diagnostics.referrer,
634
+ timestamp: options.diagnostics.timestamp,
635
+ timezone: options.diagnostics.timezone,
636
+ viewport: options.diagnostics.viewport,
637
+ client: {
638
+ browser: options.diagnostics.browser,
639
+ os: options.diagnostics.os,
640
+ language: options.diagnostics.language,
641
+ userAgent: options.diagnostics.userAgent
642
+ },
643
+ userAgentData: options.diagnostics.userAgentData,
644
+ performance: {
645
+ navigationTiming: options.diagnostics.navigationTiming
646
+ },
647
+ logs: options.diagnostics.logs,
648
+ requests: options.diagnostics.requests
649
+ },
650
+ reporter: {
651
+ id: options.config.user?.id,
652
+ name: options.config.user?.name,
653
+ email: options.config.user?.email,
654
+ role: options.config.user?.role,
655
+ ip: options.config.user?.ip,
656
+ anonymous: options.config.user?.anonymous ?? !(options.config.user?.id || options.config.user?.email || options.config.user?.name)
657
+ },
658
+ attributes: options.attributes
659
+ };
660
+ const transformed = options.config.hooks.beforeSubmit ? await options.config.hooks.beforeSubmit(payloadBase) : payloadBase;
661
+ if (!transformed) {
662
+ throw new BugReporterError("ABORTED", "Submission aborted by beforeSubmit hook.");
663
+ }
664
+ console.log("[bug-reporter] payload to submit", transformed);
665
+ console.log("[bug-reporter] payload to submit (json)", JSON.stringify(transformed, null, 2));
666
+ const response = await fetch(options.config.apiEndpoint, {
667
+ method: "POST",
668
+ headers: {
669
+ "content-type": "application/json",
670
+ ...options.config.auth.headers
671
+ },
672
+ credentials: options.config.auth.withCredentials ? "include" : "same-origin",
673
+ body: JSON.stringify(transformed)
674
+ });
675
+ if (!response.ok) {
676
+ const body = await response.text().catch(() => "");
677
+ throw new BugReporterError("SUBMIT_ERROR", `Report submit failed (${response.status}): ${body || response.statusText}`);
678
+ }
679
+ return await response.json();
680
+ }
681
+
682
+ // src/components/BugReporterProvider.tsx
683
+ var EMPTY_DRAFT = {
684
+ title: "",
685
+ description: "",
686
+ stepsToReproduce: "",
687
+ expectedBehavior: "",
688
+ actualBehavior: ""
689
+ };
690
+ var BASE_STATE = {
691
+ isOpen: false,
692
+ dockSide: "right",
693
+ step: "describe",
694
+ draft: EMPTY_DRAFT,
695
+ attributes: {},
696
+ assets: [],
697
+ uploadProgress: 0,
698
+ isSubmitting: false,
699
+ error: void 0
700
+ };
701
+ function BugReporterProvider({ config, children }) {
702
+ const resolvedConfig = useMemo(() => withDefaults(config), [config]);
703
+ const initialDockSide = resolvedConfig.theme.position === "bottom-left" || resolvedConfig.theme.position === "top-left" ? "left" : "right";
704
+ const [state, setState] = useState(() => ({
705
+ ...BASE_STATE,
706
+ dockSide: initialDockSide,
707
+ attributes: { ...resolvedConfig.attributes }
708
+ }));
709
+ const [sessionActive, setSessionActive] = useState(false);
710
+ const consoleBufferRef = useRef(null);
711
+ const networkBufferRef = useRef(null);
712
+ const assetsRef = useRef([]);
713
+ const resetAssets = useCallback((assets) => {
714
+ assets.forEach((asset) => revokeObjectUrl(asset.previewUrl));
715
+ }, []);
716
+ const open = useCallback(() => {
717
+ setSessionActive(true);
718
+ setState((prev) => ({
719
+ ...prev,
720
+ isOpen: true,
721
+ error: void 0
722
+ }));
723
+ }, []);
724
+ const close = useCallback(() => {
725
+ setState((prev) => ({
726
+ ...prev,
727
+ isOpen: false
728
+ }));
729
+ }, []);
730
+ const reset = useCallback(() => {
731
+ setSessionActive(false);
732
+ setState((prev) => {
733
+ resetAssets(prev.assets);
734
+ return {
735
+ ...BASE_STATE,
736
+ dockSide: prev.dockSide,
737
+ attributes: { ...resolvedConfig.attributes },
738
+ isOpen: prev.isOpen
739
+ };
740
+ });
741
+ consoleBufferRef.current?.clear();
742
+ networkBufferRef.current?.clear();
743
+ }, [resetAssets, resolvedConfig.attributes]);
744
+ const setStep = useCallback((step) => {
745
+ setState((prev) => ({ ...prev, step }));
746
+ }, []);
747
+ const setDockSide = useCallback((dockSide) => {
748
+ setState((prev) => ({ ...prev, dockSide }));
749
+ }, []);
750
+ const updateDraft = useCallback((next) => {
751
+ setState((prev) => ({
752
+ ...prev,
753
+ draft: {
754
+ ...prev.draft,
755
+ ...next
756
+ }
757
+ }));
758
+ }, []);
759
+ const setAttributes = useCallback((next) => {
760
+ setState((prev) => ({
761
+ ...prev,
762
+ attributes: next
763
+ }));
764
+ }, []);
765
+ const updateAttribute = useCallback((key, value2) => {
766
+ setState((prev) => ({
767
+ ...prev,
768
+ attributes: {
769
+ ...prev.attributes,
770
+ [key]: value2
771
+ }
772
+ }));
773
+ }, []);
774
+ const setAssetByType = useCallback((type, next) => {
775
+ setState((prev) => {
776
+ const previous = prev.assets.find((asset) => asset.type === type);
777
+ if (previous) {
778
+ revokeObjectUrl(previous.previewUrl);
779
+ }
780
+ const rest = prev.assets.filter((asset) => asset.type !== type);
781
+ return {
782
+ ...prev,
783
+ assets: next ? [...rest, next] : rest
784
+ };
785
+ });
786
+ }, []);
787
+ const setScreenshot = useCallback((asset) => {
788
+ setAssetByType("screenshot", asset);
789
+ }, [setAssetByType]);
790
+ const setRecording = useCallback((asset) => {
791
+ setAssetByType("recording", asset);
792
+ }, [setAssetByType]);
793
+ const submit = useCallback(async () => {
794
+ setState((prev) => ({
795
+ ...prev,
796
+ step: "submitting",
797
+ isSubmitting: true,
798
+ uploadProgress: 0,
799
+ error: void 0
800
+ }));
801
+ try {
802
+ const logs = resolvedConfig.features.consoleLogs ? consoleBufferRef.current?.snapshot() : void 0;
803
+ const requests = resolvedConfig.features.networkInfo ? networkBufferRef.current?.snapshot() : void 0;
804
+ const diagnostics = collectDiagnostics(resolvedConfig, {
805
+ logs,
806
+ requests
807
+ });
808
+ const response = await submitReport({
809
+ config: resolvedConfig,
810
+ draft: state.draft,
811
+ attributes: state.attributes,
812
+ diagnostics,
813
+ assets: state.assets,
814
+ onUploadProgress: (progress) => {
815
+ setState((prev) => ({ ...prev, uploadProgress: progress }));
816
+ }
817
+ });
818
+ setState((prev) => ({
819
+ ...prev,
820
+ diagnostics,
821
+ isSubmitting: false,
822
+ uploadProgress: 1,
823
+ step: "success"
824
+ }));
825
+ resolvedConfig.hooks.onSuccess?.(response);
826
+ } catch (error) {
827
+ const message = error instanceof Error ? error.message : "Unexpected submit failure.";
828
+ setState((prev) => ({
829
+ ...prev,
830
+ isSubmitting: false,
831
+ step: "review",
832
+ error: message
833
+ }));
834
+ resolvedConfig.hooks.onError?.(error);
835
+ }
836
+ }, [resolvedConfig, state.assets, state.attributes, state.draft]);
837
+ const retrySubmit = useCallback(async () => {
838
+ await submit();
839
+ }, [submit]);
840
+ const getDiagnosticsPreview = useCallback(() => {
841
+ const logs = resolvedConfig.features.consoleLogs ? consoleBufferRef.current?.snapshot() ?? [] : [];
842
+ const requests = resolvedConfig.features.networkInfo ? networkBufferRef.current?.snapshot() ?? [] : [];
843
+ return {
844
+ errorLogs: logs.filter((entry) => entry.level === "error"),
845
+ failedRequests: requests.filter((request) => Boolean(request.error) || request.ok === false || (request.status ?? 0) >= 400)
846
+ };
847
+ }, [resolvedConfig.features.consoleLogs, resolvedConfig.features.networkInfo]);
848
+ useEffect(() => {
849
+ if (!sessionActive) {
850
+ return;
851
+ }
852
+ let consoleBuffer = null;
853
+ let networkBuffer = null;
854
+ if (resolvedConfig.features.consoleLogs) {
855
+ consoleBuffer = new ConsoleBuffer(resolvedConfig.diagnostics.consoleBufferSize);
856
+ consoleBuffer.install();
857
+ consoleBufferRef.current = consoleBuffer;
858
+ }
859
+ if (resolvedConfig.features.networkInfo) {
860
+ networkBuffer = new NetworkBuffer(resolvedConfig.diagnostics.requestBufferSize);
861
+ networkBuffer.install();
862
+ networkBufferRef.current = networkBuffer;
863
+ }
864
+ return () => {
865
+ consoleBuffer?.uninstall();
866
+ networkBuffer?.uninstall();
867
+ consoleBufferRef.current = null;
868
+ networkBufferRef.current = null;
869
+ };
870
+ }, [
871
+ sessionActive,
872
+ resolvedConfig.diagnostics.consoleBufferSize,
873
+ resolvedConfig.diagnostics.requestBufferSize,
874
+ resolvedConfig.features.consoleLogs,
875
+ resolvedConfig.features.networkInfo
876
+ ]);
877
+ useEffect(() => {
878
+ assetsRef.current = state.assets;
879
+ }, [state.assets]);
880
+ useEffect(() => {
881
+ return () => {
882
+ resetAssets(assetsRef.current);
883
+ consoleBufferRef.current?.uninstall();
884
+ networkBufferRef.current?.uninstall();
885
+ };
886
+ }, [resetAssets]);
887
+ const value = useMemo(
888
+ () => ({
889
+ config: resolvedConfig,
890
+ state,
891
+ open,
892
+ close,
893
+ reset,
894
+ setDockSide,
895
+ setStep,
896
+ updateDraft,
897
+ setAttributes,
898
+ updateAttribute,
899
+ setScreenshot,
900
+ setRecording,
901
+ submit,
902
+ retrySubmit,
903
+ getDiagnosticsPreview
904
+ }),
905
+ [
906
+ resolvedConfig,
907
+ state,
908
+ open,
909
+ close,
910
+ reset,
911
+ setDockSide,
912
+ setStep,
913
+ updateDraft,
914
+ setAttributes,
915
+ updateAttribute,
916
+ setScreenshot,
917
+ setRecording,
918
+ submit,
919
+ retrySubmit,
920
+ getDiagnosticsPreview
921
+ ]
922
+ );
923
+ return /* @__PURE__ */ React.createElement(BugReporterContext.Provider, { value }, children);
924
+ }
925
+ function useBugReporter() {
926
+ const context = useContext(BugReporterContext);
927
+ if (!context) {
928
+ throw new Error("useBugReporter must be used inside BugReporterProvider.");
929
+ }
930
+ return context;
931
+ }
932
+ function getFocusable(node) {
933
+ return Array.from(
934
+ node.querySelectorAll(
935
+ "button, [href], input, select, textarea, [tabindex]:not([tabindex='-1'])"
936
+ )
937
+ ).filter((el) => !el.hasAttribute("disabled"));
938
+ }
939
+ function useFocusTrap(enabled, containerRef) {
940
+ useEffect(() => {
941
+ const container = containerRef.current;
942
+ if (!enabled || !container) {
943
+ return;
944
+ }
945
+ const focusable = getFocusable(container);
946
+ const first = focusable[0];
947
+ const last = focusable[focusable.length - 1];
948
+ first?.focus();
949
+ const onKeyDown = (event) => {
950
+ if (event.key !== "Tab") {
951
+ return;
952
+ }
953
+ if (!first || !last) {
954
+ event.preventDefault();
955
+ return;
956
+ }
957
+ if (event.shiftKey && document.activeElement === first) {
958
+ event.preventDefault();
959
+ last.focus();
960
+ } else if (!event.shiftKey && document.activeElement === last) {
961
+ event.preventDefault();
962
+ first.focus();
963
+ }
964
+ };
965
+ container.addEventListener("keydown", onKeyDown);
966
+ return () => {
967
+ container.removeEventListener("keydown", onKeyDown);
968
+ };
969
+ }, [enabled, containerRef]);
970
+ }
971
+
972
+ // src/styles/inline.ts
973
+ var FONT_FAMILY = '"IBM Plex Sans", "Segoe UI", sans-serif';
974
+ var THEME = {
975
+ dark: {
976
+ bg: "rgba(7, 10, 14, 0.84)",
977
+ text: "#e9eff8",
978
+ muted: "#a8b5c7",
979
+ border: "rgba(202, 219, 243, 0.22)",
980
+ fieldBg: "rgba(255, 255, 255, 0.03)",
981
+ panelBg: "rgba(5, 8, 12, 0.72)",
982
+ secondaryBg: "rgba(255, 255, 255, 0.05)",
983
+ secondaryBgAlt: "rgba(255, 255, 255, 0.03)",
984
+ surfaceBg: "rgba(255, 255, 255, 0.02)",
985
+ overlaySide: "linear-gradient(to left, rgba(0, 0, 0, 0.32), rgba(0, 0, 0, 0.08) 45%, transparent 72%)",
986
+ overlayTop: "linear-gradient(to bottom, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.1) 45%, transparent 72%)",
987
+ overlayBottom: "linear-gradient(to top, rgba(0, 0, 0, 0.34), rgba(0, 0, 0, 0.1) 45%, transparent 72%)",
988
+ launcherText: "#f8fbff",
989
+ launcherShadow: "0 10px 28px rgba(0, 0, 0, 0.35)"
990
+ },
991
+ light: {
992
+ bg: "rgba(248, 250, 252, 0.96)",
993
+ text: "#0f172a",
994
+ muted: "#475569",
995
+ border: "rgba(15, 23, 42, 0.18)",
996
+ fieldBg: "rgba(15, 23, 42, 0.04)",
997
+ panelBg: "rgba(241, 245, 249, 0.9)",
998
+ secondaryBg: "rgba(15, 23, 42, 0.06)",
999
+ secondaryBgAlt: "rgba(15, 23, 42, 0.06)",
1000
+ surfaceBg: "rgba(15, 23, 42, 0.03)",
1001
+ overlaySide: "linear-gradient(to left, rgba(15, 23, 42, 0.16), rgba(15, 23, 42, 0.06) 45%, transparent 72%)",
1002
+ overlayTop: "linear-gradient(to bottom, rgba(15, 23, 42, 0.16), rgba(15, 23, 42, 0.06) 45%, transparent 72%)",
1003
+ overlayBottom: "linear-gradient(to top, rgba(15, 23, 42, 0.16), rgba(15, 23, 42, 0.06) 45%, transparent 72%)",
1004
+ launcherText: "#ffffff",
1005
+ launcherShadow: "0 10px 24px rgba(15, 23, 42, 0.24)"
1006
+ }
1007
+ };
1008
+ function getLauncherStyle(options) {
1009
+ const { position, borderRadius, zIndex, buttonColor, themeMode } = options;
1010
+ const [vertical, horizontal] = position.split("-");
1011
+ const theme = THEME[themeMode];
1012
+ return {
1013
+ position: "fixed",
1014
+ [vertical]: "24px",
1015
+ [horizontal]: "24px",
1016
+ color: theme.launcherText,
1017
+ backgroundColor: buttonColor,
1018
+ border: 0,
1019
+ borderRadius,
1020
+ padding: "12px 18px",
1021
+ font: `600 14px ${FONT_FAMILY}`,
1022
+ cursor: "pointer",
1023
+ boxShadow: theme.launcherShadow,
1024
+ zIndex
1025
+ };
1026
+ }
1027
+ function getModalOverlayStyle(options) {
1028
+ const { dockSide, themeMode, zIndex } = options;
1029
+ const theme = THEME[themeMode];
1030
+ const base = {
1031
+ position: "fixed",
1032
+ inset: 0,
1033
+ display: "flex",
1034
+ pointerEvents: "none",
1035
+ zIndex,
1036
+ background: theme.overlaySide
1037
+ };
1038
+ if (dockSide === "left") {
1039
+ return { ...base, alignItems: "stretch", justifyContent: "flex-start" };
1040
+ }
1041
+ if (dockSide === "top") {
1042
+ return { ...base, alignItems: "flex-start", justifyContent: "stretch", background: theme.overlayTop };
1043
+ }
1044
+ if (dockSide === "bottom") {
1045
+ return { ...base, alignItems: "flex-end", justifyContent: "stretch", background: theme.overlayBottom };
1046
+ }
1047
+ return { ...base, alignItems: "stretch", justifyContent: "flex-end" };
1048
+ }
1049
+ function getModalStyle(options) {
1050
+ const { dockSide, themeMode, buttonColor } = options;
1051
+ const theme = THEME[themeMode];
1052
+ const base = {
1053
+ pointerEvents: "auto",
1054
+ width: "min(430px, 100vw)",
1055
+ height: "100vh",
1056
+ overflow: "auto",
1057
+ color: theme.text,
1058
+ background: theme.bg,
1059
+ fontFamily: FONT_FAMILY,
1060
+ backdropFilter: "blur(8px)",
1061
+ boxShadow: "-14px 0 40px rgba(0, 0, 0, 0.42)",
1062
+ borderLeft: "1px solid transparent",
1063
+ borderRight: "1px solid transparent",
1064
+ borderTop: "1px solid transparent",
1065
+ borderBottom: "1px solid transparent",
1066
+ ["--br-primary"]: buttonColor,
1067
+ ["--br-bg"]: theme.bg,
1068
+ ["--br-text"]: theme.text,
1069
+ ["--br-muted"]: theme.muted,
1070
+ ["--br-border"]: theme.border,
1071
+ ["--br-field-bg"]: theme.fieldBg,
1072
+ ["--br-panel-bg"]: theme.panelBg,
1073
+ ["--br-secondary-bg"]: theme.secondaryBg,
1074
+ ["--br-secondary-bg-alt"]: theme.secondaryBgAlt,
1075
+ ["--br-surface-bg"]: theme.surfaceBg,
1076
+ ["--br-danger"]: "#ef4444"
1077
+ };
1078
+ if (dockSide === "left") {
1079
+ return { ...base, borderRightColor: "var(--br-border)", boxShadow: "14px 0 40px rgba(0, 0, 0, 0.42)" };
1080
+ }
1081
+ if (dockSide === "top") {
1082
+ return {
1083
+ ...base,
1084
+ width: "100vw",
1085
+ maxHeight: "100vh",
1086
+ height: "min(460px, 100vh)",
1087
+ borderBottomColor: "var(--br-border)",
1088
+ boxShadow: "0 14px 40px rgba(0, 0, 0, 0.42)"
1089
+ };
1090
+ }
1091
+ if (dockSide === "bottom") {
1092
+ return {
1093
+ ...base,
1094
+ width: "100vw",
1095
+ maxHeight: "100vh",
1096
+ height: "min(460px, 100vh)",
1097
+ borderTopColor: "var(--br-border)",
1098
+ boxShadow: "0 -14px 40px rgba(0, 0, 0, 0.42)"
1099
+ };
1100
+ }
1101
+ return { ...base, borderLeftColor: "var(--br-border)" };
1102
+ }
1103
+ var inlineStyles = {
1104
+ modalHeader: {
1105
+ position: "sticky",
1106
+ top: 0,
1107
+ zIndex: 2,
1108
+ display: "flex",
1109
+ justifyContent: "space-between",
1110
+ alignItems: "center",
1111
+ padding: "14px 16px",
1112
+ borderBottom: "1px solid var(--br-border)",
1113
+ background: "var(--br-panel-bg)"
1114
+ },
1115
+ dockControls: {
1116
+ display: "flex",
1117
+ gap: "6px"
1118
+ },
1119
+ iconButton: {
1120
+ display: "inline-flex",
1121
+ alignItems: "center",
1122
+ justifyContent: "center",
1123
+ border: "1px solid var(--br-border)",
1124
+ borderRadius: "8px",
1125
+ background: "var(--br-secondary-bg-alt)",
1126
+ color: "var(--br-text)",
1127
+ width: "30px",
1128
+ height: "30px",
1129
+ padding: 0,
1130
+ cursor: "pointer"
1131
+ },
1132
+ iconSvg: {
1133
+ width: "14px",
1134
+ height: "14px",
1135
+ fill: "none",
1136
+ stroke: "currentColor",
1137
+ strokeWidth: 1.8,
1138
+ strokeLinecap: "round"
1139
+ },
1140
+ step: {
1141
+ padding: "16px"
1142
+ },
1143
+ h2: {
1144
+ margin: "0 0 8px"
1145
+ },
1146
+ h3: {
1147
+ margin: "0"
1148
+ },
1149
+ p: {
1150
+ margin: "0 0 16px",
1151
+ color: "var(--br-muted)"
1152
+ },
1153
+ field: {
1154
+ display: "flex",
1155
+ flexDirection: "column",
1156
+ gap: "8px",
1157
+ marginBottom: "14px",
1158
+ fontSize: "13px",
1159
+ fontWeight: 600
1160
+ },
1161
+ input: {
1162
+ border: "1px solid var(--br-border)",
1163
+ borderRadius: "10px",
1164
+ font: `400 14px ${FONT_FAMILY}`,
1165
+ color: "var(--br-text)",
1166
+ background: "var(--br-field-bg)",
1167
+ padding: "10px"
1168
+ },
1169
+ actions: {
1170
+ display: "flex",
1171
+ gap: "10px",
1172
+ flexWrap: "wrap",
1173
+ marginTop: "16px"
1174
+ },
1175
+ captureRow: {
1176
+ display: "flex",
1177
+ gap: "10px",
1178
+ flexWrap: "wrap",
1179
+ marginTop: "10px"
1180
+ },
1181
+ captureItem: {
1182
+ flex: "1 1 190px"
1183
+ },
1184
+ captureButton: {
1185
+ width: "100%",
1186
+ display: "inline-flex",
1187
+ alignItems: "center",
1188
+ justifyContent: "center",
1189
+ gap: "8px"
1190
+ },
1191
+ captureIcon: {
1192
+ width: "14px",
1193
+ height: "14px",
1194
+ fill: "none",
1195
+ stroke: "currentColor",
1196
+ strokeWidth: 1.7,
1197
+ strokeLinejoin: "round"
1198
+ },
1199
+ captureDone: {
1200
+ fontSize: "11px",
1201
+ color: "var(--br-text)",
1202
+ opacity: 0.7
1203
+ },
1204
+ captureNote: {
1205
+ margin: "10px 0 0",
1206
+ color: "var(--br-muted)",
1207
+ fontSize: "13px"
1208
+ },
1209
+ previewWrapper: {
1210
+ display: "grid",
1211
+ gap: "12px",
1212
+ marginTop: "14px",
1213
+ justifyItems: "center"
1214
+ },
1215
+ preview: {
1216
+ width: "100%",
1217
+ maxHeight: "240px",
1218
+ borderRadius: "10px",
1219
+ border: "1px solid var(--br-border)",
1220
+ objectFit: "contain",
1221
+ background: "var(--br-surface-bg)"
1222
+ },
1223
+ error: {
1224
+ color: "#fca5a5",
1225
+ fontSize: "14px",
1226
+ marginTop: "10px"
1227
+ },
1228
+ summary: {
1229
+ border: "1px solid var(--br-border)",
1230
+ borderRadius: "10px",
1231
+ padding: "12px",
1232
+ marginTop: "12px",
1233
+ background: "var(--br-surface-bg)"
1234
+ },
1235
+ assets: {
1236
+ display: "grid",
1237
+ gap: "12px",
1238
+ marginTop: "14px",
1239
+ gridTemplateColumns: "repeat(auto-fit, minmax(140px, 1fr))"
1240
+ },
1241
+ assetCard: {
1242
+ border: "1px solid var(--br-border)",
1243
+ borderRadius: "10px",
1244
+ padding: "10px",
1245
+ background: "var(--br-surface-bg)"
1246
+ },
1247
+ assetType: {
1248
+ fontSize: "12px",
1249
+ textTransform: "uppercase",
1250
+ color: "var(--br-muted)"
1251
+ },
1252
+ uploadProgress: {
1253
+ marginTop: "12px",
1254
+ fontSize: "14px"
1255
+ },
1256
+ progressTrack: {
1257
+ height: "8px",
1258
+ borderRadius: "999px",
1259
+ background: "rgba(148, 163, 184, 0.25)",
1260
+ overflow: "hidden",
1261
+ marginTop: "6px"
1262
+ },
1263
+ progressFill: {
1264
+ height: "100%",
1265
+ background: "var(--br-primary)"
1266
+ },
1267
+ annotation: {
1268
+ border: "1px solid var(--br-border)",
1269
+ borderRadius: "10px",
1270
+ padding: "10px",
1271
+ background: "var(--br-surface-bg)"
1272
+ },
1273
+ annotationTools: {
1274
+ display: "flex",
1275
+ gap: "8px",
1276
+ marginBottom: "10px",
1277
+ flexWrap: "wrap"
1278
+ },
1279
+ annotationCanvas: {
1280
+ width: "auto",
1281
+ maxWidth: "100%",
1282
+ height: "auto",
1283
+ maxHeight: "min(48vh, 360px)",
1284
+ display: "block",
1285
+ margin: "0 auto",
1286
+ border: "1px solid var(--br-border)",
1287
+ borderRadius: "8px"
1288
+ }
1289
+ };
1290
+ function getButtonStyle(variant, options) {
1291
+ const disabled = options?.disabled ?? false;
1292
+ const active = options?.active ?? false;
1293
+ const fullWidth = options?.fullWidth ?? false;
1294
+ const base = {
1295
+ border: "1px solid transparent",
1296
+ borderRadius: "10px",
1297
+ padding: "8px 12px",
1298
+ cursor: disabled ? "not-allowed" : "pointer",
1299
+ font: `600 13px ${FONT_FAMILY}`,
1300
+ opacity: disabled ? 0.6 : 1,
1301
+ width: fullWidth ? "100%" : void 0
1302
+ };
1303
+ if (variant === "primary") {
1304
+ return { ...base, background: "var(--br-primary)", color: "#fff" };
1305
+ }
1306
+ if (variant === "danger") {
1307
+ return { ...base, background: "var(--br-danger)", color: "#fff" };
1308
+ }
1309
+ return {
1310
+ ...base,
1311
+ background: active ? "var(--br-primary)" : "var(--br-secondary-bg)",
1312
+ borderColor: active ? "var(--br-primary)" : "var(--br-border)",
1313
+ color: active ? "#fff" : "var(--br-text)"
1314
+ };
1315
+ }
1316
+ function getDockButtonStyle(isActive) {
1317
+ if (!isActive) {
1318
+ return inlineStyles.iconButton;
1319
+ }
1320
+ return {
1321
+ ...inlineStyles.iconButton,
1322
+ borderColor: "var(--br-primary)",
1323
+ color: "#fff",
1324
+ background: "var(--br-primary)",
1325
+ boxShadow: "0 0 0 1px rgba(255, 255, 255, 0.35) inset"
1326
+ };
1327
+ }
1328
+
1329
+ // src/components/LauncherButton.tsx
1330
+ function LauncherButton({ position, text, themeMode = "dark", buttonColor }) {
1331
+ const {
1332
+ config,
1333
+ state: { isOpen },
1334
+ open
1335
+ } = useBugReporter();
1336
+ if (isOpen) {
1337
+ return null;
1338
+ }
1339
+ const resolvedPosition = position ?? config.theme.position ?? "bottom-right";
1340
+ const label = text ?? "Get Help";
1341
+ const resolvedButtonColor = buttonColor ?? config.theme.primaryColor;
1342
+ const launcherStyle = getLauncherStyle({
1343
+ position: resolvedPosition,
1344
+ borderRadius: config.theme.borderRadius,
1345
+ zIndex: config.theme.zIndex,
1346
+ buttonColor: resolvedButtonColor,
1347
+ themeMode
1348
+ });
1349
+ return /* @__PURE__ */ React.createElement(
1350
+ "button",
1351
+ {
1352
+ type: "button",
1353
+ style: launcherStyle,
1354
+ onClick: open,
1355
+ "aria-label": "Open bug reporter"
1356
+ },
1357
+ label
1358
+ );
1359
+ }
1360
+ function Modal({ isOpen, dockSide, themeMode, buttonColor, title, zIndex, onRequestClose, children }) {
1361
+ const dialogRef = useRef(null);
1362
+ useFocusTrap(isOpen, dialogRef);
1363
+ useEffect(() => {
1364
+ if (!isOpen) {
1365
+ return;
1366
+ }
1367
+ const onKeyDown = (event) => {
1368
+ if (event.key === "Escape") {
1369
+ event.preventDefault();
1370
+ onRequestClose();
1371
+ }
1372
+ };
1373
+ window.addEventListener("keydown", onKeyDown);
1374
+ return () => window.removeEventListener("keydown", onKeyDown);
1375
+ }, [isOpen, onRequestClose]);
1376
+ if (!isOpen || typeof document === "undefined") {
1377
+ return null;
1378
+ }
1379
+ const overlayStyle = getModalOverlayStyle({ dockSide, themeMode, zIndex });
1380
+ const modalStyle = getModalStyle({ dockSide, themeMode, buttonColor });
1381
+ return createPortal(
1382
+ /* @__PURE__ */ React.createElement("div", { style: overlayStyle }, /* @__PURE__ */ React.createElement("div", { ref: dialogRef, style: modalStyle, role: "dialog", "aria-modal": "true", "aria-label": title }, children)),
1383
+ document.body
1384
+ );
1385
+ }
1386
+
1387
+ // src/core/lazy.ts
1388
+ async function loadScreenshotCapture() {
1389
+ return import('./screenshot-F4W72WRK.js');
1390
+ }
1391
+ async function loadScreenRecording() {
1392
+ return import('./recording-YSR6IORT.js');
1393
+ }
1394
+
1395
+ // src/core/validation.ts
1396
+ function validateScreenshotSize(size, maxBytes) {
1397
+ if (size > maxBytes) {
1398
+ throw new BugReporterError(
1399
+ "VALIDATION_ERROR",
1400
+ `Screenshot exceeds max size (${Math.round(maxBytes / 1024 / 1024)}MB).`
1401
+ );
1402
+ }
1403
+ }
1404
+ function validateVideoSize(size, maxBytes) {
1405
+ if (size > maxBytes) {
1406
+ throw new BugReporterError("VALIDATION_ERROR", `Recording exceeds max size (${Math.round(maxBytes / 1024 / 1024)}MB).`);
1407
+ }
1408
+ }
1409
+ function drawArrow(ctx, start, end) {
1410
+ const headLength = 14;
1411
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
1412
+ ctx.beginPath();
1413
+ ctx.moveTo(start.x, start.y);
1414
+ ctx.lineTo(end.x, end.y);
1415
+ ctx.stroke();
1416
+ ctx.beginPath();
1417
+ ctx.moveTo(end.x, end.y);
1418
+ ctx.lineTo(end.x - headLength * Math.cos(angle - Math.PI / 6), end.y - headLength * Math.sin(angle - Math.PI / 6));
1419
+ ctx.lineTo(end.x - headLength * Math.cos(angle + Math.PI / 6), end.y - headLength * Math.sin(angle + Math.PI / 6));
1420
+ ctx.closePath();
1421
+ ctx.fill();
1422
+ }
1423
+ var AnnotationCanvas = forwardRef(function AnnotationCanvas2({ imageUrl }, ref) {
1424
+ const canvasRef = useRef(null);
1425
+ const imageRef = useRef(null);
1426
+ const [tool, setTool] = useState("rectangle");
1427
+ const [shapes, setShapes] = useState([]);
1428
+ const [draftShape, setDraftShape] = useState(null);
1429
+ const dragStartRef = useRef(null);
1430
+ const redraw = useCallback(
1431
+ (ctx) => {
1432
+ const canvas = canvasRef.current;
1433
+ const image = imageRef.current;
1434
+ if (!canvas || !image) {
1435
+ return;
1436
+ }
1437
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1438
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
1439
+ const toDraw = draftShape ? [...shapes, draftShape] : shapes;
1440
+ ctx.strokeStyle = "#ff3b30";
1441
+ ctx.fillStyle = "#ff3b30";
1442
+ ctx.lineWidth = 3;
1443
+ ctx.font = "16px sans-serif";
1444
+ toDraw.forEach((shape) => {
1445
+ if (shape.kind === "rectangle") {
1446
+ const x = Math.min(shape.start.x, shape.end.x);
1447
+ const y = Math.min(shape.start.y, shape.end.y);
1448
+ const width = Math.abs(shape.end.x - shape.start.x);
1449
+ const height = Math.abs(shape.end.y - shape.start.y);
1450
+ ctx.strokeRect(x, y, width, height);
1451
+ }
1452
+ if (shape.kind === "arrow") {
1453
+ drawArrow(ctx, shape.start, shape.end);
1454
+ }
1455
+ });
1456
+ },
1457
+ [draftShape, shapes]
1458
+ );
1459
+ useEffect(() => {
1460
+ const image = new Image();
1461
+ image.crossOrigin = "anonymous";
1462
+ image.onload = () => {
1463
+ imageRef.current = image;
1464
+ const canvas = canvasRef.current;
1465
+ if (!canvas) {
1466
+ return;
1467
+ }
1468
+ canvas.width = image.width;
1469
+ canvas.height = image.height;
1470
+ const ctx = canvas.getContext("2d");
1471
+ if (!ctx) {
1472
+ return;
1473
+ }
1474
+ redraw(ctx);
1475
+ };
1476
+ image.src = imageUrl;
1477
+ }, [imageUrl, redraw]);
1478
+ useEffect(() => {
1479
+ const canvas = canvasRef.current;
1480
+ if (!canvas) {
1481
+ return;
1482
+ }
1483
+ const ctx = canvas.getContext("2d");
1484
+ if (!ctx) {
1485
+ return;
1486
+ }
1487
+ redraw(ctx);
1488
+ }, [redraw]);
1489
+ const getCoords = useCallback((event) => {
1490
+ const canvas = canvasRef.current;
1491
+ if (!canvas) {
1492
+ return { x: 0, y: 0 };
1493
+ }
1494
+ const rect = canvas.getBoundingClientRect();
1495
+ const scaleX = canvas.width / rect.width;
1496
+ const scaleY = canvas.height / rect.height;
1497
+ return {
1498
+ x: (event.clientX - rect.left) * scaleX,
1499
+ y: (event.clientY - rect.top) * scaleY
1500
+ };
1501
+ }, []);
1502
+ const onMouseDown = (event) => {
1503
+ const point = getCoords(event);
1504
+ dragStartRef.current = point;
1505
+ };
1506
+ const onMouseMove = (event) => {
1507
+ const start = dragStartRef.current;
1508
+ if (!start) {
1509
+ return;
1510
+ }
1511
+ const end = getCoords(event);
1512
+ if (tool === "rectangle") {
1513
+ setDraftShape({ kind: "rectangle", start, end });
1514
+ } else if (tool === "arrow") {
1515
+ setDraftShape({ kind: "arrow", start, end });
1516
+ }
1517
+ };
1518
+ const onMouseUp = (event) => {
1519
+ const start = dragStartRef.current;
1520
+ if (!start) {
1521
+ return;
1522
+ }
1523
+ dragStartRef.current = null;
1524
+ const end = getCoords(event);
1525
+ if (tool === "rectangle") {
1526
+ setShapes((prev) => [...prev, { kind: "rectangle", start, end }]);
1527
+ } else if (tool === "arrow") {
1528
+ setShapes((prev) => [...prev, { kind: "arrow", start, end }]);
1529
+ }
1530
+ setDraftShape(null);
1531
+ };
1532
+ const canvasToBlob = useCallback(async () => {
1533
+ const canvas = canvasRef.current;
1534
+ if (!canvas) {
1535
+ throw new Error("Annotation canvas unavailable.");
1536
+ }
1537
+ const blob = await new Promise((resolve) => {
1538
+ canvas.toBlob(resolve, "image/png");
1539
+ });
1540
+ if (!blob) {
1541
+ throw new Error("Failed to create annotated screenshot.");
1542
+ }
1543
+ return blob;
1544
+ }, []);
1545
+ useImperativeHandle(
1546
+ ref,
1547
+ () => ({
1548
+ exportBlob: canvasToBlob,
1549
+ clear: () => setShapes([]),
1550
+ hasAnnotations: () => shapes.length > 0
1551
+ }),
1552
+ [canvasToBlob, shapes.length]
1553
+ );
1554
+ const toolButtons = useMemo(
1555
+ () => [
1556
+ { value: "rectangle", label: "Rectangle" },
1557
+ { value: "arrow", label: "Arrow" }
1558
+ ],
1559
+ []
1560
+ );
1561
+ return /* @__PURE__ */ React.createElement("div", { style: inlineStyles.annotation }, /* @__PURE__ */ React.createElement("div", { style: inlineStyles.annotationTools, role: "toolbar", "aria-label": "Annotation tools" }, toolButtons.map((option) => /* @__PURE__ */ React.createElement(
1562
+ "button",
1563
+ {
1564
+ key: option.value,
1565
+ type: "button",
1566
+ style: getButtonStyle("secondary", { active: tool === option.value }),
1567
+ onClick: () => setTool(option.value)
1568
+ },
1569
+ option.label
1570
+ )), /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("secondary"), onClick: () => setShapes([]) }, "Clear")), /* @__PURE__ */ React.createElement(
1571
+ "canvas",
1572
+ {
1573
+ ref: canvasRef,
1574
+ style: inlineStyles.annotationCanvas,
1575
+ onMouseDown,
1576
+ onMouseMove,
1577
+ onMouseUp,
1578
+ "aria-label": "Screenshot annotation canvas"
1579
+ }
1580
+ ));
1581
+ });
1582
+ var sharedRecording = null;
1583
+ var subscribers = /* @__PURE__ */ new Set();
1584
+ function notifySubscribers() {
1585
+ subscribers.forEach((handler) => handler());
1586
+ }
1587
+ function StepRecording({ onBack, onNext, embedded = false, compact = false }) {
1588
+ const {
1589
+ config,
1590
+ state: { assets },
1591
+ setRecording
1592
+ } = useBugReporter();
1593
+ const recording = useMemo(() => assets.find((asset) => asset.type === "recording"), [assets]);
1594
+ const activeRef = useRef(null);
1595
+ const mountedRef = useRef(true);
1596
+ const [isRecording, setIsRecording] = useState(Boolean(sharedRecording));
1597
+ const [seconds, setSeconds] = useState(sharedRecording?.seconds ?? 0);
1598
+ const [error, setError] = useState(null);
1599
+ const start = async () => {
1600
+ if (sharedRecording) {
1601
+ setError("Recording is already in progress.");
1602
+ return;
1603
+ }
1604
+ setError(null);
1605
+ setSeconds(0);
1606
+ setIsRecording(true);
1607
+ try {
1608
+ const recorderModule = await loadScreenRecording();
1609
+ const active = await recorderModule.startScreenRecording({
1610
+ maxSeconds: config.storage.limits.maxVideoSeconds,
1611
+ maxBytes: config.storage.limits.maxVideoBytes,
1612
+ onTick: (tick) => {
1613
+ if (sharedRecording) {
1614
+ sharedRecording.seconds = tick;
1615
+ notifySubscribers();
1616
+ }
1617
+ }
1618
+ });
1619
+ activeRef.current = active;
1620
+ sharedRecording = { active, seconds: 0 };
1621
+ notifySubscribers();
1622
+ const result = await active.promise;
1623
+ validateVideoSize(result.blob.size, config.storage.limits.maxVideoBytes);
1624
+ setRecording({
1625
+ id: uid("recording"),
1626
+ type: "recording",
1627
+ blob: result.blob,
1628
+ previewUrl: blobToObjectUrl(result.blob),
1629
+ mimeType: result.mimeType,
1630
+ filename: `recording-${Date.now()}.webm`,
1631
+ size: result.blob.size
1632
+ });
1633
+ } catch (recordingError) {
1634
+ if (recordingError instanceof Error && recordingError.message.includes("cancelled")) {
1635
+ return;
1636
+ }
1637
+ if (mountedRef.current) {
1638
+ setError(recordingError instanceof Error ? recordingError.message : "Recording failed.");
1639
+ }
1640
+ } finally {
1641
+ activeRef.current = null;
1642
+ sharedRecording = null;
1643
+ notifySubscribers();
1644
+ if (mountedRef.current) {
1645
+ setIsRecording(false);
1646
+ }
1647
+ }
1648
+ };
1649
+ const stop = () => {
1650
+ if (sharedRecording) {
1651
+ sharedRecording.active.stop();
1652
+ return;
1653
+ }
1654
+ activeRef.current?.stop();
1655
+ };
1656
+ useEffect(() => {
1657
+ mountedRef.current = true;
1658
+ const sync = () => {
1659
+ activeRef.current = sharedRecording?.active ?? null;
1660
+ setIsRecording(Boolean(sharedRecording));
1661
+ setSeconds(sharedRecording?.seconds ?? 0);
1662
+ };
1663
+ sync();
1664
+ subscribers.add(sync);
1665
+ return () => {
1666
+ mountedRef.current = false;
1667
+ subscribers.delete(sync);
1668
+ };
1669
+ }, []);
1670
+ if (compact) {
1671
+ const compactRecordingAction = !isRecording ? /* @__PURE__ */ React.createElement("button", { type: "button", style: { ...getButtonStyle("primary", { fullWidth: true }), ...inlineStyles.captureButton }, onClick: start }, /* @__PURE__ */ React.createElement("svg", { style: inlineStyles.captureIcon, viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "12", r: "4.5" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 8.5h3l1.2-2h8.6l1.2 2h3v7h-3l-1.2 2H7.7l-1.2-2h-3z" })), /* @__PURE__ */ React.createElement("span", null, recording ? "Retake recording" : "Record a video"), recording ? /* @__PURE__ */ React.createElement("span", { style: inlineStyles.captureDone }, "Saved") : null) : /* @__PURE__ */ React.createElement("button", { type: "button", style: { ...getButtonStyle("danger", { fullWidth: true }), ...inlineStyles.captureButton }, onClick: stop }, /* @__PURE__ */ React.createElement("svg", { style: inlineStyles.captureIcon, viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("rect", { x: "7", y: "7", width: "10", height: "10" })), /* @__PURE__ */ React.createElement("span", null, "Stop (", seconds, "s)"));
1672
+ return /* @__PURE__ */ React.createElement("div", { style: inlineStyles.captureItem }, compactRecordingAction, error ? /* @__PURE__ */ React.createElement("p", { style: { ...inlineStyles.error, marginTop: "8px" } }, error) : null);
1673
+ }
1674
+ return /* @__PURE__ */ React.createElement("div", { style: embedded ? void 0 : inlineStyles.step }, embedded ? /* @__PURE__ */ React.createElement("h3", { style: inlineStyles.h3 }, "Screen recording") : /* @__PURE__ */ React.createElement("h2", { style: inlineStyles.h2 }, "Screen recording"), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.p }, "Record up to ", config.storage.limits.maxVideoSeconds, " seconds. You can minimize this sidebar while recording."), /* @__PURE__ */ React.createElement("div", { style: inlineStyles.actions }, onBack ? /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("secondary", { disabled: isRecording }), onClick: onBack, disabled: isRecording }, "Back") : null, !isRecording ? /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("primary"), onClick: start }, recording ? "Retake recording" : "Record a video") : /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("danger"), onClick: stop }, "Stop (", seconds, "s)")), recording ? /* @__PURE__ */ React.createElement("video", { src: recording.previewUrl, style: inlineStyles.preview, controls: true }) : null, error ? /* @__PURE__ */ React.createElement("p", { style: inlineStyles.error }, error) : null, onNext ? /* @__PURE__ */ React.createElement("div", { style: inlineStyles.actions }, /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("primary"), onClick: onNext }, "Continue")) : null);
1675
+ }
1676
+
1677
+ // src/components/StepDescribe.tsx
1678
+ function StepDescribe({ onNext, CustomForm }) {
1679
+ const {
1680
+ config,
1681
+ state: { draft, attributes, assets },
1682
+ updateDraft,
1683
+ setAttributes,
1684
+ updateAttribute,
1685
+ setScreenshot
1686
+ } = useBugReporter();
1687
+ const screenshot = useMemo(() => assets.find((asset) => asset.type === "screenshot"), [assets]);
1688
+ const recording = useMemo(() => assets.find((asset) => asset.type === "recording"), [assets]);
1689
+ const annotationRef = useRef(null);
1690
+ const customFormRef = useRef(null);
1691
+ const [isCapturing, setIsCapturing] = useState(false);
1692
+ const [error, setError] = useState(null);
1693
+ useEffect(() => {
1694
+ if (!CustomForm) {
1695
+ return;
1696
+ }
1697
+ const root = customFormRef.current;
1698
+ if (!root) {
1699
+ return;
1700
+ }
1701
+ const applyInlineFormStyles = () => {
1702
+ root.querySelectorAll("input, textarea, select").forEach((control) => {
1703
+ if (control.dataset.brInlineStyled === "true") {
1704
+ return;
1705
+ }
1706
+ Object.assign(control.style, inlineStyles.input, {
1707
+ width: "100%",
1708
+ boxSizing: "border-box"
1709
+ });
1710
+ control.dataset.brInlineStyled = "true";
1711
+ });
1712
+ root.querySelectorAll("label.br-field").forEach((field) => {
1713
+ if (field.dataset.brInlineStyled === "true") {
1714
+ return;
1715
+ }
1716
+ Object.assign(field.style, inlineStyles.field);
1717
+ field.dataset.brInlineStyled = "true";
1718
+ });
1719
+ };
1720
+ applyInlineFormStyles();
1721
+ const observer = new MutationObserver(() => {
1722
+ applyInlineFormStyles();
1723
+ });
1724
+ observer.observe(root, { childList: true, subtree: true });
1725
+ return () => observer.disconnect();
1726
+ }, [CustomForm]);
1727
+ const startCapture = async () => {
1728
+ setError(null);
1729
+ setIsCapturing(true);
1730
+ try {
1731
+ const capture = await loadScreenshotCapture();
1732
+ const blob = await capture.captureScreenshotArea({
1733
+ maskSelectors: config.privacy.maskSelectors,
1734
+ redactTextPatterns: config.privacy.redactTextPatterns
1735
+ });
1736
+ validateScreenshotSize(blob.size, config.storage.limits.maxScreenshotBytes);
1737
+ setScreenshot({
1738
+ id: uid("screenshot"),
1739
+ type: "screenshot",
1740
+ blob,
1741
+ previewUrl: blobToObjectUrl(blob),
1742
+ mimeType: blob.type || "image/png",
1743
+ filename: `screenshot-${Date.now()}.png`,
1744
+ size: blob.size
1745
+ });
1746
+ } catch (captureError) {
1747
+ setError(captureError instanceof Error ? captureError.message : "Screenshot capture failed.");
1748
+ } finally {
1749
+ setIsCapturing(false);
1750
+ }
1751
+ };
1752
+ const continueToNext = async () => {
1753
+ setError(null);
1754
+ if (config.features.screenshot && !screenshot) {
1755
+ setError("Capture a screenshot to continue.");
1756
+ return;
1757
+ }
1758
+ if (config.features.screenshot && screenshot && config.features.annotations && annotationRef.current?.hasAnnotations()) {
1759
+ const annotatedBlob = await annotationRef.current.exportBlob();
1760
+ validateScreenshotSize(annotatedBlob.size, config.storage.limits.maxScreenshotBytes);
1761
+ setScreenshot({
1762
+ ...screenshot,
1763
+ blob: annotatedBlob,
1764
+ previewUrl: blobToObjectUrl(annotatedBlob),
1765
+ mimeType: annotatedBlob.type || "image/png",
1766
+ size: annotatedBlob.size
1767
+ });
1768
+ }
1769
+ onNext();
1770
+ };
1771
+ const canContinue = Boolean(draft.title.trim()) && (!config.features.screenshot || Boolean(screenshot));
1772
+ return /* @__PURE__ */ React.createElement("div", { style: { ...inlineStyles.step, display: "flex", flexDirection: "column", minHeight: "100%" } }, /* @__PURE__ */ React.createElement("h2", { style: inlineStyles.h2 }, "Report a bug"), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.p }, "Provide enough context so engineers can reproduce what happened."), /* @__PURE__ */ React.createElement("label", { style: inlineStyles.field }, "Title", /* @__PURE__ */ React.createElement(
1773
+ "input",
1774
+ {
1775
+ style: inlineStyles.input,
1776
+ value: draft.title,
1777
+ onChange: (event) => updateDraft({ title: event.target.value }),
1778
+ placeholder: "Short summary",
1779
+ required: true
1780
+ }
1781
+ )), /* @__PURE__ */ React.createElement("label", { style: inlineStyles.field }, "Description", /* @__PURE__ */ React.createElement(
1782
+ "textarea",
1783
+ {
1784
+ style: inlineStyles.input,
1785
+ value: draft.description,
1786
+ onChange: (event) => updateDraft({ description: event.target.value }),
1787
+ placeholder: "What happened?",
1788
+ rows: 4
1789
+ }
1790
+ )), CustomForm ? /* @__PURE__ */ React.createElement("div", { ref: customFormRef }, /* @__PURE__ */ React.createElement(CustomForm, { attributes, setAttributes, updateAttribute })) : null, config.features.screenshot || config.features.recording ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("h3", { style: inlineStyles.h3 }, "Capture"), /* @__PURE__ */ React.createElement("div", { style: inlineStyles.captureRow }, config.features.screenshot ? /* @__PURE__ */ React.createElement("div", { style: inlineStyles.captureItem }, /* @__PURE__ */ React.createElement(
1791
+ "button",
1792
+ {
1793
+ type: "button",
1794
+ style: { ...getButtonStyle("primary", { disabled: isCapturing, fullWidth: true }), ...inlineStyles.captureButton },
1795
+ onClick: startCapture,
1796
+ disabled: isCapturing
1797
+ },
1798
+ /* @__PURE__ */ React.createElement("svg", { style: inlineStyles.captureIcon, viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false" }, /* @__PURE__ */ React.createElement("path", { d: "M4 8.5h3l1.2-2h7.6l1.2 2h3v9H4z" }), /* @__PURE__ */ React.createElement("circle", { cx: "12", cy: "13", r: "3.5" })),
1799
+ /* @__PURE__ */ React.createElement("span", null, isCapturing ? "Capturing..." : screenshot ? "Retake screenshot" : "Screenshot"),
1800
+ screenshot ? /* @__PURE__ */ React.createElement("span", { style: inlineStyles.captureDone }, "Saved") : null
1801
+ )) : null, config.features.recording ? /* @__PURE__ */ React.createElement(StepRecording, { embedded: true, compact: true }) : null), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.captureNote }, config.features.screenshot && config.features.recording ? `Capture a screenshot and optionally record up to ${config.storage.limits.maxVideoSeconds} seconds.` : config.features.screenshot ? "Capture the relevant area before continuing." : `Record up to ${config.storage.limits.maxVideoSeconds} seconds.`), screenshot ? /* @__PURE__ */ React.createElement("div", { style: inlineStyles.previewWrapper }, config.features.annotations ? /* @__PURE__ */ React.createElement(AnnotationCanvas, { ref: annotationRef, imageUrl: screenshot.previewUrl }) : /* @__PURE__ */ React.createElement("img", { src: screenshot.previewUrl, alt: "Screenshot preview", style: inlineStyles.preview })) : null, !screenshot && recording ? /* @__PURE__ */ React.createElement("p", { style: inlineStyles.captureNote }, "Recording saved.") : null) : null, error ? /* @__PURE__ */ React.createElement("p", { style: inlineStyles.error }, error) : null, /* @__PURE__ */ React.createElement(
1802
+ "div",
1803
+ {
1804
+ style: {
1805
+ ...inlineStyles.actions,
1806
+ marginTop: "auto",
1807
+ position: "sticky",
1808
+ bottom: 0,
1809
+ background: "var(--br-bg)",
1810
+ paddingTop: "12px",
1811
+ paddingBottom: "12px"
1812
+ }
1813
+ },
1814
+ /* @__PURE__ */ React.createElement(
1815
+ "button",
1816
+ {
1817
+ type: "button",
1818
+ style: getButtonStyle("primary", { disabled: !canContinue, fullWidth: true }),
1819
+ disabled: !canContinue,
1820
+ onClick: continueToNext
1821
+ },
1822
+ "Continue"
1823
+ )
1824
+ ));
1825
+ }
1826
+
1827
+ // src/components/StepReview.tsx
1828
+ function StepReview({ onBack }) {
1829
+ const {
1830
+ state: { assets, draft, isSubmitting, uploadProgress, error },
1831
+ submit
1832
+ } = useBugReporter();
1833
+ return /* @__PURE__ */ React.createElement("div", { style: { ...inlineStyles.step, display: "flex", flexDirection: "column", minHeight: "100%" } }, /* @__PURE__ */ React.createElement("h2", { style: inlineStyles.h2 }, "Review and submit"), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.p }, "Confirm details, then send your report."), /* @__PURE__ */ React.createElement("div", { style: inlineStyles.summary }, /* @__PURE__ */ React.createElement("strong", null, draft.title || "Untitled bug report"), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.p }, draft.description || "No description provided.")), /* @__PURE__ */ React.createElement("div", { style: inlineStyles.assets }, assets.map((asset) => /* @__PURE__ */ React.createElement("div", { key: asset.id, style: inlineStyles.assetCard }, /* @__PURE__ */ React.createElement("span", { style: inlineStyles.assetType }, asset.type), asset.type === "recording" ? /* @__PURE__ */ React.createElement("video", { src: asset.previewUrl, controls: true, style: inlineStyles.preview }) : /* @__PURE__ */ React.createElement("img", { src: asset.previewUrl, alt: `${asset.type} preview`, style: inlineStyles.preview })))), isSubmitting ? /* @__PURE__ */ React.createElement("div", { style: inlineStyles.uploadProgress, "aria-live": "polite" }, "Uploading assets: ", Math.round(uploadProgress * 100), "%", /* @__PURE__ */ React.createElement("div", { style: inlineStyles.progressTrack }, /* @__PURE__ */ React.createElement("div", { style: { ...inlineStyles.progressFill, width: `${Math.round(uploadProgress * 100)}%` } }))) : null, error ? /* @__PURE__ */ React.createElement("p", { style: inlineStyles.error }, error) : null, /* @__PURE__ */ React.createElement(
1834
+ "div",
1835
+ {
1836
+ style: {
1837
+ ...inlineStyles.actions,
1838
+ marginTop: "auto",
1839
+ position: "sticky",
1840
+ bottom: 0,
1841
+ background: "var(--br-bg)",
1842
+ paddingTop: "12px",
1843
+ paddingBottom: "12px"
1844
+ }
1845
+ },
1846
+ /* @__PURE__ */ React.createElement(
1847
+ "button",
1848
+ {
1849
+ type: "button",
1850
+ style: { ...getButtonStyle("secondary", { disabled: isSubmitting }), flex: 1 },
1851
+ onClick: onBack,
1852
+ disabled: isSubmitting
1853
+ },
1854
+ "Back"
1855
+ ),
1856
+ /* @__PURE__ */ React.createElement(
1857
+ "button",
1858
+ {
1859
+ type: "button",
1860
+ style: { ...getButtonStyle("primary", { disabled: isSubmitting }), flex: 1 },
1861
+ onClick: submit,
1862
+ disabled: isSubmitting
1863
+ },
1864
+ "Submit"
1865
+ )
1866
+ ));
1867
+ }
1868
+
1869
+ // src/components/BugReporter.tsx
1870
+ var DOCK_SIDES = ["left", "right", "top", "bottom"];
1871
+ function DockIcon({ side }) {
1872
+ if (side === "left") {
1873
+ return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", style: inlineStyles.iconSvg }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h17v13h-17z" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h4.5v13H3.5z", fill: "currentColor", stroke: "none", opacity: "0.85" }));
1874
+ }
1875
+ if (side === "right") {
1876
+ return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", style: inlineStyles.iconSvg }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h17v13h-17z" }), /* @__PURE__ */ React.createElement("path", { d: "M16 5.5h4.5v13H16z", fill: "currentColor", stroke: "none", opacity: "0.85" }));
1877
+ }
1878
+ if (side === "top") {
1879
+ return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", style: inlineStyles.iconSvg }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h17v13h-17z" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h17v4.5h-17z", fill: "currentColor", stroke: "none", opacity: "0.85" }));
1880
+ }
1881
+ return /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", style: inlineStyles.iconSvg }, /* @__PURE__ */ React.createElement("path", { d: "M3.5 5.5h17v13h-17z" }), /* @__PURE__ */ React.createElement("path", { d: "M3.5 14h17v4.5h-17z", fill: "currentColor", stroke: "none", opacity: "0.85" }));
1882
+ }
1883
+ function BugReporterShell({ CustomForm, launcherPosition, launcherText, themeMode, buttonColor }) {
1884
+ const { config, state, setDockSide, setStep, close, reset } = useBugReporter();
1885
+ const nextFromDescribe = () => {
1886
+ setStep("review");
1887
+ };
1888
+ const requestClose = () => {
1889
+ if (state.step === "success") {
1890
+ reset();
1891
+ }
1892
+ close();
1893
+ };
1894
+ const modalTitle = state.step === "success" ? "Report submitted" : "Report a bug";
1895
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(LauncherButton, { position: launcherPosition, text: launcherText, themeMode, buttonColor }), /* @__PURE__ */ React.createElement(
1896
+ Modal,
1897
+ {
1898
+ isOpen: state.isOpen,
1899
+ dockSide: state.dockSide,
1900
+ themeMode,
1901
+ buttonColor,
1902
+ title: modalTitle,
1903
+ zIndex: config.theme.zIndex + 1,
1904
+ onRequestClose: requestClose
1905
+ },
1906
+ /* @__PURE__ */ React.createElement("div", { style: inlineStyles.modalHeader }, /* @__PURE__ */ React.createElement("div", { style: inlineStyles.dockControls, role: "group", "aria-label": "Dock side" }, DOCK_SIDES.map((side) => {
1907
+ const isActive = state.dockSide === side;
1908
+ return /* @__PURE__ */ React.createElement(
1909
+ "button",
1910
+ {
1911
+ key: side,
1912
+ style: getDockButtonStyle(isActive),
1913
+ type: "button",
1914
+ onClick: () => setDockSide(side),
1915
+ "aria-pressed": isActive,
1916
+ "aria-label": `Dock ${side}`
1917
+ },
1918
+ /* @__PURE__ */ React.createElement(DockIcon, { side })
1919
+ );
1920
+ })), /* @__PURE__ */ React.createElement("button", { style: inlineStyles.iconButton, type: "button", onClick: requestClose, "aria-label": "Close bug reporter" }, /* @__PURE__ */ React.createElement("svg", { viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", style: inlineStyles.iconSvg }, /* @__PURE__ */ React.createElement("path", { d: "M6 6 18 18M18 6 6 18" })))),
1921
+ state.step === "describe" ? /* @__PURE__ */ React.createElement(StepDescribe, { onNext: nextFromDescribe, CustomForm }) : null,
1922
+ state.step === "review" || state.step === "submitting" ? /* @__PURE__ */ React.createElement(
1923
+ StepReview,
1924
+ {
1925
+ onBack: () => {
1926
+ setStep("describe");
1927
+ }
1928
+ }
1929
+ ) : null,
1930
+ state.step === "success" ? /* @__PURE__ */ React.createElement("div", { style: inlineStyles.step }, /* @__PURE__ */ React.createElement("h2", { style: inlineStyles.h2 }, "Thanks, report submitted"), /* @__PURE__ */ React.createElement("p", { style: inlineStyles.p }, "Your bug report has been sent successfully."), /* @__PURE__ */ React.createElement("div", { style: inlineStyles.actions }, /* @__PURE__ */ React.createElement("button", { type: "button", style: getButtonStyle("primary"), onClick: requestClose }, "Close"))) : null
1931
+ ));
1932
+ }
1933
+ function BugReporter({
1934
+ config,
1935
+ CustomForm,
1936
+ launcherPosition,
1937
+ launcherText,
1938
+ themeMode = "dark",
1939
+ buttonColor
1940
+ }) {
1941
+ const resolvedButtonColor = buttonColor ?? config.theme?.primaryColor ?? "#3b82f6";
1942
+ return /* @__PURE__ */ React.createElement(BugReporterProvider, { config }, /* @__PURE__ */ React.createElement(
1943
+ BugReporterShell,
1944
+ {
1945
+ CustomForm,
1946
+ launcherPosition,
1947
+ launcherText,
1948
+ themeMode,
1949
+ buttonColor: resolvedButtonColor
1950
+ }
1951
+ ));
1952
+ }
1953
+
1954
+ export { BugReporter, BugReporterProvider, useBugReporter };
1955
+ //# sourceMappingURL=index.js.map
1956
+ //# sourceMappingURL=index.js.map