@jarve/bug-reporter 0.1.1 → 0.3.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
@@ -57,16 +57,21 @@ function cn(...inputs) {
57
57
 
58
58
  // src/floating-button.tsx
59
59
  var import_jsx_runtime = require("react/jsx-runtime");
60
- function FloatingButton({ isActive, onClick }) {
60
+ function FloatingButton({ isActive, onClick, position = "right" }) {
61
+ const sideClasses = position === "left" ? "left-4 md:left-6" : "right-4 md:right-6";
61
62
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
62
63
  "button",
63
64
  {
64
65
  onClick,
65
66
  className: cn(
66
- "fixed bottom-6 right-6 z-[9999] flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-all duration-200",
67
- "hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2",
68
- isActive ? "bg-red-500 text-white animate-pulse focus:ring-red-400" : "bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-400",
69
- "bottom-4 right-4 h-10 w-10 md:bottom-6 md:right-6 md:h-12 md:w-12"
67
+ "fixed z-[9999] flex items-center justify-center rounded-full shadow-lg transition-all duration-200",
68
+ "hover:scale-110 focus:ring-2 focus:ring-offset-2 focus:outline-none",
69
+ // size + vertical position
70
+ "bottom-4 h-11 w-11 md:bottom-6 md:h-12 md:w-12",
71
+ // horizontal side
72
+ sideClasses,
73
+ // active vs idle colors
74
+ 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"
70
75
  ),
71
76
  title: isActive ? "Cancel bug capture" : "Report a bug",
72
77
  "aria-label": isActive ? "Cancel bug capture" : "Report a bug",
@@ -134,7 +139,18 @@ function buildSelectorPath(element, stopAt) {
134
139
  }
135
140
  return parts.join(" > ");
136
141
  }
137
- function collectElementInfo(target, section, event) {
142
+ function isTouchCapable() {
143
+ return "ontouchstart" in window || navigator.maxTouchPoints > 0;
144
+ }
145
+ function extractCoordinates(e) {
146
+ return {
147
+ pageX: e.pageX,
148
+ pageY: e.pageY,
149
+ clientX: e.clientX,
150
+ clientY: e.clientY
151
+ };
152
+ }
153
+ function collectElementInfo(target, section, coords) {
138
154
  const sectionRect = section.getBoundingClientRect();
139
155
  const dataAttributes = {};
140
156
  for (const attr of Array.from(target.attributes)) {
@@ -150,10 +166,10 @@ function collectElementInfo(target, section, event) {
150
166
  ariaLabel: target.getAttribute("aria-label") || null,
151
167
  dataAttributes,
152
168
  selectorPath: buildSelectorPath(target, section),
153
- clickX: event.pageX,
154
- clickY: event.pageY,
155
- relativeClickX: event.clientX - sectionRect.left,
156
- relativeClickY: event.clientY - sectionRect.top
169
+ clickX: coords.pageX,
170
+ clickY: coords.pageY,
171
+ relativeClickX: coords.clientX - sectionRect.left,
172
+ relativeClickY: coords.clientY - sectionRect.top
157
173
  };
158
174
  }
159
175
  function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
@@ -185,8 +201,7 @@ var errorListener = null;
185
201
  var rejectionListener = null;
186
202
  function startCapturing() {
187
203
  if (isCapturing) return;
188
- if (console.error.__bugReporterPatched)
189
- return;
204
+ if (console.error.__bugReporterPatched) return;
190
205
  isCapturing = true;
191
206
  capturedErrors = [];
192
207
  originalConsoleError = console.error;
@@ -378,6 +393,15 @@ function clearCapturedNetworkErrors() {
378
393
 
379
394
  // src/capture-overlay.tsx
380
395
  var import_jsx_runtime2 = require("react/jsx-runtime");
396
+ function dataUrlToBlob(dataUrl) {
397
+ var _a;
398
+ const [header, base64] = dataUrl.split(",");
399
+ const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
400
+ const bytes = atob(base64);
401
+ const arr = new Uint8Array(bytes.length);
402
+ for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
403
+ return new Blob([arr], { type: mime });
404
+ }
381
405
  function CaptureOverlay({
382
406
  isActive,
383
407
  siteId,
@@ -389,48 +413,30 @@ function CaptureOverlay({
389
413
  const [hoveredElement, setHoveredElement] = (0, import_react.useState)(null);
390
414
  const [hoveredRect, setHoveredRect] = (0, import_react.useState)(null);
391
415
  const [isCapturing3, setIsCapturing] = (0, import_react.useState)(false);
416
+ const [isTouchMode, setIsTouchMode] = (0, import_react.useState)(false);
417
+ const [selectedSection, setSelectedSection] = (0, import_react.useState)(null);
418
+ const [selectedRect, setSelectedRect] = (0, import_react.useState)(null);
419
+ const [selectedTarget, setSelectedTarget] = (0, import_react.useState)(null);
392
420
  const overlayRef = (0, import_react.useRef)(null);
393
421
  const hoveredElementRef = (0, import_react.useRef)(null);
394
422
  const rafRef = (0, import_react.useRef)(null);
395
- const handleMouseMove = (0, import_react.useCallback)(
396
- (e) => {
397
- if (!isActive || isCapturing3) return;
398
- if (rafRef.current) return;
399
- rafRef.current = requestAnimationFrame(() => {
400
- rafRef.current = null;
401
- const target = e.target;
402
- if (!(target instanceof HTMLElement)) return;
403
- if (target.closest("[data-bug-reporter]")) {
404
- setHoveredElement(null);
405
- setHoveredRect(null);
406
- hoveredElementRef.current = null;
407
- return;
408
- }
409
- const section = getNearestSection(target);
410
- setHoveredElement(section);
411
- hoveredElementRef.current = section;
412
- setHoveredRect(section ? section.getBoundingClientRect() : null);
413
- });
414
- },
415
- [isActive, isCapturing3]
416
- );
417
- const handleClick = (0, import_react.useCallback)(
418
- async (e) => {
419
- var _a, _b;
420
- if (!isActive || isCapturing3) return;
421
- const target = e.target;
422
- if (!(target instanceof HTMLElement)) return;
423
- if (target.closest("[data-bug-reporter]")) return;
424
- e.preventDefault();
425
- e.stopPropagation();
426
- const section = getNearestSection(target);
427
- if (!section) return;
428
- const elementInfo = collectElementInfo(target, section, e);
423
+ const touchCoordsRef = (0, import_react.useRef)(null);
424
+ (0, import_react.useEffect)(() => {
425
+ if (isActive) {
426
+ setIsTouchMode(isTouchCapable());
427
+ }
428
+ }, [isActive]);
429
+ const captureScreenshot = (0, import_react.useCallback)(
430
+ async (section, target, coords) => {
431
+ const elementInfo = collectElementInfo(target, section, coords);
429
432
  setIsCapturing(true);
430
433
  try {
431
434
  setHoveredElement(null);
432
435
  setHoveredRect(null);
433
436
  hoveredElementRef.current = null;
437
+ setSelectedSection(null);
438
+ setSelectedRect(null);
439
+ setSelectedTarget(null);
434
440
  await new Promise((r) => setTimeout(r, 50));
435
441
  const MAX_DIMENSION = 2e3;
436
442
  const sectionRect = section.getBoundingClientRect();
@@ -440,12 +446,7 @@ function CaptureOverlay({
440
446
  pixelRatio,
441
447
  skipFonts: true
442
448
  });
443
- const [header, base64] = dataUrl.split(",");
444
- const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
445
- const bytes = atob(base64);
446
- const arr = new Uint8Array(bytes.length);
447
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
448
- const blob = new Blob([arr], { type: mime });
449
+ const blob = dataUrlToBlob(dataUrl);
449
450
  const metadata = collectMetadata(
450
451
  section,
451
452
  siteId,
@@ -457,7 +458,10 @@ function CaptureOverlay({
457
458
  const networkErrors = getCapturedNetworkErrors();
458
459
  onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
459
460
  } catch (err) {
460
- console.warn("Bug reporter: first capture attempt failed, retrying with simpler settings", err);
461
+ console.warn(
462
+ "Bug reporter: first capture attempt failed, retrying with simpler settings",
463
+ err
464
+ );
461
465
  try {
462
466
  const dataUrl = await (0, import_html_to_image.toPng)(section, {
463
467
  quality: 0.6,
@@ -465,12 +469,7 @@ function CaptureOverlay({
465
469
  skipFonts: true,
466
470
  cacheBust: true
467
471
  });
468
- const [header, base64] = dataUrl.split(",");
469
- const mime = ((_b = header.match(/:(.*?);/)) == null ? void 0 : _b[1]) || "image/png";
470
- const bytes = atob(base64);
471
- const arr = new Uint8Array(bytes.length);
472
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
473
- const retryBlob = new Blob([arr], { type: mime });
472
+ const retryBlob = dataUrlToBlob(dataUrl);
474
473
  const metadata = collectMetadata(
475
474
  section,
476
475
  siteId,
@@ -481,7 +480,7 @@ function CaptureOverlay({
481
480
  const consoleErrors = getCapturedErrors();
482
481
  const networkErrors = getCapturedNetworkErrors();
483
482
  onCapture({ screenshot: retryBlob, metadata, consoleErrors, networkErrors });
484
- } catch (e2) {
483
+ } catch (e) {
485
484
  console.error("Bug reporter: screenshot capture failed after retry");
486
485
  const metadata = collectMetadata(
487
486
  section,
@@ -503,7 +502,75 @@ function CaptureOverlay({
503
502
  setIsCapturing(false);
504
503
  }
505
504
  },
506
- [isActive, isCapturing3, siteId, reporterName, reporterEmail, onCapture]
505
+ [siteId, reporterName, reporterEmail, onCapture]
506
+ );
507
+ const handleMouseMove = (0, import_react.useCallback)(
508
+ (e) => {
509
+ if (!isActive || isCapturing3 || isTouchMode) return;
510
+ if (rafRef.current) return;
511
+ rafRef.current = requestAnimationFrame(() => {
512
+ rafRef.current = null;
513
+ const target = e.target;
514
+ if (!(target instanceof HTMLElement)) return;
515
+ if (target.closest("[data-bug-reporter]")) {
516
+ setHoveredElement(null);
517
+ setHoveredRect(null);
518
+ hoveredElementRef.current = null;
519
+ return;
520
+ }
521
+ const section = getNearestSection(target);
522
+ setHoveredElement(section);
523
+ hoveredElementRef.current = section;
524
+ setHoveredRect(section ? section.getBoundingClientRect() : null);
525
+ });
526
+ },
527
+ [isActive, isCapturing3, isTouchMode]
528
+ );
529
+ const handleClick = (0, import_react.useCallback)(
530
+ async (e) => {
531
+ if (!isActive || isCapturing3 || isTouchMode) return;
532
+ const target = e.target;
533
+ if (!(target instanceof HTMLElement)) return;
534
+ if (target.closest("[data-bug-reporter]")) return;
535
+ e.preventDefault();
536
+ e.stopPropagation();
537
+ const section = getNearestSection(target);
538
+ if (!section) return;
539
+ await captureScreenshot(section, target, extractCoordinates(e));
540
+ },
541
+ [isActive, isCapturing3, isTouchMode, captureScreenshot]
542
+ );
543
+ const handleTouchEnd = (0, import_react.useCallback)(
544
+ (e) => {
545
+ if (!isActive || isCapturing3) return;
546
+ const touch = e.changedTouches[0];
547
+ if (!touch) return;
548
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
549
+ if (!(target instanceof HTMLElement)) return;
550
+ if (target.closest("[data-bug-reporter]")) return;
551
+ const section = getNearestSection(target);
552
+ if (!section) return;
553
+ setSelectedSection(section);
554
+ setSelectedRect(section.getBoundingClientRect());
555
+ setSelectedTarget(target);
556
+ touchCoordsRef.current = extractCoordinates(touch);
557
+ },
558
+ [isActive, isCapturing3]
559
+ );
560
+ const handleConfirmCapture = (0, import_react.useCallback)(async () => {
561
+ if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
562
+ await captureScreenshot(selectedSection, selectedTarget, touchCoordsRef.current);
563
+ }, [selectedSection, selectedTarget, captureScreenshot]);
564
+ const handlePointerDown = (0, import_react.useCallback)(
565
+ (e) => {
566
+ if (!isActive) return;
567
+ if (e.pointerType === "touch") {
568
+ setIsTouchMode(true);
569
+ } else if (e.pointerType === "mouse") {
570
+ setIsTouchMode(false);
571
+ }
572
+ },
573
+ [isActive]
507
574
  );
508
575
  const handleKeyDown = (0, import_react.useCallback)(
509
576
  (e) => {
@@ -516,60 +583,131 @@ function CaptureOverlay({
516
583
  [isActive, onCancel]
517
584
  );
518
585
  const handleScroll = (0, import_react.useCallback)(() => {
519
- if (!hoveredElementRef.current) return;
520
- setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
521
- }, []);
586
+ if (hoveredElementRef.current) {
587
+ setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
588
+ }
589
+ if (selectedSection) {
590
+ setSelectedRect(selectedSection.getBoundingClientRect());
591
+ }
592
+ }, [selectedSection]);
522
593
  (0, import_react.useEffect)(() => {
523
594
  if (!isActive) {
524
595
  setHoveredElement(null);
525
596
  setHoveredRect(null);
526
597
  hoveredElementRef.current = null;
598
+ setSelectedSection(null);
599
+ setSelectedRect(null);
600
+ setSelectedTarget(null);
601
+ touchCoordsRef.current = null;
527
602
  return;
528
603
  }
529
- document.addEventListener("mousemove", handleMouseMove, true);
530
- document.addEventListener("click", handleClick, true);
604
+ document.addEventListener("pointerdown", handlePointerDown);
531
605
  document.addEventListener("keydown", handleKeyDown);
532
606
  window.addEventListener("scroll", handleScroll, { passive: true });
607
+ if (isTouchMode) {
608
+ document.addEventListener("touchend", handleTouchEnd, { passive: true });
609
+ } else {
610
+ document.addEventListener("mousemove", handleMouseMove, true);
611
+ document.addEventListener("click", handleClick, true);
612
+ }
533
613
  return () => {
534
- document.removeEventListener("mousemove", handleMouseMove, true);
535
- document.removeEventListener("click", handleClick, true);
614
+ document.removeEventListener("pointerdown", handlePointerDown);
536
615
  document.removeEventListener("keydown", handleKeyDown);
537
616
  window.removeEventListener("scroll", handleScroll);
617
+ document.removeEventListener("touchend", handleTouchEnd);
618
+ document.removeEventListener("mousemove", handleMouseMove, true);
619
+ document.removeEventListener("click", handleClick, true);
538
620
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
539
621
  };
540
- }, [isActive, handleMouseMove, handleClick, handleKeyDown, handleScroll]);
541
- if (!isActive || !hoveredElement || !hoveredRect) return null;
622
+ }, [
623
+ isActive,
624
+ isTouchMode,
625
+ handleMouseMove,
626
+ handleClick,
627
+ handleTouchEnd,
628
+ handlePointerDown,
629
+ handleKeyDown,
630
+ handleScroll
631
+ ]);
632
+ const highlightRect = isTouchMode ? selectedRect : hoveredRect;
633
+ const showHighlight = isTouchMode ? !!selectedSection : !!hoveredElement && !!hoveredRect;
634
+ if (!isActive) return null;
542
635
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
543
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
636
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
544
637
  "div",
545
638
  {
546
639
  "data-bug-reporter": true,
547
640
  role: "alert",
548
641
  "aria-live": "assertive",
549
- className: "fixed top-0 left-0 right-0 z-[10000] bg-indigo-600 text-white text-center py-2 px-4 text-sm font-medium",
550
- children: [
551
- "Click on the section with the bug. Press ",
552
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-1.5 py-0.5 bg-indigo-800 rounded text-xs mx-1", children: "Esc" }),
642
+ 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",
643
+ children: isTouchMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
644
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Tap the section with the bug" }),
645
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
646
+ "button",
647
+ {
648
+ onClick: onCancel,
649
+ className: "min-h-[44px] rounded-md bg-white/20 px-3 py-1 text-sm font-medium",
650
+ children: "Cancel"
651
+ }
652
+ )
653
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
654
+ "Click on the section with the bug. Press",
655
+ " ",
656
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "mx-1 rounded bg-indigo-800 px-1.5 py-0.5 text-xs", children: "Esc" }),
553
657
  " to cancel."
554
- ]
658
+ ] })
555
659
  }
556
660
  ),
557
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
661
+ showHighlight && highlightRect && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
558
662
  "div",
559
663
  {
560
664
  ref: overlayRef,
561
665
  "data-bug-reporter": true,
562
- className: "fixed pointer-events-none z-[9998] border-2 border-indigo-500 rounded-sm transition-all duration-150 ease-out",
666
+ className: "pointer-events-none fixed z-[9998] rounded-sm border-2 border-indigo-500 transition-all duration-150 ease-out",
563
667
  style: {
564
- top: hoveredRect.top - 2,
565
- left: hoveredRect.left - 2,
566
- width: hoveredRect.width + 4,
567
- height: hoveredRect.height + 4,
668
+ top: highlightRect.top - 2,
669
+ left: highlightRect.left - 2,
670
+ width: highlightRect.width + 4,
671
+ height: highlightRect.height + 4,
568
672
  backgroundColor: "rgba(99, 102, 241, 0.08)"
569
673
  }
570
674
  }
571
675
  ),
572
- isActive && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
676
+ isTouchMode && selectedSection && !isCapturing3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
677
+ "div",
678
+ {
679
+ "data-bug-reporter": true,
680
+ className: "fixed right-0 bottom-0 left-0 z-[10000] border-t border-gray-200 bg-white shadow-lg",
681
+ style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
682
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
683
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "truncate text-sm font-medium text-gray-900", children: "Capture this section?" }),
684
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex shrink-0 gap-2", children: [
685
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
686
+ "button",
687
+ {
688
+ onClick: () => {
689
+ setSelectedSection(null);
690
+ setSelectedRect(null);
691
+ setSelectedTarget(null);
692
+ touchCoordsRef.current = null;
693
+ },
694
+ className: "min-h-[44px] rounded-md border border-gray-300 px-4 text-sm font-medium text-gray-700",
695
+ children: "Cancel"
696
+ }
697
+ ),
698
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
699
+ "button",
700
+ {
701
+ onClick: handleConfirmCapture,
702
+ className: "min-h-[44px] rounded-md bg-indigo-600 px-4 text-sm font-medium text-white",
703
+ children: "Capture"
704
+ }
705
+ )
706
+ ] })
707
+ ] })
708
+ }
709
+ ),
710
+ !isTouchMode && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
573
711
  ] });
574
712
  }
575
713
 
@@ -635,7 +773,9 @@ function ReportModal({
635
773
  })
636
774
  });
637
775
  if (response.status === 401) {
638
- console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
776
+ console.error(
777
+ "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
778
+ );
639
779
  setMessages([
640
780
  {
641
781
  role: "assistant",
@@ -732,9 +872,7 @@ function ReportModal({
732
872
  setModalState("submitted");
733
873
  } catch (err) {
734
874
  console.error("Bug reporter: failed to submit report", err);
735
- setErrorMessage(
736
- err instanceof Error ? err.message : "Failed to submit report"
737
- );
875
+ setErrorMessage(err instanceof Error ? err.message : "Failed to submit report");
738
876
  setModalState("error");
739
877
  }
740
878
  },
@@ -747,10 +885,7 @@ function ReportModal({
747
885
  if (!input.trim() || isLoading || !captureResult) return;
748
886
  const userMessage = input.trim();
749
887
  setInput("");
750
- const newMessages = [
751
- ...messages,
752
- { role: "user", content: userMessage }
753
- ];
888
+ const newMessages = [...messages, { role: "user", content: userMessage }];
754
889
  setMessages(newMessages);
755
890
  setIsLoading(true);
756
891
  try {
@@ -766,7 +901,9 @@ function ReportModal({
766
901
  })
767
902
  });
768
903
  if (response.status === 401) {
769
- console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
904
+ console.error(
905
+ "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
906
+ );
770
907
  setMessages([
771
908
  ...newMessages,
772
909
  {
@@ -778,10 +915,7 @@ function ReportModal({
778
915
  }
779
916
  if (!response.ok) throw new Error("Failed to get AI response");
780
917
  const data = await response.json();
781
- setMessages([
782
- ...newMessages,
783
- { role: "assistant", content: data.message }
784
- ]);
918
+ setMessages([...newMessages, { role: "assistant", content: data.message }]);
785
919
  if (data.readyToSubmit && data.structuredReport) {
786
920
  await submitReport(
787
921
  [...newMessages, { role: "assistant", content: data.message }],
@@ -814,7 +948,7 @@ function ReportModal({
814
948
  onClose();
815
949
  }
816
950
  function handleKeyDown(e) {
817
- if (e.key === "Enter" && !e.shiftKey) {
951
+ if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
818
952
  e.preventDefault();
819
953
  sendMessage();
820
954
  }
@@ -832,65 +966,66 @@ function ReportModal({
832
966
  "div",
833
967
  {
834
968
  className: cn(
835
- "bg-white dark:bg-gray-950 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-800 flex flex-col overflow-hidden",
836
- "w-full max-w-lg mx-4",
837
- "max-[768px]:mx-0 max-[768px]:rounded-none max-[768px]:max-w-none max-[768px]:h-full",
969
+ "flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-950",
970
+ "mx-4 w-full max-w-lg",
971
+ "max-[768px]:mx-0 max-[768px]:h-full max-[768px]:max-w-none max-[768px]:rounded-none",
838
972
  "min-[769px]:max-h-[85vh]"
839
973
  ),
840
974
  children: [
841
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center justify-between px-4 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/30 dark:bg-gray-900/30", children: [
975
+ /* @__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: [
842
976
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
843
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "font-semibold text-sm text-gray-900 dark:text-gray-100", children: "Bug Report" }),
977
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Bug Report" }),
844
978
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
845
979
  ] }),
846
980
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
847
981
  "button",
848
982
  {
849
983
  onClick: handleClose,
850
- className: "p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
984
+ className: "rounded-md p-1.5 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800",
851
985
  "aria-label": "Close",
852
986
  children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "h-4 w-4 text-gray-600 dark:text-gray-400" })
853
987
  }
854
988
  )
855
989
  ] }),
856
- screenshotUrl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "px-4 py-3 border-b border-gray-200 dark:border-gray-800 bg-gray-50/10 dark:bg-gray-900/10", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
990
+ screenshotUrl && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "border-b border-gray-200 bg-gray-50/10 px-4 py-3 dark:border-gray-800 dark:bg-gray-900/10", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
857
991
  "img",
858
992
  {
859
993
  src: screenshotUrl,
860
994
  alt: "Captured section",
861
- className: "w-full max-h-40 object-contain rounded-md border border-gray-200 dark:border-gray-700"
995
+ className: "max-h-40 w-full rounded-md border border-gray-200 object-contain dark:border-gray-700"
862
996
  }
863
997
  ) }),
864
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-0", children: modalState === "submitted" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
865
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.CheckCircle2, { className: "h-12 w-12 text-green-500 mb-3" }),
866
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
867
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [
868
- "Reference: ",
869
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("code", { className: "text-xs bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded", children: reportId == null ? void 0 : reportId.slice(0, 8) })
998
+ /* @__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: [
999
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
1000
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
1001
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
1002
+ "Reference:",
1003
+ " ",
1004
+ /* @__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) })
870
1005
  ] }),
871
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-2", children: "Thanks for the report \u2014 we'll look into it." }),
1006
+ /* @__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." }),
872
1007
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
873
1008
  "button",
874
1009
  {
875
1010
  onClick: handleClose,
876
- className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
1011
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
877
1012
  children: "Done"
878
1013
  }
879
1014
  )
880
1015
  ] }) : modalState === "error" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
881
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "h-12 w-12 text-red-500 mb-3" }),
882
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
883
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: errorMessage || "Something went wrong. Please try again." }),
1016
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.X, { className: "mb-3 h-12 w-12 text-red-500" }),
1017
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
1018
+ /* @__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." }),
884
1019
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
885
1020
  "button",
886
1021
  {
887
1022
  onClick: () => setModalState("chatting"),
888
- className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
1023
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
889
1024
  children: "Try Again"
890
1025
  }
891
1026
  )
892
1027
  ] }) : modalState === "submitting" ? /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col items-center justify-center py-8", children: [
893
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-8 w-8 animate-spin text-indigo-500 mb-3" }),
1028
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
894
1029
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
895
1030
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
896
1031
  (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." }),
@@ -899,42 +1034,53 @@ function ReportModal({
899
1034
  {
900
1035
  className: cn(
901
1036
  "text-sm leading-relaxed",
902
- msg.role === "assistant" ? "bg-gray-100/50 dark:bg-gray-800/50 rounded-lg p-3 text-gray-900 dark:text-gray-100" : "bg-indigo-600 text-white rounded-lg p-3 ml-8"
1037
+ 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"
903
1038
  ),
904
1039
  children: msg.content
905
1040
  },
906
1041
  i
907
1042
  )),
908
- isLoading && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "bg-gray-100/50 dark:bg-gray-800/50 rounded-lg p-3 flex items-center gap-2", children: [
1043
+ 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: [
909
1044
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
910
1045
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
911
1046
  ] }),
912
1047
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { ref: chatEndRef })
913
1048
  ] }) }),
914
- modalState === "chatting" && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "border-t border-gray-200 dark:border-gray-800 px-4 py-3", children: [
915
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex gap-2", children: [
916
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
917
- "textarea",
918
- {
919
- ref: inputRef,
920
- value: input,
921
- onChange: (e) => setInput(e.target.value),
922
- onKeyDown: handleKeyDown,
923
- placeholder: "Describe what's wrong...",
924
- rows: 2,
925
- className: "flex-1 resize-none rounded-md border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-950 px-3 py-2 text-sm text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-indigo-400 focus:border-transparent",
926
- disabled: isLoading
927
- }
928
- ),
929
- /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-1", children: [
930
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1049
+ 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: [
1050
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1051
+ "textarea",
1052
+ {
1053
+ ref: inputRef,
1054
+ value: input,
1055
+ onChange: (e) => setInput(e.target.value),
1056
+ onKeyDown: handleKeyDown,
1057
+ placeholder: "Describe what's wrong...",
1058
+ rows: 2,
1059
+ className: "w-full resize-none rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 focus:border-transparent focus:ring-2 focus:ring-indigo-400 focus:outline-none dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100",
1060
+ disabled: isLoading
1061
+ }
1062
+ ),
1063
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex flex-col gap-2", children: [
1064
+ captureResult && (captureResult.consoleErrors.length > 0 || captureResult.networkErrors.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "flex-1 text-xs text-amber-600", children: [
1065
+ [
1066
+ captureResult.consoleErrors.length > 0 ? `${captureResult.consoleErrors.length} console error${captureResult.consoleErrors.length !== 1 ? "s" : ""}` : null,
1067
+ captureResult.networkErrors.length > 0 ? `${captureResult.networkErrors.length} failed request${captureResult.networkErrors.length !== 1 ? "s" : ""}` : null
1068
+ ].filter(Boolean).join(" + "),
1069
+ " ",
1070
+ "captured \u2014 these will be included in the report."
1071
+ ] }),
1072
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex justify-end gap-2", children: [
1073
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
931
1074
  "button",
932
1075
  {
933
1076
  onClick: sendMessage,
934
1077
  disabled: !input.trim() || isLoading,
935
- className: "p-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
1078
+ className: "flex items-center gap-1 rounded-md bg-indigo-600 px-3 py-2 text-nowrap text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50",
936
1079
  title: "Send message",
937
- children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Send, { className: "h-4 w-4" })
1080
+ children: [
1081
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Send, { className: "h-4 w-4" }),
1082
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm font-medium", children: "Send message" })
1083
+ ]
938
1084
  }
939
1085
  ),
940
1086
  messages.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
@@ -942,22 +1088,14 @@ function ReportModal({
942
1088
  {
943
1089
  onClick: handleManualSubmit,
944
1090
  disabled: isLoading,
945
- className: "px-2 py-1 rounded-md bg-green-600 text-white hover:bg-green-700 disabled:opacity-50 text-xs font-medium transition-colors",
1091
+ className: "rounded-md bg-green-600 px-3 py-2 text-xs font-medium text-nowrap text-white transition-colors hover:bg-green-700 disabled:opacity-50",
946
1092
  title: "Submit report now",
947
- children: "Submit"
1093
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-sm font-medium", children: "Submit report" })
948
1094
  }
949
1095
  )
950
1096
  ] })
951
- ] }),
952
- captureResult && (captureResult.consoleErrors.length > 0 || captureResult.networkErrors.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("p", { className: "text-xs text-amber-600 mt-1.5", children: [
953
- [
954
- captureResult.consoleErrors.length > 0 ? `${captureResult.consoleErrors.length} console error${captureResult.consoleErrors.length !== 1 ? "s" : ""}` : null,
955
- captureResult.networkErrors.length > 0 ? `${captureResult.networkErrors.length} failed request${captureResult.networkErrors.length !== 1 ? "s" : ""}` : null
956
- ].filter(Boolean).join(" + "),
957
- " ",
958
- "captured \u2014 these will be included in the report."
959
1097
  ] })
960
- ] })
1098
+ ] }) })
961
1099
  ]
962
1100
  }
963
1101
  )
@@ -971,8 +1109,10 @@ function JarveBugReporter({
971
1109
  apiUrl,
972
1110
  apiKey,
973
1111
  user,
1112
+ buttonPosition,
974
1113
  children
975
1114
  }) {
1115
+ const safeApiKey = apiKey || "";
976
1116
  const [captureMode, setCaptureMode] = (0, import_react3.useState)(false);
977
1117
  const [captureResult, setCaptureResult] = (0, import_react3.useState)(null);
978
1118
  const [showModal, setShowModal] = (0, import_react3.useState)(false);
@@ -1001,12 +1141,19 @@ function JarveBugReporter({
1001
1141
  clearCapturedErrors();
1002
1142
  clearCapturedNetworkErrors();
1003
1143
  }, []);
1004
- const siteId = apiKey.startsWith("brk_") ? apiKey.slice(4, 12) : "external";
1144
+ const siteId = safeApiKey.startsWith("brk_") ? safeApiKey.slice(4, 12) : "external";
1005
1145
  const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
1006
1146
  const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
1007
1147
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
1008
1148
  children,
1009
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(FloatingButton, { isActive: captureMode, onClick: toggleCaptureMode }),
1149
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1150
+ FloatingButton,
1151
+ {
1152
+ isActive: captureMode,
1153
+ onClick: toggleCaptureMode,
1154
+ position: buttonPosition
1155
+ }
1156
+ ),
1010
1157
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1011
1158
  CaptureOverlay,
1012
1159
  {