@jarve/bug-reporter 0.4.3 → 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.
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
- 'use client';
2
1
  "use strict";
2
+ var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __defProps = Object.defineProperties;
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
7
7
  var __getOwnPropNames = Object.getOwnPropertyNames;
8
8
  var __getOwnPropSymbols = Object.getOwnPropertySymbols;
9
+ var __getProtoOf = Object.getPrototypeOf;
9
10
  var __hasOwnProp = Object.prototype.hasOwnProperty;
10
11
  var __propIsEnum = Object.prototype.propertyIsEnumerable;
11
12
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
@@ -33,6 +34,14 @@ var __copyProps = (to, from, except, desc) => {
33
34
  }
34
35
  return to;
35
36
  };
37
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
38
+ // If the importer is in node compatibility mode or this is not an ESM
39
+ // file that has been converted to a CommonJS file using a Babel-
40
+ // compatible transform (i.e. "__esModule" has not been set), then set
41
+ // "default" to the CommonJS "module.exports" for node compatibility.
42
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
43
+ mod
44
+ ));
36
45
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
37
46
 
38
47
  // src/index.ts
@@ -43,26 +52,26 @@ __export(index_exports, {
43
52
  module.exports = __toCommonJS(index_exports);
44
53
 
45
54
  // src/bug-reporter.tsx
46
- var import_react5 = require("react");
55
+ var import_react4 = require("react");
47
56
 
48
57
  // src/floating-button.tsx
49
58
  var import_react = require("react");
50
59
  var import_lucide_react = require("lucide-react");
51
-
52
- // src/cn.ts
53
- var import_clsx = require("clsx");
54
- var import_tailwind_merge = require("tailwind-merge");
55
- function cn(...inputs) {
56
- return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
57
- }
58
-
59
- // src/floating-button.tsx
60
+ var import_widget_shared = require("@jarve/widget-shared");
60
61
  var import_jsx_runtime = require("react/jsx-runtime");
61
62
  var STACK_OFFSET = 56;
62
- function FloatingButton({ isActive, onClick, position = "right" }) {
63
+ function hasLauncherElement() {
64
+ return typeof document !== "undefined" && !!document.querySelector("[data-jarve-launcher]");
65
+ }
66
+ function FloatingButton({
67
+ isActive,
68
+ onClick,
69
+ position = "right",
70
+ zIndexBase = 1e4
71
+ }) {
63
72
  const [hovered, setHovered] = (0, import_react.useState)(false);
64
73
  const [stackOffset, setStackOffset] = (0, import_react.useState)(0);
65
- const [launcherPresent, setLauncherPresent] = (0, import_react.useState)(false);
74
+ const [launcherPresent, setLauncherPresent] = (0, import_react.useState)(hasLauncherElement);
66
75
  const ref = (0, import_react.useRef)(null);
67
76
  const isLeft = position === "left";
68
77
  const sideClasses = isLeft ? "left-4 md:left-6" : "right-4 md:right-6";
@@ -71,6 +80,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
71
80
  if (!el) return;
72
81
  let rafId;
73
82
  const recalculate = () => {
83
+ if (!ref.current) return;
74
84
  const widgets = Array.from(
75
85
  document.querySelectorAll(`[data-jarve-widget][data-jarve-position="${position}"]`)
76
86
  );
@@ -82,25 +92,26 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
82
92
  const index = widgets.indexOf(el);
83
93
  setStackOffset(index > 0 ? index * STACK_OFFSET : 0);
84
94
  };
85
- recalculate();
86
- const observer = new MutationObserver(() => {
95
+ const scheduleRecalculate = () => {
87
96
  cancelAnimationFrame(rafId);
88
97
  rafId = requestAnimationFrame(recalculate);
89
- });
90
- observer.observe(document.body, { childList: true, subtree: true });
98
+ };
99
+ recalculate();
100
+ const offReg = (0, import_widget_shared.on)("jarve:widget-registered", () => scheduleRecalculate());
101
+ const offDereg = (0, import_widget_shared.on)("jarve:widget-deregistered", () => scheduleRecalculate());
91
102
  return () => {
92
- observer.disconnect();
103
+ offReg();
104
+ offDereg();
93
105
  cancelAnimationFrame(rafId);
94
106
  };
95
107
  }, [position]);
96
108
  (0, import_react.useEffect)(() => {
97
- const checkLauncher = () => {
98
- setLauncherPresent(!!document.querySelector("[data-jarve-launcher]"));
109
+ const offMounted = (0, import_widget_shared.on)("jarve:launcher-mounted", () => setLauncherPresent(true));
110
+ const offUnmounted = (0, import_widget_shared.on)("jarve:launcher-unmounted", () => setLauncherPresent(false));
111
+ return () => {
112
+ offMounted();
113
+ offUnmounted();
99
114
  };
100
- checkLauncher();
101
- const observer = new MutationObserver(checkLauncher);
102
- observer.observe(document.body, { childList: true, subtree: true });
103
- return () => observer.disconnect();
104
115
  }, []);
105
116
  if (launcherPresent) return null;
106
117
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -109,15 +120,17 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
109
120
  ref,
110
121
  "data-jarve-widget": "bug-reporter",
111
122
  "data-jarve-position": position,
112
- className: cn("fixed z-[9999]", "bottom-4 md:bottom-6", sideClasses),
113
- style: stackOffset > 0 ? { transform: `translateY(-${stackOffset}px)` } : void 0,
123
+ className: (0, import_widget_shared.cn)("fixed z-[calc(var(--jarve-z-base)-1)]", "bottom-4 md:bottom-6", sideClasses),
124
+ style: __spreadValues({
125
+ ["--jarve-z-base"]: String(zIndexBase)
126
+ }, stackOffset > 0 ? { transform: `translateY(-${stackOffset}px)` } : {}),
114
127
  onMouseEnter: () => setHovered(true),
115
128
  onMouseLeave: () => setHovered(false),
116
129
  children: [
117
130
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
118
131
  "div",
119
132
  {
120
- className: cn(
133
+ className: (0, import_widget_shared.cn)(
121
134
  "pointer-events-none absolute bottom-full mb-3 w-max max-w-[240px] rounded-xl bg-gray-900/95 px-3.5 py-2.5 text-xs leading-relaxed text-white shadow-xl backdrop-blur-sm transition-all duration-200",
122
135
  isLeft ? "left-0" : "right-0",
123
136
  hovered && !isActive ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0"
@@ -129,7 +142,7 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
129
142
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
130
143
  "div",
131
144
  {
132
- className: cn(
145
+ className: (0, import_widget_shared.cn)(
133
146
  "absolute top-full h-0 w-0 border-x-[6px] border-t-[6px] border-x-transparent border-t-gray-900",
134
147
  isLeft ? "left-4" : "right-4"
135
148
  )
@@ -142,9 +155,9 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
142
155
  "button",
143
156
  {
144
157
  onClick,
145
- className: cn(
158
+ className: (0, import_widget_shared.cn)(
146
159
  "flex items-center justify-center rounded-full shadow-lg transition-all duration-200",
147
- "hover:scale-110 focus:ring-2 focus:ring-offset-2 focus:outline-none",
160
+ "focus:ring-2 focus:ring-offset-2 focus:outline-none motion-safe:hover:scale-110",
148
161
  "h-11 w-11 md:h-12 md:w-12",
149
162
  isActive ? "animate-pulse bg-red-500 text-white focus:ring-red-400" : "bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-400"
150
163
  ),
@@ -160,10 +173,9 @@ function FloatingButton({ isActive, onClick, position = "right" }) {
160
173
 
161
174
  // src/capture-overlay.tsx
162
175
  var import_react2 = require("react");
163
- var import_html_to_image = require("html-to-image");
164
176
 
165
177
  // src/utils.ts
166
- var import_ua_parser_js = require("ua-parser-js");
178
+ var import_widget_shared2 = require("@jarve/widget-shared");
167
179
  function getNearestSection(element) {
168
180
  let current = element;
169
181
  while (current && current !== document.body) {
@@ -193,13 +205,7 @@ function getDeviceType() {
193
205
  return "desktop";
194
206
  }
195
207
  function parseUserAgent() {
196
- const parser = new import_ua_parser_js.UAParser(navigator.userAgent);
197
- const browser = parser.getBrowser();
198
- const osInfo = parser.getOS();
199
- return {
200
- browser: `${browser.name || "Unknown"} ${browser.version || ""}`.trim(),
201
- os: `${osInfo.name || "Unknown"} ${osInfo.version || ""}`.trim()
202
- };
208
+ return (0, import_widget_shared2.parseUserAgent)();
203
209
  }
204
210
  function buildSelectorPath(element, stopAt) {
205
211
  const parts = [];
@@ -270,84 +276,114 @@ function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, cl
270
276
  };
271
277
  }
272
278
 
279
+ // src/console-capture.ts
280
+ var import_widget_shared3 = require("@jarve/widget-shared");
281
+
282
+ // src/redact.ts
283
+ var SENSITIVE_JSON_KEY = /("(?:token|password|secret|api_?key|authorization)"\s*:\s*")[^"]*"/gi;
284
+ var BEARER_TOKEN = /(bearer)\s+[^\s"',;}]+/gi;
285
+ var COOKIE_HEADER = /(set-cookie|cookie)\s*:\s*[^\r\n]+/gi;
286
+ function redactSensitive(input) {
287
+ if (input === null || input === void 0) return input;
288
+ return input.replace(COOKIE_HEADER, (_, header) => `${header}: [REDACTED]`).replace(SENSITIVE_JSON_KEY, (_, keyPrefix) => `${keyPrefix}[REDACTED]"`).replace(BEARER_TOKEN, (_, word) => `${word} [REDACTED]`);
289
+ }
290
+
273
291
  // src/console-capture.ts
274
292
  var MAX_ERRORS = 50;
293
+ var MAX_MSG_LEN = 500;
294
+ function serializeArg(arg) {
295
+ var _a;
296
+ if (typeof arg === "string") return arg;
297
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}
298
+ ${(_a = arg.stack) != null ? _a : ""}`;
299
+ try {
300
+ return JSON.stringify(arg);
301
+ } catch (e) {
302
+ return String(arg);
303
+ }
304
+ }
275
305
  var capturedErrors = [];
276
- var isCapturing = false;
277
306
  var originalConsoleError = null;
307
+ var patchedConsoleErrorRef = null;
278
308
  var errorListener = null;
279
309
  var rejectionListener = null;
280
- function startCapturing() {
281
- if (isCapturing) return;
282
- if (console.error.__bugReporterPatched) return;
283
- isCapturing = true;
284
- capturedErrors = [];
285
- originalConsoleError = console.error;
286
- const patchedConsoleError = (...args) => {
287
- const MAX_MSG_LEN = 500;
288
- const message = args.map((a) => {
289
- if (typeof a === "string") return a.slice(0, MAX_MSG_LEN);
290
- try {
291
- const s = JSON.stringify(a);
292
- return s.slice(0, MAX_MSG_LEN);
293
- } catch (e) {
294
- return String(a).slice(0, MAX_MSG_LEN);
310
+ var releasePatch = null;
311
+ var patchManager = (0, import_widget_shared3.createPatchManager)({
312
+ markerKey: "__jarveBugReporterConsolePatch",
313
+ install: () => {
314
+ capturedErrors = [];
315
+ originalConsoleError = console.error;
316
+ const patchedConsoleError = (...args) => {
317
+ const message = args.map((arg) => redactSensitive(serializeArg(arg)).slice(0, MAX_MSG_LEN)).join(" ").slice(0, MAX_MSG_LEN);
318
+ capturedErrors.push({
319
+ message,
320
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
321
+ });
322
+ if (capturedErrors.length > MAX_ERRORS) {
323
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
295
324
  }
296
- }).join(" ").slice(0, MAX_MSG_LEN);
297
- capturedErrors.push({
298
- message,
299
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
300
- });
301
- if (capturedErrors.length > MAX_ERRORS) {
302
- capturedErrors = capturedErrors.slice(-MAX_ERRORS);
325
+ originalConsoleError.apply(console, args);
326
+ };
327
+ patchedConsoleError.__bugReporterPatched = true;
328
+ patchedConsoleErrorRef = patchedConsoleError;
329
+ console.error = patchedConsoleError;
330
+ errorListener = (event) => {
331
+ capturedErrors.push({
332
+ message: event.message,
333
+ source: event.filename,
334
+ lineno: event.lineno,
335
+ colno: event.colno,
336
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
337
+ });
338
+ if (capturedErrors.length > MAX_ERRORS) {
339
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
340
+ }
341
+ };
342
+ window.addEventListener("error", errorListener);
343
+ rejectionListener = (event) => {
344
+ capturedErrors.push({
345
+ message: `Unhandled Promise Rejection: ${event.reason instanceof Error ? event.reason.message : String(event.reason)}`,
346
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
347
+ });
348
+ if (capturedErrors.length > MAX_ERRORS) {
349
+ capturedErrors = capturedErrors.slice(-MAX_ERRORS);
350
+ }
351
+ };
352
+ window.addEventListener("unhandledrejection", rejectionListener);
353
+ },
354
+ uninstall: () => {
355
+ if (console.error === patchedConsoleErrorRef && originalConsoleError) {
356
+ console.error = originalConsoleError;
357
+ } else {
358
+ console.warn(
359
+ "Bug reporter: console.error was replaced by another library after we patched it. Leaving the foreign patch in place \u2014 original console.error is no longer reachable via our handle."
360
+ );
303
361
  }
304
- originalConsoleError.apply(console, args);
305
- };
306
- patchedConsoleError.__bugReporterPatched = true;
307
- console.error = patchedConsoleError;
308
- errorListener = (event) => {
309
- capturedErrors.push({
310
- message: event.message,
311
- source: event.filename,
312
- lineno: event.lineno,
313
- colno: event.colno,
314
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
315
- });
316
- if (capturedErrors.length > MAX_ERRORS) {
317
- capturedErrors = capturedErrors.slice(-MAX_ERRORS);
362
+ if (patchedConsoleErrorRef) {
363
+ delete patchedConsoleErrorRef.__bugReporterPatched;
318
364
  }
319
- };
320
- window.addEventListener("error", errorListener);
321
- rejectionListener = (event) => {
322
- capturedErrors.push({
323
- message: `Unhandled Promise Rejection: ${event.reason instanceof Error ? event.reason.message : String(event.reason)}`,
324
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
325
- });
326
- if (capturedErrors.length > MAX_ERRORS) {
327
- capturedErrors = capturedErrors.slice(-MAX_ERRORS);
365
+ originalConsoleError = null;
366
+ patchedConsoleErrorRef = null;
367
+ if (errorListener) {
368
+ window.removeEventListener("error", errorListener);
369
+ errorListener = null;
328
370
  }
329
- };
330
- window.addEventListener("unhandledrejection", rejectionListener);
331
- }
332
- function stopCapturing() {
333
- if (!isCapturing) return;
334
- isCapturing = false;
335
- if (originalConsoleError) {
336
- const restoreTarget = originalConsoleError;
337
- console.error = restoreTarget;
338
- if (console.error !== restoreTarget) {
339
- console.warn("Bug reporter: failed to restore original console.error");
371
+ if (rejectionListener) {
372
+ window.removeEventListener("unhandledrejection", rejectionListener);
373
+ rejectionListener = null;
340
374
  }
341
- originalConsoleError = null;
342
- }
343
- if (errorListener) {
344
- window.removeEventListener("error", errorListener);
345
- errorListener = null;
346
- }
347
- if (rejectionListener) {
348
- window.removeEventListener("unhandledrejection", rejectionListener);
349
- rejectionListener = null;
350
375
  }
376
+ });
377
+ function startCapturing() {
378
+ if (typeof window === "undefined") return;
379
+ if (releasePatch) return;
380
+ releasePatch = patchManager.acquire();
381
+ }
382
+ function stopCapturing() {
383
+ if (!releasePatch) return;
384
+ const release = releasePatch;
385
+ releasePatch = null;
386
+ release();
351
387
  }
352
388
  function getCapturedErrors() {
353
389
  return [...capturedErrors];
@@ -357,17 +393,38 @@ function clearCapturedErrors() {
357
393
  }
358
394
 
359
395
  // src/network-capture.ts
396
+ var import_widget_shared4 = require("@jarve/widget-shared");
360
397
  var MAX_REQUESTS = 30;
361
398
  var MAX_BODY_READ_BYTES = 64 * 1024;
362
399
  var TRUNCATE_LEN = 500;
363
400
  var capturedRequests = [];
364
- var isCapturing2 = false;
365
401
  var originalFetch = null;
402
+ var patchedFetchRef = null;
403
+ var releasePatch2 = null;
404
+ var bodyCaptureMode = "off";
366
405
  function truncateBody(body, maxLen = TRUNCATE_LEN) {
367
406
  if (!body) return null;
368
407
  if (body.length <= maxLen) return body;
369
408
  return body.slice(0, maxLen) + "...(truncated)";
370
409
  }
410
+ function sanitizeRequestUrl(raw) {
411
+ try {
412
+ const parsed = new URL(raw);
413
+ return {
414
+ url: parsed.origin + parsed.pathname,
415
+ hasQueryString: parsed.search.length > 0 || parsed.hash.length > 0
416
+ };
417
+ } catch (e) {
418
+ const queryIdx = raw.indexOf("?");
419
+ const hashIdx = raw.indexOf("#");
420
+ const cutoffs = [queryIdx, hashIdx].filter((i) => i >= 0);
421
+ if (cutoffs.length === 0) {
422
+ return { url: raw, hasQueryString: false };
423
+ }
424
+ const cut = Math.min(...cutoffs);
425
+ return { url: raw.slice(0, cut), hasQueryString: true };
426
+ }
427
+ }
371
428
  async function readBoundedBody(response) {
372
429
  try {
373
430
  const contentLength = response.headers.get("Content-Length");
@@ -395,72 +452,98 @@ async function readBoundedBody(response) {
395
452
  }
396
453
  const decoder = new TextDecoder();
397
454
  const text2 = chunks.map((c) => decoder.decode(c, { stream: true })).join("");
398
- return truncateBody(text2);
455
+ return truncateBody(redactSensitive(text2));
399
456
  }
400
457
  const text = await cloned.text();
401
- return truncateBody(text);
458
+ return truncateBody(redactSensitive(text));
402
459
  } catch (e) {
403
460
  return null;
404
461
  }
405
462
  }
406
- function startNetworkCapture() {
407
- if (isCapturing2 || typeof window === "undefined") return;
408
- if (window.fetch.__bugReporterPatched) return;
409
- isCapturing2 = true;
410
- capturedRequests = [];
411
- originalFetch = window.fetch;
412
- const patchedFetch = async function patchedFetch2(input, init) {
413
- const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
414
- const method = (init == null ? void 0 : init.method) || (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET") || "GET";
415
- if (url.includes("/api/bug-reporter/") || url.includes("/bug-reporter/external/") || url.startsWith("data:") || url.startsWith("blob:")) {
416
- return originalFetch.call(window, input, init);
417
- }
418
- try {
419
- const response = await originalFetch.call(window, input, init);
420
- if (response.status >= 400) {
421
- const responseBody = await readBoundedBody(response);
463
+ var patchManager2 = (0, import_widget_shared4.createPatchManager)({
464
+ markerKey: "__jarveBugReporterFetchPatch",
465
+ install: () => {
466
+ if (typeof window === "undefined") return;
467
+ capturedRequests = [];
468
+ originalFetch = window.fetch;
469
+ const patchedFetch = async function patchedFetch2(input, init) {
470
+ const rawUrl = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
471
+ const method = (init == null ? void 0 : init.method) || (typeof input !== "string" && !(input instanceof URL) ? input.method : "GET") || "GET";
472
+ if (rawUrl.includes("/api/bug-reporter/") || rawUrl.includes("/bug-reporter/external/") || rawUrl.startsWith("data:") || rawUrl.startsWith("blob:")) {
473
+ return originalFetch.call(window, input, init);
474
+ }
475
+ const { url, hasQueryString } = sanitizeRequestUrl(rawUrl);
476
+ try {
477
+ const response = await originalFetch.call(window, input, init);
478
+ if (response.status >= 400) {
479
+ const responseBody = bodyCaptureMode === "on" ? await readBoundedBody(response) : null;
480
+ capturedRequests.push({
481
+ url,
482
+ hasQueryString,
483
+ method: method.toUpperCase(),
484
+ status: response.status,
485
+ statusText: response.statusText,
486
+ responseBody,
487
+ contentType: response.headers.get("content-type"),
488
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
489
+ });
490
+ if (capturedRequests.length > MAX_REQUESTS) {
491
+ capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
492
+ }
493
+ }
494
+ return response;
495
+ } catch (error) {
422
496
  capturedRequests.push({
423
497
  url,
498
+ hasQueryString,
424
499
  method: method.toUpperCase(),
425
- status: response.status,
426
- statusText: response.statusText,
427
- responseBody,
500
+ status: 0,
501
+ statusText: error instanceof Error ? error.message : "Network error",
502
+ responseBody: null,
503
+ contentType: null,
428
504
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
429
505
  });
430
506
  if (capturedRequests.length > MAX_REQUESTS) {
431
507
  capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
432
508
  }
509
+ throw error;
433
510
  }
434
- return response;
435
- } catch (error) {
436
- capturedRequests.push({
437
- url,
438
- method: method.toUpperCase(),
439
- status: 0,
440
- statusText: error instanceof Error ? error.message : "Network error",
441
- responseBody: null,
442
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
443
- });
444
- if (capturedRequests.length > MAX_REQUESTS) {
445
- capturedRequests = capturedRequests.slice(-MAX_REQUESTS);
446
- }
447
- throw error;
511
+ };
512
+ patchedFetch.__bugReporterPatched = true;
513
+ patchedFetchRef = patchedFetch;
514
+ window.fetch = patchedFetch;
515
+ },
516
+ uninstall: () => {
517
+ if (typeof window === "undefined") {
518
+ originalFetch = null;
519
+ patchedFetchRef = null;
520
+ return;
448
521
  }
449
- };
450
- patchedFetch.__bugReporterPatched = true;
451
- window.fetch = patchedFetch;
452
- }
453
- function stopNetworkCapture() {
454
- if (!isCapturing2) return;
455
- isCapturing2 = false;
456
- if (originalFetch) {
457
- const restoreTarget = originalFetch;
458
- window.fetch = restoreTarget;
459
- if (window.fetch !== restoreTarget) {
460
- console.warn("Bug reporter: failed to restore original fetch");
522
+ if (window.fetch === patchedFetchRef && originalFetch) {
523
+ window.fetch = originalFetch;
524
+ } else {
525
+ console.warn(
526
+ "Bug reporter: window.fetch was replaced by another library after we patched it. Leaving the foreign patch in place \u2014 original fetch is no longer reachable via our handle."
527
+ );
528
+ }
529
+ if (patchedFetchRef) {
530
+ delete patchedFetchRef.__bugReporterPatched;
461
531
  }
462
532
  originalFetch = null;
533
+ patchedFetchRef = null;
463
534
  }
535
+ });
536
+ function startNetworkCapture(options = {}) {
537
+ if (typeof window === "undefined") return;
538
+ bodyCaptureMode = options.captureResponseBodies ? "on" : "off";
539
+ if (releasePatch2) return;
540
+ releasePatch2 = patchManager2.acquire();
541
+ }
542
+ function stopNetworkCapture() {
543
+ if (!releasePatch2) return;
544
+ const release = releasePatch2;
545
+ releasePatch2 = null;
546
+ release();
464
547
  }
465
548
  function getCapturedNetworkErrors() {
466
549
  return [...capturedRequests];
@@ -474,11 +557,16 @@ var import_jsx_runtime2 = require("react/jsx-runtime");
474
557
  function dataUrlToBlob(dataUrl) {
475
558
  var _a;
476
559
  const [header, base64] = dataUrl.split(",");
560
+ if (!header || !base64) return new Blob();
477
561
  const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
478
- const bytes = atob(base64);
479
- const arr = new Uint8Array(bytes.length);
480
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
481
- return new Blob([arr], { type: mime });
562
+ try {
563
+ const bytes = atob(base64);
564
+ const arr = new Uint8Array(bytes.length);
565
+ for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
566
+ return new Blob([arr], { type: mime });
567
+ } catch (e) {
568
+ return new Blob();
569
+ }
482
570
  }
483
571
  function CaptureOverlay({
484
572
  isActive,
@@ -486,11 +574,12 @@ function CaptureOverlay({
486
574
  reporterName,
487
575
  reporterEmail,
488
576
  onCapture,
489
- onCancel
577
+ onCancel,
578
+ zIndexBase = 1e4
490
579
  }) {
491
580
  const [hoveredElement, setHoveredElement] = (0, import_react2.useState)(null);
492
581
  const [hoveredRect, setHoveredRect] = (0, import_react2.useState)(null);
493
- const [isCapturing3, setIsCapturing] = (0, import_react2.useState)(false);
582
+ const [isCapturing, setIsCapturing] = (0, import_react2.useState)(false);
494
583
  const [isTouchMode, setIsTouchMode] = (0, import_react2.useState)(false);
495
584
  const [selectedSection, setSelectedSection] = (0, import_react2.useState)(null);
496
585
  const [selectedRect, setSelectedRect] = (0, import_react2.useState)(null);
@@ -504,10 +593,20 @@ function CaptureOverlay({
504
593
  setIsTouchMode(isTouchCapable());
505
594
  }
506
595
  }, [isActive]);
596
+ (0, import_react2.useEffect)(() => {
597
+ if (typeof document === "undefined") return;
598
+ if (!isActive || isTouchMode) return;
599
+ const root = document.documentElement;
600
+ root.classList.add("jarve-capturing");
601
+ return () => {
602
+ root.classList.remove("jarve-capturing");
603
+ };
604
+ }, [isActive, isTouchMode]);
507
605
  const captureScreenshot = (0, import_react2.useCallback)(
508
606
  async (section, target, coords) => {
509
607
  const elementInfo = collectElementInfo(target, section, coords);
510
608
  setIsCapturing(true);
609
+ const { toPng } = await import("html-to-image");
511
610
  try {
512
611
  setHoveredElement(null);
513
612
  setHoveredRect(null);
@@ -519,7 +618,7 @@ function CaptureOverlay({
519
618
  const MAX_DIMENSION = 2e3;
520
619
  const sectionRect = section.getBoundingClientRect();
521
620
  const pixelRatio = sectionRect.width > MAX_DIMENSION || sectionRect.height > MAX_DIMENSION ? 1 : 2;
522
- const dataUrl = await (0, import_html_to_image.toPng)(section, {
621
+ const dataUrl = await toPng(section, {
523
622
  quality: 0.9,
524
623
  pixelRatio,
525
624
  skipFonts: true
@@ -541,7 +640,7 @@ function CaptureOverlay({
541
640
  err
542
641
  );
543
642
  try {
544
- const dataUrl = await (0, import_html_to_image.toPng)(section, {
643
+ const dataUrl = await toPng(section, {
545
644
  quality: 0.6,
546
645
  pixelRatio: 1,
547
646
  skipFonts: true,
@@ -584,7 +683,7 @@ function CaptureOverlay({
584
683
  );
585
684
  const handleMouseMove = (0, import_react2.useCallback)(
586
685
  (e) => {
587
- if (!isActive || isCapturing3 || isTouchMode) return;
686
+ if (!isActive || isCapturing || isTouchMode) return;
588
687
  if (rafRef.current) return;
589
688
  rafRef.current = requestAnimationFrame(() => {
590
689
  rafRef.current = null;
@@ -602,11 +701,11 @@ function CaptureOverlay({
602
701
  setHoveredRect(section ? section.getBoundingClientRect() : null);
603
702
  });
604
703
  },
605
- [isActive, isCapturing3, isTouchMode]
704
+ [isActive, isCapturing, isTouchMode]
606
705
  );
607
706
  const handleClick = (0, import_react2.useCallback)(
608
707
  async (e) => {
609
- if (!isActive || isCapturing3 || isTouchMode) return;
708
+ if (!isActive || isCapturing || isTouchMode) return;
610
709
  const target = e.target;
611
710
  if (!(target instanceof HTMLElement)) return;
612
711
  if (target.closest("[data-bug-reporter]")) return;
@@ -616,16 +715,18 @@ function CaptureOverlay({
616
715
  if (!section) return;
617
716
  await captureScreenshot(section, target, extractCoordinates(e));
618
717
  },
619
- [isActive, isCapturing3, isTouchMode, captureScreenshot]
718
+ [isActive, isCapturing, isTouchMode, captureScreenshot]
620
719
  );
621
720
  const handleTouchEnd = (0, import_react2.useCallback)(
622
721
  (e) => {
623
- if (!isActive || isCapturing3) return;
722
+ if (!isActive || isCapturing) return;
624
723
  const touch = e.changedTouches[0];
625
724
  if (!touch) return;
626
725
  const target = document.elementFromPoint(touch.clientX, touch.clientY);
627
726
  if (!(target instanceof HTMLElement)) return;
628
727
  if (target.closest("[data-bug-reporter]")) return;
728
+ if (e.cancelable) e.preventDefault();
729
+ e.stopPropagation();
629
730
  const section = getNearestSection(target);
630
731
  if (!section) return;
631
732
  setSelectedSection(section);
@@ -633,7 +734,7 @@ function CaptureOverlay({
633
734
  setSelectedTarget(target);
634
735
  touchCoordsRef.current = extractCoordinates(touch);
635
736
  },
636
- [isActive, isCapturing3]
737
+ [isActive, isCapturing]
637
738
  );
638
739
  const handleConfirmCapture = (0, import_react2.useCallback)(async () => {
639
740
  if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
@@ -683,7 +784,7 @@ function CaptureOverlay({
683
784
  document.addEventListener("keydown", handleKeyDown);
684
785
  window.addEventListener("scroll", handleScroll, { passive: true });
685
786
  if (isTouchMode) {
686
- document.addEventListener("touchend", handleTouchEnd, { passive: true });
787
+ document.addEventListener("touchend", handleTouchEnd, { passive: false });
687
788
  } else {
688
789
  document.addEventListener("mousemove", handleMouseMove, true);
689
790
  document.addEventListener("click", handleClick, true);
@@ -715,9 +816,10 @@ function CaptureOverlay({
715
816
  "div",
716
817
  {
717
818
  "data-bug-reporter": true,
718
- role: "alert",
719
- "aria-live": "assertive",
720
- className: "fixed top-0 right-0 left-0 z-[10000] flex items-center justify-center gap-3 bg-indigo-600 px-4 py-2 text-center text-sm font-medium text-white",
819
+ role: "status",
820
+ "aria-live": "polite",
821
+ className: "fixed top-0 right-0 left-0 z-[calc(var(--jarve-z-base))] flex items-center justify-center gap-3 bg-indigo-600 px-4 py-2 text-center text-sm font-medium text-white",
822
+ style: { ["--jarve-z-base"]: String(zIndexBase) },
721
823
  children: isTouchMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
722
824
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Tap the section with the bug" }),
723
825
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
@@ -741,8 +843,9 @@ function CaptureOverlay({
741
843
  {
742
844
  ref: overlayRef,
743
845
  "data-bug-reporter": true,
744
- className: "pointer-events-none fixed z-[9998] rounded-sm border-2 border-indigo-500 transition-all duration-150 ease-out",
846
+ className: "pointer-events-none fixed z-[calc(var(--jarve-z-base)-2)] rounded-sm border-2 border-indigo-500 transition-all duration-150 ease-out",
745
847
  style: {
848
+ ["--jarve-z-base"]: String(zIndexBase),
746
849
  top: highlightRect.top - 2,
747
850
  left: highlightRect.left - 2,
748
851
  width: highlightRect.width + 4,
@@ -751,12 +854,15 @@ function CaptureOverlay({
751
854
  }
752
855
  }
753
856
  ),
754
- isTouchMode && selectedSection && !isCapturing3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
857
+ isTouchMode && selectedSection && !isCapturing && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
755
858
  "div",
756
859
  {
757
860
  "data-bug-reporter": true,
758
- className: "fixed right-0 bottom-0 left-0 z-[10000] border-t border-gray-200 bg-white shadow-lg",
759
- style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
861
+ className: "fixed right-0 bottom-0 left-0 z-[calc(var(--jarve-z-base))] border-t border-gray-200 bg-white shadow-lg",
862
+ style: {
863
+ ["--jarve-z-base"]: String(zIndexBase),
864
+ paddingBottom: "env(safe-area-inset-bottom, 0px)"
865
+ },
760
866
  children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
761
867
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate text-sm font-medium text-gray-900", children: "Capture this section?" }),
762
868
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 gap-2", children: [
@@ -784,132 +890,179 @@ function CaptureOverlay({
784
890
  ] })
785
891
  ] })
786
892
  }
787
- ),
788
- !isTouchMode && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
893
+ )
789
894
  ] });
790
895
  }
791
896
 
792
897
  // src/report-modal.tsx
793
898
  var import_react3 = require("react");
794
899
  var import_lucide_react2 = require("lucide-react");
900
+ var import_widget_shared5 = require("@jarve/widget-shared");
795
901
  var import_jsx_runtime3 = require("react/jsx-runtime");
902
+ var STARTER_ASSISTANT_MESSAGE = {
903
+ role: "assistant",
904
+ content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
905
+ };
906
+ var DRAFT_STORAGE_PREFIX = "jarve:draft:bug-reporter:";
907
+ function draftStorageKey(siteId) {
908
+ return `${DRAFT_STORAGE_PREFIX}${siteId}`;
909
+ }
910
+ function readDraft(siteId) {
911
+ if (typeof sessionStorage === "undefined") return null;
912
+ try {
913
+ const raw = sessionStorage.getItem(draftStorageKey(siteId));
914
+ if (!raw) return null;
915
+ const parsed = JSON.parse(raw);
916
+ if (typeof (parsed == null ? void 0 : parsed.input) !== "string" || !Array.isArray(parsed == null ? void 0 : parsed.messages)) return null;
917
+ return parsed;
918
+ } catch (e) {
919
+ return null;
920
+ }
921
+ }
922
+ function writeDraft(siteId, draft) {
923
+ if (typeof sessionStorage === "undefined") return;
924
+ try {
925
+ sessionStorage.setItem(draftStorageKey(siteId), JSON.stringify(draft));
926
+ } catch (e) {
927
+ }
928
+ }
929
+ function clearDraft(siteId) {
930
+ if (typeof sessionStorage === "undefined") return;
931
+ try {
932
+ sessionStorage.removeItem(draftStorageKey(siteId));
933
+ } catch (e) {
934
+ }
935
+ }
796
936
  function ReportModal({
797
937
  isOpen,
798
938
  captureResult,
799
939
  apiConfig,
800
940
  siteId,
801
- user,
802
- onClose
941
+ onClose,
942
+ zIndexBase = 1e4,
943
+ theme = "auto",
944
+ onBeforeSubmit
803
945
  }) {
804
- const [messages, setMessages] = (0, import_react3.useState)([]);
946
+ const resolvedTheme = (0, import_widget_shared5.useResolvedTheme)(theme);
947
+ const [messages, setMessages] = (0, import_react3.useState)([STARTER_ASSISTANT_MESSAGE]);
805
948
  const [input, setInput] = (0, import_react3.useState)("");
806
949
  const [isLoading, setIsLoading] = (0, import_react3.useState)(false);
807
950
  const [modalState, setModalState] = (0, import_react3.useState)("chatting");
808
951
  const [reportId, setReportId] = (0, import_react3.useState)(null);
809
952
  const [screenshotUrl, setScreenshotUrl] = (0, import_react3.useState)(null);
810
953
  const [errorMessage, setErrorMessage] = (0, import_react3.useState)(null);
811
- const chatEndRef = (0, import_react3.useRef)(null);
954
+ const [submitAlert, setSubmitAlert] = (0, import_react3.useState)(null);
955
+ const chatScrollRef = (0, import_react3.useRef)(null);
812
956
  const inputRef = (0, import_react3.useRef)(null);
813
- const hasInitRef = (0, import_react3.useRef)(false);
957
+ const dialogRef = (0, import_react3.useRef)(null);
958
+ const [sessionToken, setSessionToken] = (0, import_react3.useState)(() => "");
959
+ const activeSessionRef = (0, import_react3.useRef)("");
960
+ const abortControllerRef = (0, import_react3.useRef)(null);
961
+ if (abortControllerRef.current === null) {
962
+ abortControllerRef.current = new AbortController();
963
+ }
964
+ (0, import_react3.useEffect)(() => {
965
+ var _a, _b, _c;
966
+ if (isOpen) {
967
+ const next = crypto.randomUUID();
968
+ activeSessionRef.current = next;
969
+ setSessionToken(next);
970
+ const draft = readDraft(siteId);
971
+ setMessages(draft && draft.messages.length > 0 ? draft.messages : [STARTER_ASSISTANT_MESSAGE]);
972
+ setInput((_a = draft == null ? void 0 : draft.input) != null ? _a : "");
973
+ setIsLoading(false);
974
+ setModalState("chatting");
975
+ setReportId(null);
976
+ setErrorMessage(null);
977
+ setSubmitAlert(null);
978
+ if ((_b = abortControllerRef.current) == null ? void 0 : _b.signal.aborted) {
979
+ abortControllerRef.current = new AbortController();
980
+ }
981
+ } else {
982
+ (_c = abortControllerRef.current) == null ? void 0 : _c.abort();
983
+ }
984
+ }, [isOpen, siteId]);
985
+ (0, import_react3.useEffect)(() => {
986
+ return () => {
987
+ var _a;
988
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
989
+ };
990
+ }, []);
991
+ (0, import_react3.useEffect)(() => {
992
+ if (!isOpen) return;
993
+ if (modalState === "submitted" || modalState === "submitting") return;
994
+ const hasNonStarterMessages = messages.length > 1 || messages.length === 1 && messages[0] !== STARTER_ASSISTANT_MESSAGE;
995
+ if (!input && !hasNonStarterMessages) return;
996
+ writeDraft(siteId, { input, messages });
997
+ }, [isOpen, modalState, input, messages, siteId]);
814
998
  const apiHeaders = (0, import_react3.useMemo)(
815
999
  () => ({
816
1000
  "Content-Type": "application/json",
817
- "X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim()
1001
+ "X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
1002
+ "X-Jarve-Session": sessionToken
1003
+ }),
1004
+ [apiConfig.apiKey, sessionToken]
1005
+ );
1006
+ const submitHeaders = (0, import_react3.useMemo)(
1007
+ () => ({
1008
+ "X-Bug-Reporter-Key": (apiConfig.apiKey || "").trim(),
1009
+ "X-Jarve-Session": sessionToken
818
1010
  }),
819
- [apiConfig.apiKey]
1011
+ [apiConfig.apiKey, sessionToken]
820
1012
  );
821
1013
  (0, import_react3.useEffect)(() => {
822
- if ((captureResult == null ? void 0 : captureResult.screenshot) && captureResult.screenshot.size > 0) {
823
- const url = URL.createObjectURL(captureResult.screenshot);
824
- setScreenshotUrl((prev) => {
825
- if (prev) URL.revokeObjectURL(prev);
826
- return url;
827
- });
828
- return () => {
829
- URL.revokeObjectURL(url);
830
- setScreenshotUrl(null);
831
- };
1014
+ const blob = captureResult == null ? void 0 : captureResult.screenshot;
1015
+ if (!blob || blob.size === 0) {
1016
+ setScreenshotUrl(null);
1017
+ return;
832
1018
  }
833
- setScreenshotUrl((prev) => {
834
- if (prev) URL.revokeObjectURL(prev);
835
- return null;
836
- });
1019
+ const url = URL.createObjectURL(blob);
1020
+ setScreenshotUrl(url);
1021
+ return () => {
1022
+ URL.revokeObjectURL(url);
1023
+ };
837
1024
  }, [captureResult]);
838
- const sendInitialMessage = (0, import_react3.useCallback)(async () => {
839
- if (!captureResult) return;
840
- setIsLoading(true);
841
- try {
842
- const response = await fetch(`${apiConfig.apiUrl}/chat`, {
843
- method: "POST",
844
- headers: apiHeaders,
845
- body: JSON.stringify({
846
- messages: [],
847
- metadata: captureResult.metadata,
848
- consoleErrors: captureResult.consoleErrors,
849
- networkErrors: captureResult.networkErrors,
850
- clickedElement: captureResult.metadata.clickedElement || null
851
- })
852
- });
853
- if (response.status === 401) {
854
- console.error(
855
- "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
856
- );
857
- setMessages([
858
- {
859
- role: "assistant",
860
- content: "The bug reporter service isn't configured correctly. Please let the site administrator know."
861
- }
862
- ]);
863
- return;
864
- }
865
- if (!response.ok) throw new Error("Failed to get AI response");
866
- const data = await response.json();
867
- setMessages([{ role: "assistant", content: data.message }]);
868
- } catch (e) {
869
- setMessages([
870
- {
871
- role: "assistant",
872
- content: "I can see you've captured a section of the page. What's going wrong here? Please describe the issue you're experiencing."
873
- }
874
- ]);
875
- } finally {
876
- setIsLoading(false);
877
- }
878
- }, [captureResult, apiConfig.apiUrl, apiHeaders]);
879
1025
  (0, import_react3.useEffect)(() => {
880
- if (isOpen && captureResult && !hasInitRef.current) {
881
- hasInitRef.current = true;
882
- sendInitialMessage();
1026
+ const container = chatScrollRef.current;
1027
+ if (!container || typeof container.scrollTo !== "function") return;
1028
+ const gap = container.scrollHeight - container.scrollTop - container.clientHeight;
1029
+ if (gap < 100) {
1030
+ container.scrollTo({ top: container.scrollHeight, behavior: "smooth" });
883
1031
  }
884
- }, [isOpen, captureResult, sendInitialMessage]);
885
- (0, import_react3.useEffect)(() => {
886
- var _a;
887
- (_a = chatEndRef.current) == null ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
888
1032
  }, [messages]);
1033
+ (0, import_widget_shared5.useReturnFocus)(isOpen);
1034
+ (0, import_widget_shared5.useFocusTrap)(isOpen, dialogRef);
889
1035
  (0, import_react3.useEffect)(() => {
890
1036
  var _a;
891
1037
  if (isOpen && !isLoading) {
892
1038
  (_a = inputRef.current) == null ? void 0 : _a.focus();
893
1039
  }
894
1040
  }, [isOpen, isLoading, messages]);
1041
+ const buildFetchSignal = (0, import_react3.useCallback)(() => {
1042
+ const controller = abortControllerRef.current;
1043
+ return (0, import_widget_shared5.createTimeoutAbortSignal)(controller.signal, 3e4);
1044
+ }, []);
1045
+ const runWithFetchSignal = (0, import_react3.useCallback)(
1046
+ async (input2, init) => {
1047
+ const { signal, cleanup } = buildFetchSignal();
1048
+ try {
1049
+ return await fetch(input2, __spreadProps(__spreadValues({}, init), { signal }));
1050
+ } finally {
1051
+ cleanup();
1052
+ }
1053
+ },
1054
+ [buildFetchSignal]
1055
+ );
895
1056
  const submitReport = (0, import_react3.useCallback)(
896
1057
  async (conversation, structuredReport) => {
897
1058
  if (!captureResult || modalState !== "chatting") return;
898
1059
  setModalState("submitting");
1060
+ const sessionAtDispatch = activeSessionRef.current;
899
1061
  try {
900
- let screenshotBase64;
901
- if (captureResult.screenshot.size > 0) {
902
- const reader = new FileReader();
903
- screenshotBase64 = await new Promise((resolve, reject) => {
904
- reader.onload = () => resolve(reader.result);
905
- reader.onerror = reject;
906
- reader.readAsDataURL(captureResult.screenshot);
907
- });
908
- }
909
1062
  let report = structuredReport;
910
1063
  if (!report) {
911
1064
  try {
912
- const summaryResponse = await fetch(`${apiConfig.apiUrl}/chat`, {
1065
+ const summaryResponse = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
913
1066
  method: "POST",
914
1067
  headers: apiHeaders,
915
1068
  body: JSON.stringify({
@@ -921,6 +1074,7 @@ function ReportModal({
921
1074
  requestSummary: true
922
1075
  })
923
1076
  });
1077
+ if (sessionAtDispatch !== activeSessionRef.current) return;
924
1078
  if (summaryResponse.ok) {
925
1079
  const data2 = await summaryResponse.json();
926
1080
  report = data2.structuredReport;
@@ -928,46 +1082,87 @@ function ReportModal({
928
1082
  } catch (e) {
929
1083
  }
930
1084
  }
931
- const response = await fetch(`${apiConfig.apiUrl}/submit`, {
1085
+ if (sessionAtDispatch !== activeSessionRef.current) return;
1086
+ const basePayload = {
1087
+ metadata: captureResult.metadata,
1088
+ conversation,
1089
+ structuredReport: report,
1090
+ consoleErrors: captureResult.consoleErrors,
1091
+ networkErrors: captureResult.networkErrors,
1092
+ clickedElement: captureResult.metadata.clickedElement || null
1093
+ };
1094
+ let finalPayload = basePayload;
1095
+ if (onBeforeSubmit) {
1096
+ try {
1097
+ const maybe = await onBeforeSubmit(basePayload);
1098
+ if (sessionAtDispatch !== activeSessionRef.current) return;
1099
+ if (maybe === null || maybe === false) {
1100
+ setSubmitAlert("Submission cancelled. Update your report and try again.");
1101
+ setModalState("chatting");
1102
+ return;
1103
+ }
1104
+ finalPayload = maybe;
1105
+ } catch (hookErr) {
1106
+ if (sessionAtDispatch !== activeSessionRef.current) return;
1107
+ console.error("Bug reporter: onBeforeSubmit hook rejected the payload", hookErr);
1108
+ setSubmitAlert("Submission failed \u2014 please try again.");
1109
+ setModalState("chatting");
1110
+ return;
1111
+ }
1112
+ }
1113
+ const formData = new FormData();
1114
+ if (captureResult.screenshot.size > 0) {
1115
+ formData.append("screenshot", captureResult.screenshot);
1116
+ }
1117
+ formData.append("payload", JSON.stringify(finalPayload));
1118
+ const response = await runWithFetchSignal(`${apiConfig.apiUrl}/submit`, {
932
1119
  method: "POST",
933
- headers: apiHeaders,
934
- body: JSON.stringify({
935
- screenshot: screenshotBase64,
936
- metadata: captureResult.metadata,
937
- conversation,
938
- structuredReport: report,
939
- consoleErrors: captureResult.consoleErrors,
940
- networkErrors: captureResult.networkErrors,
941
- clickedElement: captureResult.metadata.clickedElement || null
942
- })
1120
+ headers: submitHeaders,
1121
+ body: formData
943
1122
  });
1123
+ if (sessionAtDispatch !== activeSessionRef.current) return;
944
1124
  if (!response.ok) {
945
1125
  const errorData = await response.json().catch(() => ({}));
946
1126
  throw new Error(errorData.error || "Failed to submit report");
947
1127
  }
948
1128
  const data = await response.json();
1129
+ if (sessionAtDispatch !== activeSessionRef.current) return;
949
1130
  setReportId(data.id);
950
1131
  setModalState("submitted");
1132
+ clearDraft(siteId);
951
1133
  } catch (err) {
1134
+ if (sessionAtDispatch !== activeSessionRef.current) return;
952
1135
  console.error("Bug reporter: failed to submit report", err);
953
1136
  setErrorMessage(err instanceof Error ? err.message : "Failed to submit report");
954
1137
  setModalState("error");
955
1138
  }
956
1139
  },
957
- [captureResult, apiConfig.apiUrl, apiHeaders, modalState]
1140
+ [
1141
+ captureResult,
1142
+ apiConfig.apiUrl,
1143
+ apiHeaders,
1144
+ submitHeaders,
1145
+ modalState,
1146
+ siteId,
1147
+ onBeforeSubmit,
1148
+ runWithFetchSignal
1149
+ ]
958
1150
  );
959
1151
  const handleManualSubmit = (0, import_react3.useCallback)(() => {
1152
+ setSubmitAlert(null);
960
1153
  submitReport(messages);
961
1154
  }, [submitReport, messages]);
962
1155
  async function sendMessage() {
963
1156
  if (!input.trim() || isLoading || !captureResult) return;
964
1157
  const userMessage = input.trim();
965
1158
  setInput("");
1159
+ setSubmitAlert(null);
966
1160
  const newMessages = [...messages, { role: "user", content: userMessage }];
967
1161
  setMessages(newMessages);
968
1162
  setIsLoading(true);
1163
+ const sessionAtDispatch = activeSessionRef.current;
969
1164
  try {
970
- const response = await fetch(`${apiConfig.apiUrl}/chat`, {
1165
+ const response = await runWithFetchSignal(`${apiConfig.apiUrl}/chat`, {
971
1166
  method: "POST",
972
1167
  headers: apiHeaders,
973
1168
  body: JSON.stringify({
@@ -978,6 +1173,7 @@ function ReportModal({
978
1173
  clickedElement: captureResult.metadata.clickedElement || null
979
1174
  })
980
1175
  });
1176
+ if (sessionAtDispatch !== activeSessionRef.current) return;
981
1177
  if (response.status === 401) {
982
1178
  console.error(
983
1179
  "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
@@ -993,6 +1189,7 @@ function ReportModal({
993
1189
  }
994
1190
  if (!response.ok) throw new Error("Failed to get AI response");
995
1191
  const data = await response.json();
1192
+ if (sessionAtDispatch !== activeSessionRef.current) return;
996
1193
  setMessages([...newMessages, { role: "assistant", content: data.message }]);
997
1194
  if (data.readyToSubmit && data.structuredReport) {
998
1195
  await submitReport(
@@ -1001,6 +1198,7 @@ function ReportModal({
1001
1198
  );
1002
1199
  }
1003
1200
  } catch (e) {
1201
+ if (sessionAtDispatch !== activeSessionRef.current) return;
1004
1202
  setMessages([
1005
1203
  ...newMessages,
1006
1204
  {
@@ -1009,22 +1207,23 @@ function ReportModal({
1009
1207
  }
1010
1208
  ]);
1011
1209
  } finally {
1012
- setIsLoading(false);
1210
+ if (sessionAtDispatch === activeSessionRef.current) {
1211
+ setIsLoading(false);
1212
+ }
1013
1213
  }
1014
1214
  }
1015
1215
  function handleClose() {
1016
- setMessages([]);
1216
+ var _a;
1217
+ (_a = abortControllerRef.current) == null ? void 0 : _a.abort();
1218
+ setMessages([STARTER_ASSISTANT_MESSAGE]);
1017
1219
  setInput("");
1018
1220
  setModalState("chatting");
1019
1221
  setReportId(null);
1020
1222
  setErrorMessage(null);
1021
- if (screenshotUrl) {
1022
- URL.revokeObjectURL(screenshotUrl);
1023
- }
1024
- setScreenshotUrl(null);
1025
- hasInitRef.current = false;
1223
+ setSubmitAlert(null);
1026
1224
  onClose();
1027
1225
  }
1226
+ (0, import_widget_shared5.useEscapeKey)(handleClose, isOpen);
1028
1227
  function handleKeyDown(e) {
1029
1228
  if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
1030
1229
  e.preventDefault();
@@ -1036,14 +1235,24 @@ function ReportModal({
1036
1235
  "div",
1037
1236
  {
1038
1237
  "data-bug-reporter": true,
1039
- className: "fixed inset-0 z-[10001] flex items-center justify-center bg-black/50 backdrop-blur-sm",
1238
+ className: "fixed inset-0 z-[calc(var(--jarve-z-base)+1)] flex items-center justify-center bg-black/50 backdrop-blur-sm",
1239
+ style: { ["--jarve-z-base"]: String(zIndexBase) },
1040
1240
  onClick: (e) => {
1041
1241
  if (e.target === e.currentTarget) handleClose();
1042
1242
  },
1043
1243
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1044
1244
  "div",
1045
1245
  {
1046
- className: cn(
1246
+ ref: dialogRef,
1247
+ role: "dialog",
1248
+ "aria-modal": "true",
1249
+ "aria-labelledby": "jarve-bug-reporter-title",
1250
+ className: (0, import_widget_shared5.cn)(
1251
+ // Self-owned theme wrapper — `dark` toggles the existing Tailwind
1252
+ // `dark:` utilities via the shared `@custom-variant dark (&:is(.dark *))`
1253
+ // rule in styles.css. This makes the widget independent of the
1254
+ // host app's Tailwind darkMode config. See Issue E11.
1255
+ resolvedTheme === "dark" ? "jarve-theme-dark dark" : "jarve-theme-light",
1047
1256
  "flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-950",
1048
1257
  "mx-4 w-full max-w-lg",
1049
1258
  "max-[768px]:mx-0 max-[768px]:h-full max-[768px]:max-w-none max-[768px]:rounded-none",
@@ -1052,7 +1261,14 @@ function ReportModal({
1052
1261
  children: [
1053
1262
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gray-50/30 px-4 py-3 dark:border-gray-800 dark:bg-gray-900/30", children: [
1054
1263
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
1055
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Bug Report" }),
1264
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1265
+ "h2",
1266
+ {
1267
+ id: "jarve-bug-reporter-title",
1268
+ className: "text-sm font-semibold text-gray-900 dark:text-gray-100",
1269
+ children: "Bug Report"
1270
+ }
1271
+ ),
1056
1272
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
1057
1273
  ] }),
1058
1274
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -1073,58 +1289,76 @@ function ReportModal({
1073
1289
  className: "max-h-40 w-full rounded-md border border-gray-200 object-contain dark:border-gray-700"
1074
1290
  }
1075
1291
  ) }),
1076
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3", children: modalState === "submitted" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
1077
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
1078
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
1079
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
1080
- "Reference:",
1081
- " ",
1082
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800", children: reportId == null ? void 0 : reportId.slice(0, 8) })
1083
- ] }),
1084
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: "Thanks for the report \u2014 we'll look into it." }),
1085
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1086
- "button",
1087
- {
1088
- onClick: handleClose,
1089
- className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
1090
- children: "Done"
1091
- }
1092
- )
1093
- ] }) : modalState === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
1094
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "mb-3 h-12 w-12 text-red-500" }),
1095
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
1096
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: errorMessage || "Something went wrong. Please try again." }),
1292
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1293
+ "div",
1294
+ {
1295
+ ref: chatScrollRef,
1296
+ "data-testid": "chat-scroll",
1297
+ className: "min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3",
1298
+ children: modalState === "submitted" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
1299
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
1300
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
1301
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
1302
+ "Reference:",
1303
+ " ",
1304
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "rounded bg-gray-100 px-1.5 py-0.5 text-xs dark:bg-gray-800", children: reportId == null ? void 0 : reportId.slice(0, 8) })
1305
+ ] }),
1306
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: "Thanks for the report \u2014 we'll look into it." }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1308
+ "button",
1309
+ {
1310
+ onClick: handleClose,
1311
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
1312
+ children: "Done"
1313
+ }
1314
+ )
1315
+ ] }) : modalState === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
1316
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "mb-3 h-12 w-12 text-red-500" }),
1317
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
1318
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: errorMessage || "Something went wrong. Please try again." }),
1319
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1320
+ "button",
1321
+ {
1322
+ onClick: () => setModalState("chatting"),
1323
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
1324
+ children: "Try Again"
1325
+ }
1326
+ )
1327
+ ] }) : modalState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8", children: [
1328
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
1329
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
1330
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1331
+ (captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
1332
+ messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1333
+ "div",
1334
+ {
1335
+ className: (0, import_widget_shared5.cn)(
1336
+ "text-sm leading-relaxed",
1337
+ msg.role === "assistant" ? "rounded-lg bg-gray-100/50 p-3 text-gray-900 dark:bg-gray-800/50 dark:text-gray-100" : "ml-8 rounded-lg bg-indigo-600 p-3 text-white"
1338
+ ),
1339
+ children: msg.content
1340
+ },
1341
+ i
1342
+ )),
1343
+ isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
1344
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
1345
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
1346
+ ] })
1347
+ ] })
1348
+ }
1349
+ ),
1350
+ modalState === "chatting" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
1351
+ submitAlert && // onBeforeSubmit rejected the payload — keep the draft and
1352
+ // let the user retry. role="alert" announces to AT users.
1353
+ // See Issue B8.
1097
1354
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1098
- "button",
1355
+ "p",
1099
1356
  {
1100
- onClick: () => setModalState("chatting"),
1101
- className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
1102
- children: "Try Again"
1357
+ role: "alert",
1358
+ className: "rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300",
1359
+ children: submitAlert
1103
1360
  }
1104
- )
1105
- ] }) : modalState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8", children: [
1106
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
1107
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
1108
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
1109
- (captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
1110
- messages.map((msg, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1111
- "div",
1112
- {
1113
- className: cn(
1114
- "text-sm leading-relaxed",
1115
- msg.role === "assistant" ? "rounded-lg bg-gray-100/50 p-3 text-gray-900 dark:bg-gray-800/50 dark:text-gray-100" : "ml-8 rounded-lg bg-indigo-600 p-3 text-white"
1116
- ),
1117
- children: msg.content
1118
- },
1119
- i
1120
- )),
1121
- isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
1122
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
1123
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
1124
- ] }),
1125
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: chatEndRef })
1126
- ] }) }),
1127
- modalState === "chatting" && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
1361
+ ),
1128
1362
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1129
1363
  "textarea",
1130
1364
  {
@@ -1181,46 +1415,67 @@ function ReportModal({
1181
1415
  );
1182
1416
  }
1183
1417
 
1184
- // src/use-has-mounted.ts
1185
- var import_react4 = require("react");
1186
- function useHasMounted() {
1187
- const [mounted, setMounted] = (0, import_react4.useState)(false);
1188
- (0, import_react4.useEffect)(() => {
1189
- setMounted(true);
1190
- }, []);
1191
- return mounted;
1192
- }
1193
-
1194
1418
  // src/bug-reporter.tsx
1419
+ var import_widget_shared6 = require("@jarve/widget-shared");
1195
1420
  var import_jsx_runtime4 = require("react/jsx-runtime");
1421
+ function isValidApiUrl(raw) {
1422
+ if (typeof raw !== "string" || raw.length === 0) return false;
1423
+ try {
1424
+ const parsed = new URL(raw);
1425
+ if (parsed.protocol === "https:") return true;
1426
+ if (parsed.protocol === "http:" && parsed.hostname === "localhost") return true;
1427
+ return false;
1428
+ } catch (e) {
1429
+ return false;
1430
+ }
1431
+ }
1196
1432
  function JarveBugReporter({
1197
1433
  apiUrl,
1198
1434
  apiKey,
1435
+ siteId,
1199
1436
  user,
1200
1437
  buttonPosition,
1438
+ zIndexBase,
1439
+ captureResponseBodies = false,
1440
+ theme,
1441
+ onBeforeSubmit,
1201
1442
  children
1202
1443
  }) {
1203
- const safeApiKey = apiKey || "";
1204
- const mounted = useHasMounted();
1205
- const [captureMode, setCaptureMode] = (0, import_react5.useState)(false);
1206
- const [captureResult, setCaptureResult] = (0, import_react5.useState)(null);
1207
- const [showModal, setShowModal] = (0, import_react5.useState)(false);
1208
- (0, import_react5.useEffect)(() => {
1444
+ const mounted = (0, import_widget_shared6.useHasMounted)();
1445
+ const apiUrlValid = (0, import_react4.useMemo)(() => isValidApiUrl(apiUrl), [apiUrl]);
1446
+ (0, import_react4.useEffect)(() => {
1447
+ if (!apiUrlValid) {
1448
+ console.error(
1449
+ `Jarve bug reporter: invalid apiUrl \u2014 widget disabled (received: ${JSON.stringify(apiUrl)})`
1450
+ );
1451
+ }
1452
+ }, [apiUrl, apiUrlValid]);
1453
+ const [captureMode, setCaptureMode] = (0, import_react4.useState)(false);
1454
+ const [captureResult, setCaptureResult] = (0, import_react4.useState)(null);
1455
+ const [showModal, setShowModal] = (0, import_react4.useState)(false);
1456
+ const [isPrimary, setIsPrimary] = (0, import_react4.useState)(false);
1457
+ (0, import_react4.useEffect)(() => {
1458
+ if (!apiUrlValid || !isPrimary) return;
1209
1459
  startCapturing();
1210
- startNetworkCapture();
1460
+ startNetworkCapture({ captureResponseBodies });
1211
1461
  return () => {
1212
1462
  stopCapturing();
1213
1463
  stopNetworkCapture();
1214
1464
  };
1215
- }, []);
1216
- const toggleCaptureMode = (0, import_react5.useCallback)(() => {
1465
+ }, [apiUrlValid, isPrimary, captureResponseBodies]);
1466
+ const toggleCaptureMode = (0, import_react4.useCallback)(() => {
1217
1467
  setCaptureMode((prev) => !prev);
1218
1468
  }, []);
1219
- (0, import_react5.useEffect)(() => {
1469
+ (0, import_react4.useEffect)(() => {
1220
1470
  if (typeof window === "undefined") return;
1471
+ if (!apiUrlValid) return;
1221
1472
  if (!window.__jarve_widgets) {
1222
1473
  window.__jarve_widgets = /* @__PURE__ */ new Map();
1223
1474
  }
1475
+ if (window.__jarve_widgets.has("bug-reporter")) {
1476
+ console.warn("JarveBugReporter: instance already mounted; additional instance ignored");
1477
+ return;
1478
+ }
1224
1479
  window.__jarve_widgets.set("bug-reporter", {
1225
1480
  type: "bug-reporter",
1226
1481
  label: "Report a Bug",
@@ -1230,54 +1485,63 @@ function JarveBugReporter({
1230
1485
  isActive: false,
1231
1486
  trigger: toggleCaptureMode
1232
1487
  });
1233
- window.dispatchEvent(
1234
- new CustomEvent("jarve:widget-registered", { detail: { type: "bug-reporter" } })
1235
- );
1488
+ setIsPrimary(true);
1489
+ (0, import_widget_shared6.emit)("jarve:widget-registered", { type: "bug-reporter" });
1236
1490
  return () => {
1237
1491
  var _a;
1238
1492
  (_a = window.__jarve_widgets) == null ? void 0 : _a.delete("bug-reporter");
1239
- window.dispatchEvent(
1240
- new CustomEvent("jarve:widget-deregistered", { detail: { type: "bug-reporter" } })
1241
- );
1493
+ setIsPrimary(false);
1494
+ (0, import_widget_shared6.emit)("jarve:widget-deregistered", { type: "bug-reporter" });
1242
1495
  };
1243
- }, [toggleCaptureMode]);
1244
- (0, import_react5.useEffect)(() => {
1496
+ }, [toggleCaptureMode, apiUrlValid]);
1497
+ (0, import_react4.useEffect)(() => {
1245
1498
  var _a;
1246
1499
  if (typeof window === "undefined") return;
1500
+ if (!isPrimary) return;
1247
1501
  const entry = (_a = window.__jarve_widgets) == null ? void 0 : _a.get("bug-reporter");
1248
1502
  if (entry) {
1249
1503
  entry.isActive = captureMode || showModal;
1250
- window.dispatchEvent(
1251
- new CustomEvent("jarve:state-change", { detail: { type: "bug-reporter" } })
1252
- );
1504
+ (0, import_widget_shared6.emit)("jarve:state-change", { type: "bug-reporter", isActive: entry.isActive });
1253
1505
  }
1254
- }, [captureMode, showModal]);
1255
- const handleCapture = (0, import_react5.useCallback)((result) => {
1506
+ }, [captureMode, showModal, isPrimary]);
1507
+ const handleCapture = (0, import_react4.useCallback)((result) => {
1256
1508
  setCaptureResult(result);
1257
1509
  setCaptureMode(false);
1258
1510
  setShowModal(true);
1259
1511
  }, []);
1260
- const handleCancelCapture = (0, import_react5.useCallback)(() => {
1512
+ const handleCancelCapture = (0, import_react4.useCallback)(() => {
1261
1513
  setCaptureMode(false);
1262
1514
  }, []);
1263
- const handleCloseModal = (0, import_react5.useCallback)(() => {
1515
+ const handleCloseModal = (0, import_react4.useCallback)(() => {
1516
+ setCaptureMode(false);
1264
1517
  setShowModal(false);
1265
1518
  setCaptureResult(null);
1266
1519
  clearCapturedErrors();
1267
1520
  clearCapturedNetworkErrors();
1268
1521
  }, []);
1269
- const siteId = safeApiKey.startsWith("brk_") ? safeApiKey.slice(4, 12) : "external";
1522
+ (0, import_react4.useEffect)(() => {
1523
+ if (typeof window === "undefined") return;
1524
+ if (!isPrimary) return;
1525
+ window.addEventListener("jarve:close-bug-modal", handleCloseModal);
1526
+ return () => {
1527
+ window.removeEventListener("jarve:close-bug-modal", handleCloseModal);
1528
+ };
1529
+ }, [isPrimary, handleCloseModal]);
1270
1530
  const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
1271
1531
  const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
1532
+ if (!apiUrlValid) {
1533
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children });
1534
+ }
1272
1535
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1273
1536
  children,
1274
- mounted && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1537
+ mounted && isPrimary && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1275
1538
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1276
1539
  FloatingButton,
1277
1540
  {
1278
1541
  isActive: captureMode,
1279
1542
  onClick: toggleCaptureMode,
1280
- position: buttonPosition
1543
+ position: buttonPosition,
1544
+ zIndexBase
1281
1545
  }
1282
1546
  ),
1283
1547
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1288,7 +1552,8 @@ function JarveBugReporter({
1288
1552
  reporterName,
1289
1553
  reporterEmail,
1290
1554
  onCapture: handleCapture,
1291
- onCancel: handleCancelCapture
1555
+ onCancel: handleCancelCapture,
1556
+ zIndexBase
1292
1557
  }
1293
1558
  ),
1294
1559
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -1299,7 +1564,10 @@ function JarveBugReporter({
1299
1564
  apiConfig: { apiUrl, apiKey },
1300
1565
  siteId,
1301
1566
  user: { name: reporterName, email: reporterEmail },
1302
- onClose: handleCloseModal
1567
+ onClose: handleCloseModal,
1568
+ zIndexBase,
1569
+ theme,
1570
+ onBeforeSubmit
1303
1571
  }
1304
1572
  )
1305
1573
  ] })
@@ -1309,4 +1577,3 @@ function JarveBugReporter({
1309
1577
  0 && (module.exports = {
1310
1578
  JarveBugReporter
1311
1579
  });
1312
- //# sourceMappingURL=index.js.map