@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.mjs CHANGED
@@ -34,16 +34,21 @@ function cn(...inputs) {
34
34
 
35
35
  // src/floating-button.tsx
36
36
  import { jsx } from "react/jsx-runtime";
37
- function FloatingButton({ isActive, onClick }) {
37
+ function FloatingButton({ isActive, onClick, position = "right" }) {
38
+ const sideClasses = position === "left" ? "left-4 md:left-6" : "right-4 md:right-6";
38
39
  return /* @__PURE__ */ jsx(
39
40
  "button",
40
41
  {
41
42
  onClick,
42
43
  className: cn(
43
- "fixed bottom-6 right-6 z-[9999] flex h-12 w-12 items-center justify-center rounded-full shadow-lg transition-all duration-200",
44
- "hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2",
45
- 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",
46
- "bottom-4 right-4 h-10 w-10 md:bottom-6 md:right-6 md:h-12 md:w-12"
44
+ "fixed z-[9999] flex items-center justify-center rounded-full shadow-lg transition-all duration-200",
45
+ "hover:scale-110 focus:ring-2 focus:ring-offset-2 focus:outline-none",
46
+ // size + vertical position
47
+ "bottom-4 h-11 w-11 md:bottom-6 md:h-12 md:w-12",
48
+ // horizontal side
49
+ sideClasses,
50
+ // active vs idle colors
51
+ 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"
47
52
  ),
48
53
  title: isActive ? "Cancel bug capture" : "Report a bug",
49
54
  "aria-label": isActive ? "Cancel bug capture" : "Report a bug",
@@ -111,7 +116,18 @@ function buildSelectorPath(element, stopAt) {
111
116
  }
112
117
  return parts.join(" > ");
113
118
  }
114
- function collectElementInfo(target, section, event) {
119
+ function isTouchCapable() {
120
+ return "ontouchstart" in window || navigator.maxTouchPoints > 0;
121
+ }
122
+ function extractCoordinates(e) {
123
+ return {
124
+ pageX: e.pageX,
125
+ pageY: e.pageY,
126
+ clientX: e.clientX,
127
+ clientY: e.clientY
128
+ };
129
+ }
130
+ function collectElementInfo(target, section, coords) {
115
131
  const sectionRect = section.getBoundingClientRect();
116
132
  const dataAttributes = {};
117
133
  for (const attr of Array.from(target.attributes)) {
@@ -127,10 +143,10 @@ function collectElementInfo(target, section, event) {
127
143
  ariaLabel: target.getAttribute("aria-label") || null,
128
144
  dataAttributes,
129
145
  selectorPath: buildSelectorPath(target, section),
130
- clickX: event.pageX,
131
- clickY: event.pageY,
132
- relativeClickX: event.clientX - sectionRect.left,
133
- relativeClickY: event.clientY - sectionRect.top
146
+ clickX: coords.pageX,
147
+ clickY: coords.pageY,
148
+ relativeClickX: coords.clientX - sectionRect.left,
149
+ relativeClickY: coords.clientY - sectionRect.top
134
150
  };
135
151
  }
136
152
  function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
@@ -162,8 +178,7 @@ var errorListener = null;
162
178
  var rejectionListener = null;
163
179
  function startCapturing() {
164
180
  if (isCapturing) return;
165
- if (console.error.__bugReporterPatched)
166
- return;
181
+ if (console.error.__bugReporterPatched) return;
167
182
  isCapturing = true;
168
183
  capturedErrors = [];
169
184
  originalConsoleError = console.error;
@@ -355,6 +370,15 @@ function clearCapturedNetworkErrors() {
355
370
 
356
371
  // src/capture-overlay.tsx
357
372
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
373
+ function dataUrlToBlob(dataUrl) {
374
+ var _a;
375
+ const [header, base64] = dataUrl.split(",");
376
+ const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
377
+ const bytes = atob(base64);
378
+ const arr = new Uint8Array(bytes.length);
379
+ for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
380
+ return new Blob([arr], { type: mime });
381
+ }
358
382
  function CaptureOverlay({
359
383
  isActive,
360
384
  siteId,
@@ -366,48 +390,30 @@ function CaptureOverlay({
366
390
  const [hoveredElement, setHoveredElement] = useState(null);
367
391
  const [hoveredRect, setHoveredRect] = useState(null);
368
392
  const [isCapturing3, setIsCapturing] = useState(false);
393
+ const [isTouchMode, setIsTouchMode] = useState(false);
394
+ const [selectedSection, setSelectedSection] = useState(null);
395
+ const [selectedRect, setSelectedRect] = useState(null);
396
+ const [selectedTarget, setSelectedTarget] = useState(null);
369
397
  const overlayRef = useRef(null);
370
398
  const hoveredElementRef = useRef(null);
371
399
  const rafRef = useRef(null);
372
- const handleMouseMove = useCallback(
373
- (e) => {
374
- if (!isActive || isCapturing3) return;
375
- if (rafRef.current) return;
376
- rafRef.current = requestAnimationFrame(() => {
377
- rafRef.current = null;
378
- const target = e.target;
379
- if (!(target instanceof HTMLElement)) return;
380
- if (target.closest("[data-bug-reporter]")) {
381
- setHoveredElement(null);
382
- setHoveredRect(null);
383
- hoveredElementRef.current = null;
384
- return;
385
- }
386
- const section = getNearestSection(target);
387
- setHoveredElement(section);
388
- hoveredElementRef.current = section;
389
- setHoveredRect(section ? section.getBoundingClientRect() : null);
390
- });
391
- },
392
- [isActive, isCapturing3]
393
- );
394
- const handleClick = useCallback(
395
- async (e) => {
396
- var _a, _b;
397
- if (!isActive || isCapturing3) return;
398
- const target = e.target;
399
- if (!(target instanceof HTMLElement)) return;
400
- if (target.closest("[data-bug-reporter]")) return;
401
- e.preventDefault();
402
- e.stopPropagation();
403
- const section = getNearestSection(target);
404
- if (!section) return;
405
- const elementInfo = collectElementInfo(target, section, e);
400
+ const touchCoordsRef = useRef(null);
401
+ useEffect(() => {
402
+ if (isActive) {
403
+ setIsTouchMode(isTouchCapable());
404
+ }
405
+ }, [isActive]);
406
+ const captureScreenshot = useCallback(
407
+ async (section, target, coords) => {
408
+ const elementInfo = collectElementInfo(target, section, coords);
406
409
  setIsCapturing(true);
407
410
  try {
408
411
  setHoveredElement(null);
409
412
  setHoveredRect(null);
410
413
  hoveredElementRef.current = null;
414
+ setSelectedSection(null);
415
+ setSelectedRect(null);
416
+ setSelectedTarget(null);
411
417
  await new Promise((r) => setTimeout(r, 50));
412
418
  const MAX_DIMENSION = 2e3;
413
419
  const sectionRect = section.getBoundingClientRect();
@@ -417,12 +423,7 @@ function CaptureOverlay({
417
423
  pixelRatio,
418
424
  skipFonts: true
419
425
  });
420
- const [header, base64] = dataUrl.split(",");
421
- const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
422
- const bytes = atob(base64);
423
- const arr = new Uint8Array(bytes.length);
424
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
425
- const blob = new Blob([arr], { type: mime });
426
+ const blob = dataUrlToBlob(dataUrl);
426
427
  const metadata = collectMetadata(
427
428
  section,
428
429
  siteId,
@@ -434,7 +435,10 @@ function CaptureOverlay({
434
435
  const networkErrors = getCapturedNetworkErrors();
435
436
  onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
436
437
  } catch (err) {
437
- console.warn("Bug reporter: first capture attempt failed, retrying with simpler settings", err);
438
+ console.warn(
439
+ "Bug reporter: first capture attempt failed, retrying with simpler settings",
440
+ err
441
+ );
438
442
  try {
439
443
  const dataUrl = await toPng(section, {
440
444
  quality: 0.6,
@@ -442,12 +446,7 @@ function CaptureOverlay({
442
446
  skipFonts: true,
443
447
  cacheBust: true
444
448
  });
445
- const [header, base64] = dataUrl.split(",");
446
- const mime = ((_b = header.match(/:(.*?);/)) == null ? void 0 : _b[1]) || "image/png";
447
- const bytes = atob(base64);
448
- const arr = new Uint8Array(bytes.length);
449
- for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
450
- const retryBlob = new Blob([arr], { type: mime });
449
+ const retryBlob = dataUrlToBlob(dataUrl);
451
450
  const metadata = collectMetadata(
452
451
  section,
453
452
  siteId,
@@ -458,7 +457,7 @@ function CaptureOverlay({
458
457
  const consoleErrors = getCapturedErrors();
459
458
  const networkErrors = getCapturedNetworkErrors();
460
459
  onCapture({ screenshot: retryBlob, metadata, consoleErrors, networkErrors });
461
- } catch (e2) {
460
+ } catch (e) {
462
461
  console.error("Bug reporter: screenshot capture failed after retry");
463
462
  const metadata = collectMetadata(
464
463
  section,
@@ -480,7 +479,75 @@ function CaptureOverlay({
480
479
  setIsCapturing(false);
481
480
  }
482
481
  },
483
- [isActive, isCapturing3, siteId, reporterName, reporterEmail, onCapture]
482
+ [siteId, reporterName, reporterEmail, onCapture]
483
+ );
484
+ const handleMouseMove = useCallback(
485
+ (e) => {
486
+ if (!isActive || isCapturing3 || isTouchMode) return;
487
+ if (rafRef.current) return;
488
+ rafRef.current = requestAnimationFrame(() => {
489
+ rafRef.current = null;
490
+ const target = e.target;
491
+ if (!(target instanceof HTMLElement)) return;
492
+ if (target.closest("[data-bug-reporter]")) {
493
+ setHoveredElement(null);
494
+ setHoveredRect(null);
495
+ hoveredElementRef.current = null;
496
+ return;
497
+ }
498
+ const section = getNearestSection(target);
499
+ setHoveredElement(section);
500
+ hoveredElementRef.current = section;
501
+ setHoveredRect(section ? section.getBoundingClientRect() : null);
502
+ });
503
+ },
504
+ [isActive, isCapturing3, isTouchMode]
505
+ );
506
+ const handleClick = useCallback(
507
+ async (e) => {
508
+ if (!isActive || isCapturing3 || isTouchMode) return;
509
+ const target = e.target;
510
+ if (!(target instanceof HTMLElement)) return;
511
+ if (target.closest("[data-bug-reporter]")) return;
512
+ e.preventDefault();
513
+ e.stopPropagation();
514
+ const section = getNearestSection(target);
515
+ if (!section) return;
516
+ await captureScreenshot(section, target, extractCoordinates(e));
517
+ },
518
+ [isActive, isCapturing3, isTouchMode, captureScreenshot]
519
+ );
520
+ const handleTouchEnd = useCallback(
521
+ (e) => {
522
+ if (!isActive || isCapturing3) return;
523
+ const touch = e.changedTouches[0];
524
+ if (!touch) return;
525
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
526
+ if (!(target instanceof HTMLElement)) return;
527
+ if (target.closest("[data-bug-reporter]")) return;
528
+ const section = getNearestSection(target);
529
+ if (!section) return;
530
+ setSelectedSection(section);
531
+ setSelectedRect(section.getBoundingClientRect());
532
+ setSelectedTarget(target);
533
+ touchCoordsRef.current = extractCoordinates(touch);
534
+ },
535
+ [isActive, isCapturing3]
536
+ );
537
+ const handleConfirmCapture = useCallback(async () => {
538
+ if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
539
+ await captureScreenshot(selectedSection, selectedTarget, touchCoordsRef.current);
540
+ }, [selectedSection, selectedTarget, captureScreenshot]);
541
+ const handlePointerDown = useCallback(
542
+ (e) => {
543
+ if (!isActive) return;
544
+ if (e.pointerType === "touch") {
545
+ setIsTouchMode(true);
546
+ } else if (e.pointerType === "mouse") {
547
+ setIsTouchMode(false);
548
+ }
549
+ },
550
+ [isActive]
484
551
  );
485
552
  const handleKeyDown = useCallback(
486
553
  (e) => {
@@ -493,60 +560,131 @@ function CaptureOverlay({
493
560
  [isActive, onCancel]
494
561
  );
495
562
  const handleScroll = useCallback(() => {
496
- if (!hoveredElementRef.current) return;
497
- setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
498
- }, []);
563
+ if (hoveredElementRef.current) {
564
+ setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
565
+ }
566
+ if (selectedSection) {
567
+ setSelectedRect(selectedSection.getBoundingClientRect());
568
+ }
569
+ }, [selectedSection]);
499
570
  useEffect(() => {
500
571
  if (!isActive) {
501
572
  setHoveredElement(null);
502
573
  setHoveredRect(null);
503
574
  hoveredElementRef.current = null;
575
+ setSelectedSection(null);
576
+ setSelectedRect(null);
577
+ setSelectedTarget(null);
578
+ touchCoordsRef.current = null;
504
579
  return;
505
580
  }
506
- document.addEventListener("mousemove", handleMouseMove, true);
507
- document.addEventListener("click", handleClick, true);
581
+ document.addEventListener("pointerdown", handlePointerDown);
508
582
  document.addEventListener("keydown", handleKeyDown);
509
583
  window.addEventListener("scroll", handleScroll, { passive: true });
584
+ if (isTouchMode) {
585
+ document.addEventListener("touchend", handleTouchEnd, { passive: true });
586
+ } else {
587
+ document.addEventListener("mousemove", handleMouseMove, true);
588
+ document.addEventListener("click", handleClick, true);
589
+ }
510
590
  return () => {
511
- document.removeEventListener("mousemove", handleMouseMove, true);
512
- document.removeEventListener("click", handleClick, true);
591
+ document.removeEventListener("pointerdown", handlePointerDown);
513
592
  document.removeEventListener("keydown", handleKeyDown);
514
593
  window.removeEventListener("scroll", handleScroll);
594
+ document.removeEventListener("touchend", handleTouchEnd);
595
+ document.removeEventListener("mousemove", handleMouseMove, true);
596
+ document.removeEventListener("click", handleClick, true);
515
597
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
516
598
  };
517
- }, [isActive, handleMouseMove, handleClick, handleKeyDown, handleScroll]);
518
- if (!isActive || !hoveredElement || !hoveredRect) return null;
599
+ }, [
600
+ isActive,
601
+ isTouchMode,
602
+ handleMouseMove,
603
+ handleClick,
604
+ handleTouchEnd,
605
+ handlePointerDown,
606
+ handleKeyDown,
607
+ handleScroll
608
+ ]);
609
+ const highlightRect = isTouchMode ? selectedRect : hoveredRect;
610
+ const showHighlight = isTouchMode ? !!selectedSection : !!hoveredElement && !!hoveredRect;
611
+ if (!isActive) return null;
519
612
  return /* @__PURE__ */ jsxs(Fragment, { children: [
520
- /* @__PURE__ */ jsxs(
613
+ /* @__PURE__ */ jsx2(
521
614
  "div",
522
615
  {
523
616
  "data-bug-reporter": true,
524
617
  role: "alert",
525
618
  "aria-live": "assertive",
526
- 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",
527
- children: [
528
- "Click on the section with the bug. Press ",
529
- /* @__PURE__ */ jsx2("kbd", { className: "px-1.5 py-0.5 bg-indigo-800 rounded text-xs mx-1", children: "Esc" }),
619
+ 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",
620
+ children: isTouchMode ? /* @__PURE__ */ jsxs(Fragment, { children: [
621
+ /* @__PURE__ */ jsx2("span", { children: "Tap the section with the bug" }),
622
+ /* @__PURE__ */ jsx2(
623
+ "button",
624
+ {
625
+ onClick: onCancel,
626
+ className: "min-h-[44px] rounded-md bg-white/20 px-3 py-1 text-sm font-medium",
627
+ children: "Cancel"
628
+ }
629
+ )
630
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
631
+ "Click on the section with the bug. Press",
632
+ " ",
633
+ /* @__PURE__ */ jsx2("kbd", { className: "mx-1 rounded bg-indigo-800 px-1.5 py-0.5 text-xs", children: "Esc" }),
530
634
  " to cancel."
531
- ]
635
+ ] })
532
636
  }
533
637
  ),
534
- /* @__PURE__ */ jsx2(
638
+ showHighlight && highlightRect && /* @__PURE__ */ jsx2(
535
639
  "div",
536
640
  {
537
641
  ref: overlayRef,
538
642
  "data-bug-reporter": true,
539
- className: "fixed pointer-events-none z-[9998] border-2 border-indigo-500 rounded-sm transition-all duration-150 ease-out",
643
+ className: "pointer-events-none fixed z-[9998] rounded-sm border-2 border-indigo-500 transition-all duration-150 ease-out",
540
644
  style: {
541
- top: hoveredRect.top - 2,
542
- left: hoveredRect.left - 2,
543
- width: hoveredRect.width + 4,
544
- height: hoveredRect.height + 4,
645
+ top: highlightRect.top - 2,
646
+ left: highlightRect.left - 2,
647
+ width: highlightRect.width + 4,
648
+ height: highlightRect.height + 4,
545
649
  backgroundColor: "rgba(99, 102, 241, 0.08)"
546
650
  }
547
651
  }
548
652
  ),
549
- isActive && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
653
+ isTouchMode && selectedSection && !isCapturing3 && /* @__PURE__ */ jsx2(
654
+ "div",
655
+ {
656
+ "data-bug-reporter": true,
657
+ className: "fixed right-0 bottom-0 left-0 z-[10000] border-t border-gray-200 bg-white shadow-lg",
658
+ style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
659
+ children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
660
+ /* @__PURE__ */ jsx2("span", { className: "truncate text-sm font-medium text-gray-900", children: "Capture this section?" }),
661
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 gap-2", children: [
662
+ /* @__PURE__ */ jsx2(
663
+ "button",
664
+ {
665
+ onClick: () => {
666
+ setSelectedSection(null);
667
+ setSelectedRect(null);
668
+ setSelectedTarget(null);
669
+ touchCoordsRef.current = null;
670
+ },
671
+ className: "min-h-[44px] rounded-md border border-gray-300 px-4 text-sm font-medium text-gray-700",
672
+ children: "Cancel"
673
+ }
674
+ ),
675
+ /* @__PURE__ */ jsx2(
676
+ "button",
677
+ {
678
+ onClick: handleConfirmCapture,
679
+ className: "min-h-[44px] rounded-md bg-indigo-600 px-4 text-sm font-medium text-white",
680
+ children: "Capture"
681
+ }
682
+ )
683
+ ] })
684
+ ] })
685
+ }
686
+ ),
687
+ !isTouchMode && /* @__PURE__ */ jsx2("style", { children: `* { cursor: crosshair !important; }` })
550
688
  ] });
551
689
  }
552
690
 
@@ -612,7 +750,9 @@ function ReportModal({
612
750
  })
613
751
  });
614
752
  if (response.status === 401) {
615
- console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
753
+ console.error(
754
+ "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
755
+ );
616
756
  setMessages([
617
757
  {
618
758
  role: "assistant",
@@ -709,9 +849,7 @@ function ReportModal({
709
849
  setModalState("submitted");
710
850
  } catch (err) {
711
851
  console.error("Bug reporter: failed to submit report", err);
712
- setErrorMessage(
713
- err instanceof Error ? err.message : "Failed to submit report"
714
- );
852
+ setErrorMessage(err instanceof Error ? err.message : "Failed to submit report");
715
853
  setModalState("error");
716
854
  }
717
855
  },
@@ -724,10 +862,7 @@ function ReportModal({
724
862
  if (!input.trim() || isLoading || !captureResult) return;
725
863
  const userMessage = input.trim();
726
864
  setInput("");
727
- const newMessages = [
728
- ...messages,
729
- { role: "user", content: userMessage }
730
- ];
865
+ const newMessages = [...messages, { role: "user", content: userMessage }];
731
866
  setMessages(newMessages);
732
867
  setIsLoading(true);
733
868
  try {
@@ -743,7 +878,9 @@ function ReportModal({
743
878
  })
744
879
  });
745
880
  if (response.status === 401) {
746
- console.error("Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop.");
881
+ console.error(
882
+ "Bug reporter: invalid or missing API key. Check your BugReporter apiKey prop."
883
+ );
747
884
  setMessages([
748
885
  ...newMessages,
749
886
  {
@@ -755,10 +892,7 @@ function ReportModal({
755
892
  }
756
893
  if (!response.ok) throw new Error("Failed to get AI response");
757
894
  const data = await response.json();
758
- setMessages([
759
- ...newMessages,
760
- { role: "assistant", content: data.message }
761
- ]);
895
+ setMessages([...newMessages, { role: "assistant", content: data.message }]);
762
896
  if (data.readyToSubmit && data.structuredReport) {
763
897
  await submitReport(
764
898
  [...newMessages, { role: "assistant", content: data.message }],
@@ -791,7 +925,7 @@ function ReportModal({
791
925
  onClose();
792
926
  }
793
927
  function handleKeyDown(e) {
794
- if (e.key === "Enter" && !e.shiftKey) {
928
+ if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
795
929
  e.preventDefault();
796
930
  sendMessage();
797
931
  }
@@ -809,65 +943,66 @@ function ReportModal({
809
943
  "div",
810
944
  {
811
945
  className: cn(
812
- "bg-white dark:bg-gray-950 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-800 flex flex-col overflow-hidden",
813
- "w-full max-w-lg mx-4",
814
- "max-[768px]:mx-0 max-[768px]:rounded-none max-[768px]:max-w-none max-[768px]:h-full",
946
+ "flex flex-col overflow-hidden rounded-xl border border-gray-200 bg-white shadow-2xl dark:border-gray-800 dark:bg-gray-950",
947
+ "mx-4 w-full max-w-lg",
948
+ "max-[768px]:mx-0 max-[768px]:h-full max-[768px]:max-w-none max-[768px]:rounded-none",
815
949
  "min-[769px]:max-h-[85vh]"
816
950
  ),
817
951
  children: [
818
- /* @__PURE__ */ jsxs2("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: [
952
+ /* @__PURE__ */ jsxs2("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: [
819
953
  /* @__PURE__ */ jsxs2("div", { children: [
820
- /* @__PURE__ */ jsx3("h2", { className: "font-semibold text-sm text-gray-900 dark:text-gray-100", children: "Bug Report" }),
954
+ /* @__PURE__ */ jsx3("h2", { className: "text-sm font-semibold text-gray-900 dark:text-gray-100", children: "Bug Report" }),
821
955
  /* @__PURE__ */ jsx3("p", { className: "text-xs text-gray-500 dark:text-gray-400", children: siteId })
822
956
  ] }),
823
957
  /* @__PURE__ */ jsx3(
824
958
  "button",
825
959
  {
826
960
  onClick: handleClose,
827
- className: "p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
961
+ className: "rounded-md p-1.5 transition-colors hover:bg-gray-100 dark:hover:bg-gray-800",
828
962
  "aria-label": "Close",
829
963
  children: /* @__PURE__ */ jsx3(X, { className: "h-4 w-4 text-gray-600 dark:text-gray-400" })
830
964
  }
831
965
  )
832
966
  ] }),
833
- screenshotUrl && /* @__PURE__ */ jsx3("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__ */ jsx3(
967
+ screenshotUrl && /* @__PURE__ */ jsx3("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__ */ jsx3(
834
968
  "img",
835
969
  {
836
970
  src: screenshotUrl,
837
971
  alt: "Captured section",
838
- className: "w-full max-h-40 object-contain rounded-md border border-gray-200 dark:border-gray-700"
972
+ className: "max-h-40 w-full rounded-md border border-gray-200 object-contain dark:border-gray-700"
839
973
  }
840
974
  ) }),
841
- /* @__PURE__ */ jsx3("div", { className: "flex-1 overflow-y-auto px-4 py-3 space-y-3 min-h-0", children: modalState === "submitted" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
842
- /* @__PURE__ */ jsx3(CheckCircle2, { className: "h-12 w-12 text-green-500 mb-3" }),
843
- /* @__PURE__ */ jsx3("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
844
- /* @__PURE__ */ jsxs2("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: [
845
- "Reference: ",
846
- /* @__PURE__ */ jsx3("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) })
975
+ /* @__PURE__ */ jsx3("div", { className: "min-h-0 flex-1 space-y-3 overflow-y-auto px-4 py-3", children: modalState === "submitted" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
976
+ /* @__PURE__ */ jsx3(CheckCircle2, { className: "mb-3 h-12 w-12 text-green-500" }),
977
+ /* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Report Submitted" }),
978
+ /* @__PURE__ */ jsxs2("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: [
979
+ "Reference:",
980
+ " ",
981
+ /* @__PURE__ */ jsx3("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) })
847
982
  ] }),
848
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-2", children: "Thanks for the report \u2014 we'll look into it." }),
983
+ /* @__PURE__ */ jsx3("p", { className: "mt-2 text-sm text-gray-500 dark:text-gray-400", children: "Thanks for the report \u2014 we'll look into it." }),
849
984
  /* @__PURE__ */ jsx3(
850
985
  "button",
851
986
  {
852
987
  onClick: handleClose,
853
- className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
988
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
854
989
  children: "Done"
855
990
  }
856
991
  )
857
992
  ] }) : modalState === "error" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8 text-center", children: [
858
- /* @__PURE__ */ jsx3(X, { className: "h-12 w-12 text-red-500 mb-3" }),
859
- /* @__PURE__ */ jsx3("h3", { className: "font-semibold text-lg text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
860
- /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400 mt-1", children: errorMessage || "Something went wrong. Please try again." }),
993
+ /* @__PURE__ */ jsx3(X, { className: "mb-3 h-12 w-12 text-red-500" }),
994
+ /* @__PURE__ */ jsx3("h3", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: "Submission Failed" }),
995
+ /* @__PURE__ */ jsx3("p", { className: "mt-1 text-sm text-gray-500 dark:text-gray-400", children: errorMessage || "Something went wrong. Please try again." }),
861
996
  /* @__PURE__ */ jsx3(
862
997
  "button",
863
998
  {
864
999
  onClick: () => setModalState("chatting"),
865
- className: "mt-4 px-4 py-2 bg-indigo-600 text-white rounded-md text-sm hover:bg-indigo-700 transition-colors",
1000
+ className: "mt-4 rounded-md bg-indigo-600 px-4 py-2 text-sm text-white transition-colors hover:bg-indigo-700",
866
1001
  children: "Try Again"
867
1002
  }
868
1003
  )
869
1004
  ] }) : modalState === "submitting" ? /* @__PURE__ */ jsxs2("div", { className: "flex flex-col items-center justify-center py-8", children: [
870
- /* @__PURE__ */ jsx3(Loader2, { className: "h-8 w-8 animate-spin text-indigo-500 mb-3" }),
1005
+ /* @__PURE__ */ jsx3(Loader2, { className: "mb-3 h-8 w-8 animate-spin text-indigo-500" }),
871
1006
  /* @__PURE__ */ jsx3("p", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Submitting your report..." })
872
1007
  ] }) : /* @__PURE__ */ jsxs2(Fragment2, { children: [
873
1008
  (captureResult == null ? void 0 : captureResult.screenshot.size) === 0 && /* @__PURE__ */ jsx3("p", { className: "text-xs text-amber-600", children: "Screenshot could not be captured. Please describe the visual issue in detail." }),
@@ -876,42 +1011,53 @@ function ReportModal({
876
1011
  {
877
1012
  className: cn(
878
1013
  "text-sm leading-relaxed",
879
- 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"
1014
+ 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"
880
1015
  ),
881
1016
  children: msg.content
882
1017
  },
883
1018
  i
884
1019
  )),
885
- isLoading && /* @__PURE__ */ jsxs2("div", { className: "bg-gray-100/50 dark:bg-gray-800/50 rounded-lg p-3 flex items-center gap-2", children: [
1020
+ isLoading && /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 rounded-lg bg-gray-100/50 p-3 dark:bg-gray-800/50", children: [
886
1021
  /* @__PURE__ */ jsx3(Loader2, { className: "h-3.5 w-3.5 animate-spin text-gray-500" }),
887
1022
  /* @__PURE__ */ jsx3("span", { className: "text-sm text-gray-500 dark:text-gray-400", children: "Thinking..." })
888
1023
  ] }),
889
1024
  /* @__PURE__ */ jsx3("div", { ref: chatEndRef })
890
1025
  ] }) }),
891
- modalState === "chatting" && /* @__PURE__ */ jsxs2("div", { className: "border-t border-gray-200 dark:border-gray-800 px-4 py-3", children: [
892
- /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
893
- /* @__PURE__ */ jsx3(
894
- "textarea",
895
- {
896
- ref: inputRef,
897
- value: input,
898
- onChange: (e) => setInput(e.target.value),
899
- onKeyDown: handleKeyDown,
900
- placeholder: "Describe what's wrong...",
901
- rows: 2,
902
- 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",
903
- disabled: isLoading
904
- }
905
- ),
906
- /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1", children: [
907
- /* @__PURE__ */ jsx3(
1026
+ modalState === "chatting" && /* @__PURE__ */ jsx3("div", { className: "border-t border-gray-200 px-4 py-3 dark:border-gray-800", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
1027
+ /* @__PURE__ */ jsx3(
1028
+ "textarea",
1029
+ {
1030
+ ref: inputRef,
1031
+ value: input,
1032
+ onChange: (e) => setInput(e.target.value),
1033
+ onKeyDown: handleKeyDown,
1034
+ placeholder: "Describe what's wrong...",
1035
+ rows: 2,
1036
+ 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",
1037
+ disabled: isLoading
1038
+ }
1039
+ ),
1040
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-2", children: [
1041
+ captureResult && (captureResult.consoleErrors.length > 0 || captureResult.networkErrors.length > 0) && /* @__PURE__ */ jsxs2("p", { className: "flex-1 text-xs text-amber-600", children: [
1042
+ [
1043
+ captureResult.consoleErrors.length > 0 ? `${captureResult.consoleErrors.length} console error${captureResult.consoleErrors.length !== 1 ? "s" : ""}` : null,
1044
+ captureResult.networkErrors.length > 0 ? `${captureResult.networkErrors.length} failed request${captureResult.networkErrors.length !== 1 ? "s" : ""}` : null
1045
+ ].filter(Boolean).join(" + "),
1046
+ " ",
1047
+ "captured \u2014 these will be included in the report."
1048
+ ] }),
1049
+ /* @__PURE__ */ jsxs2("div", { className: "flex justify-end gap-2", children: [
1050
+ /* @__PURE__ */ jsxs2(
908
1051
  "button",
909
1052
  {
910
1053
  onClick: sendMessage,
911
1054
  disabled: !input.trim() || isLoading,
912
- className: "p-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",
1055
+ 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",
913
1056
  title: "Send message",
914
- children: /* @__PURE__ */ jsx3(Send, { className: "h-4 w-4" })
1057
+ children: [
1058
+ /* @__PURE__ */ jsx3(Send, { className: "h-4 w-4" }),
1059
+ /* @__PURE__ */ jsx3("span", { className: "text-sm font-medium", children: "Send message" })
1060
+ ]
915
1061
  }
916
1062
  ),
917
1063
  messages.length >= 2 && /* @__PURE__ */ jsx3(
@@ -919,22 +1065,14 @@ function ReportModal({
919
1065
  {
920
1066
  onClick: handleManualSubmit,
921
1067
  disabled: isLoading,
922
- 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",
1068
+ 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",
923
1069
  title: "Submit report now",
924
- children: "Submit"
1070
+ children: /* @__PURE__ */ jsx3("span", { className: "text-sm font-medium", children: "Submit report" })
925
1071
  }
926
1072
  )
927
1073
  ] })
928
- ] }),
929
- captureResult && (captureResult.consoleErrors.length > 0 || captureResult.networkErrors.length > 0) && /* @__PURE__ */ jsxs2("p", { className: "text-xs text-amber-600 mt-1.5", children: [
930
- [
931
- captureResult.consoleErrors.length > 0 ? `${captureResult.consoleErrors.length} console error${captureResult.consoleErrors.length !== 1 ? "s" : ""}` : null,
932
- captureResult.networkErrors.length > 0 ? `${captureResult.networkErrors.length} failed request${captureResult.networkErrors.length !== 1 ? "s" : ""}` : null
933
- ].filter(Boolean).join(" + "),
934
- " ",
935
- "captured \u2014 these will be included in the report."
936
1074
  ] })
937
- ] })
1075
+ ] }) })
938
1076
  ]
939
1077
  }
940
1078
  )
@@ -948,8 +1086,10 @@ function JarveBugReporter({
948
1086
  apiUrl,
949
1087
  apiKey,
950
1088
  user,
1089
+ buttonPosition,
951
1090
  children
952
1091
  }) {
1092
+ const safeApiKey = apiKey || "";
953
1093
  const [captureMode, setCaptureMode] = useState3(false);
954
1094
  const [captureResult, setCaptureResult] = useState3(null);
955
1095
  const [showModal, setShowModal] = useState3(false);
@@ -978,12 +1118,19 @@ function JarveBugReporter({
978
1118
  clearCapturedErrors();
979
1119
  clearCapturedNetworkErrors();
980
1120
  }, []);
981
- const siteId = apiKey.startsWith("brk_") ? apiKey.slice(4, 12) : "external";
1121
+ const siteId = safeApiKey.startsWith("brk_") ? safeApiKey.slice(4, 12) : "external";
982
1122
  const reporterName = (user == null ? void 0 : user.name) || "Anonymous";
983
1123
  const reporterEmail = (user == null ? void 0 : user.email) || "unknown@external";
984
1124
  return /* @__PURE__ */ jsxs3(Fragment3, { children: [
985
1125
  children,
986
- /* @__PURE__ */ jsx4(FloatingButton, { isActive: captureMode, onClick: toggleCaptureMode }),
1126
+ /* @__PURE__ */ jsx4(
1127
+ FloatingButton,
1128
+ {
1129
+ isActive: captureMode,
1130
+ onClick: toggleCaptureMode,
1131
+ position: buttonPosition
1132
+ }
1133
+ ),
987
1134
  /* @__PURE__ */ jsx4(
988
1135
  CaptureOverlay,
989
1136
  {