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