@jarve/bug-reporter 0.1.1 → 0.2.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
@@ -66,7 +66,7 @@ function FloatingButton({ isActive, onClick }) {
66
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
67
  "hover:scale-110 focus:outline-none focus:ring-2 focus:ring-offset-2",
68
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"
69
+ "bottom-4 right-4 h-11 w-11 md:bottom-6 md:right-6 md:h-12 md:w-12"
70
70
  ),
71
71
  title: isActive ? "Cancel bug capture" : "Report a bug",
72
72
  "aria-label": isActive ? "Cancel bug capture" : "Report a bug",
@@ -134,7 +134,18 @@ function buildSelectorPath(element, stopAt) {
134
134
  }
135
135
  return parts.join(" > ");
136
136
  }
137
- function collectElementInfo(target, section, event) {
137
+ function isTouchCapable() {
138
+ return "ontouchstart" in window || navigator.maxTouchPoints > 0;
139
+ }
140
+ function extractCoordinates(e) {
141
+ return {
142
+ pageX: e.pageX,
143
+ pageY: e.pageY,
144
+ clientX: e.clientX,
145
+ clientY: e.clientY
146
+ };
147
+ }
148
+ function collectElementInfo(target, section, coords) {
138
149
  const sectionRect = section.getBoundingClientRect();
139
150
  const dataAttributes = {};
140
151
  for (const attr of Array.from(target.attributes)) {
@@ -150,10 +161,10 @@ function collectElementInfo(target, section, event) {
150
161
  ariaLabel: target.getAttribute("aria-label") || null,
151
162
  dataAttributes,
152
163
  selectorPath: buildSelectorPath(target, section),
153
- clickX: event.pageX,
154
- clickY: event.pageY,
155
- relativeClickX: event.clientX - sectionRect.left,
156
- relativeClickY: event.clientY - sectionRect.top
164
+ clickX: coords.pageX,
165
+ clickY: coords.pageY,
166
+ relativeClickX: coords.clientX - sectionRect.left,
167
+ relativeClickY: coords.clientY - sectionRect.top
157
168
  };
158
169
  }
159
170
  function collectMetadata(sectionElement, siteId, reporterName, reporterEmail, clickedElement) {
@@ -378,6 +389,15 @@ function clearCapturedNetworkErrors() {
378
389
 
379
390
  // src/capture-overlay.tsx
380
391
  var import_jsx_runtime2 = require("react/jsx-runtime");
392
+ function dataUrlToBlob(dataUrl) {
393
+ var _a;
394
+ const [header, base64] = dataUrl.split(",");
395
+ const mime = ((_a = header.match(/:(.*?);/)) == null ? void 0 : _a[1]) || "image/png";
396
+ const bytes = atob(base64);
397
+ const arr = new Uint8Array(bytes.length);
398
+ for (let i = 0; i < bytes.length; i++) arr[i] = bytes.charCodeAt(i);
399
+ return new Blob([arr], { type: mime });
400
+ }
381
401
  function CaptureOverlay({
382
402
  isActive,
383
403
  siteId,
@@ -389,48 +409,30 @@ function CaptureOverlay({
389
409
  const [hoveredElement, setHoveredElement] = (0, import_react.useState)(null);
390
410
  const [hoveredRect, setHoveredRect] = (0, import_react.useState)(null);
391
411
  const [isCapturing3, setIsCapturing] = (0, import_react.useState)(false);
412
+ const [isTouchMode, setIsTouchMode] = (0, import_react.useState)(false);
413
+ const [selectedSection, setSelectedSection] = (0, import_react.useState)(null);
414
+ const [selectedRect, setSelectedRect] = (0, import_react.useState)(null);
415
+ const [selectedTarget, setSelectedTarget] = (0, import_react.useState)(null);
392
416
  const overlayRef = (0, import_react.useRef)(null);
393
417
  const hoveredElementRef = (0, import_react.useRef)(null);
394
418
  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);
419
+ const touchCoordsRef = (0, import_react.useRef)(null);
420
+ (0, import_react.useEffect)(() => {
421
+ if (isActive) {
422
+ setIsTouchMode(isTouchCapable());
423
+ }
424
+ }, [isActive]);
425
+ const captureScreenshot = (0, import_react.useCallback)(
426
+ async (section, target, coords) => {
427
+ const elementInfo = collectElementInfo(target, section, coords);
429
428
  setIsCapturing(true);
430
429
  try {
431
430
  setHoveredElement(null);
432
431
  setHoveredRect(null);
433
432
  hoveredElementRef.current = null;
433
+ setSelectedSection(null);
434
+ setSelectedRect(null);
435
+ setSelectedTarget(null);
434
436
  await new Promise((r) => setTimeout(r, 50));
435
437
  const MAX_DIMENSION = 2e3;
436
438
  const sectionRect = section.getBoundingClientRect();
@@ -440,19 +442,8 @@ function CaptureOverlay({
440
442
  pixelRatio,
441
443
  skipFonts: true
442
444
  });
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 metadata = collectMetadata(
450
- section,
451
- siteId,
452
- reporterName,
453
- reporterEmail,
454
- elementInfo
455
- );
445
+ const blob = dataUrlToBlob(dataUrl);
446
+ const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
456
447
  const consoleErrors = getCapturedErrors();
457
448
  const networkErrors = getCapturedNetworkErrors();
458
449
  onCapture({ screenshot: blob, metadata, consoleErrors, networkErrors });
@@ -465,31 +456,14 @@ function CaptureOverlay({
465
456
  skipFonts: true,
466
457
  cacheBust: true
467
458
  });
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 });
474
- const metadata = collectMetadata(
475
- section,
476
- siteId,
477
- reporterName,
478
- reporterEmail,
479
- elementInfo
480
- );
459
+ const retryBlob = dataUrlToBlob(dataUrl);
460
+ const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
481
461
  const consoleErrors = getCapturedErrors();
482
462
  const networkErrors = getCapturedNetworkErrors();
483
463
  onCapture({ screenshot: retryBlob, metadata, consoleErrors, networkErrors });
484
- } catch (e2) {
464
+ } catch (e) {
485
465
  console.error("Bug reporter: screenshot capture failed after retry");
486
- const metadata = collectMetadata(
487
- section,
488
- siteId,
489
- reporterName,
490
- reporterEmail,
491
- elementInfo
492
- );
466
+ const metadata = collectMetadata(section, siteId, reporterName, reporterEmail, elementInfo);
493
467
  const consoleErrors = getCapturedErrors();
494
468
  const networkErrors = getCapturedNetworkErrors();
495
469
  onCapture({
@@ -503,7 +477,75 @@ function CaptureOverlay({
503
477
  setIsCapturing(false);
504
478
  }
505
479
  },
506
- [isActive, isCapturing3, siteId, reporterName, reporterEmail, onCapture]
480
+ [siteId, reporterName, reporterEmail, onCapture]
481
+ );
482
+ const handleMouseMove = (0, import_react.useCallback)(
483
+ (e) => {
484
+ if (!isActive || isCapturing3 || isTouchMode) return;
485
+ if (rafRef.current) return;
486
+ rafRef.current = requestAnimationFrame(() => {
487
+ rafRef.current = null;
488
+ const target = e.target;
489
+ if (!(target instanceof HTMLElement)) return;
490
+ if (target.closest("[data-bug-reporter]")) {
491
+ setHoveredElement(null);
492
+ setHoveredRect(null);
493
+ hoveredElementRef.current = null;
494
+ return;
495
+ }
496
+ const section = getNearestSection(target);
497
+ setHoveredElement(section);
498
+ hoveredElementRef.current = section;
499
+ setHoveredRect(section ? section.getBoundingClientRect() : null);
500
+ });
501
+ },
502
+ [isActive, isCapturing3, isTouchMode]
503
+ );
504
+ const handleClick = (0, import_react.useCallback)(
505
+ async (e) => {
506
+ if (!isActive || isCapturing3 || isTouchMode) return;
507
+ const target = e.target;
508
+ if (!(target instanceof HTMLElement)) return;
509
+ if (target.closest("[data-bug-reporter]")) return;
510
+ e.preventDefault();
511
+ e.stopPropagation();
512
+ const section = getNearestSection(target);
513
+ if (!section) return;
514
+ await captureScreenshot(section, target, extractCoordinates(e));
515
+ },
516
+ [isActive, isCapturing3, isTouchMode, captureScreenshot]
517
+ );
518
+ const handleTouchEnd = (0, import_react.useCallback)(
519
+ (e) => {
520
+ if (!isActive || isCapturing3) return;
521
+ const touch = e.changedTouches[0];
522
+ if (!touch) return;
523
+ const target = document.elementFromPoint(touch.clientX, touch.clientY);
524
+ if (!(target instanceof HTMLElement)) return;
525
+ if (target.closest("[data-bug-reporter]")) return;
526
+ const section = getNearestSection(target);
527
+ if (!section) return;
528
+ setSelectedSection(section);
529
+ setSelectedRect(section.getBoundingClientRect());
530
+ setSelectedTarget(target);
531
+ touchCoordsRef.current = extractCoordinates(touch);
532
+ },
533
+ [isActive, isCapturing3]
534
+ );
535
+ const handleConfirmCapture = (0, import_react.useCallback)(async () => {
536
+ if (!selectedSection || !selectedTarget || !touchCoordsRef.current) return;
537
+ await captureScreenshot(selectedSection, selectedTarget, touchCoordsRef.current);
538
+ }, [selectedSection, selectedTarget, captureScreenshot]);
539
+ const handlePointerDown = (0, import_react.useCallback)(
540
+ (e) => {
541
+ if (!isActive) return;
542
+ if (e.pointerType === "touch") {
543
+ setIsTouchMode(true);
544
+ } else if (e.pointerType === "mouse") {
545
+ setIsTouchMode(false);
546
+ }
547
+ },
548
+ [isActive]
507
549
  );
508
550
  const handleKeyDown = (0, import_react.useCallback)(
509
551
  (e) => {
@@ -516,60 +558,121 @@ function CaptureOverlay({
516
558
  [isActive, onCancel]
517
559
  );
518
560
  const handleScroll = (0, import_react.useCallback)(() => {
519
- if (!hoveredElementRef.current) return;
520
- setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
521
- }, []);
561
+ if (hoveredElementRef.current) {
562
+ setHoveredRect(hoveredElementRef.current.getBoundingClientRect());
563
+ }
564
+ if (selectedSection) {
565
+ setSelectedRect(selectedSection.getBoundingClientRect());
566
+ }
567
+ }, [selectedSection]);
522
568
  (0, import_react.useEffect)(() => {
523
569
  if (!isActive) {
524
570
  setHoveredElement(null);
525
571
  setHoveredRect(null);
526
572
  hoveredElementRef.current = null;
573
+ setSelectedSection(null);
574
+ setSelectedRect(null);
575
+ setSelectedTarget(null);
576
+ touchCoordsRef.current = null;
527
577
  return;
528
578
  }
529
- document.addEventListener("mousemove", handleMouseMove, true);
530
- document.addEventListener("click", handleClick, true);
579
+ document.addEventListener("pointerdown", handlePointerDown);
531
580
  document.addEventListener("keydown", handleKeyDown);
532
581
  window.addEventListener("scroll", handleScroll, { passive: true });
582
+ if (isTouchMode) {
583
+ document.addEventListener("touchend", handleTouchEnd, { passive: true });
584
+ } else {
585
+ document.addEventListener("mousemove", handleMouseMove, true);
586
+ document.addEventListener("click", handleClick, true);
587
+ }
533
588
  return () => {
534
- document.removeEventListener("mousemove", handleMouseMove, true);
535
- document.removeEventListener("click", handleClick, true);
589
+ document.removeEventListener("pointerdown", handlePointerDown);
536
590
  document.removeEventListener("keydown", handleKeyDown);
537
591
  window.removeEventListener("scroll", handleScroll);
592
+ document.removeEventListener("touchend", handleTouchEnd);
593
+ document.removeEventListener("mousemove", handleMouseMove, true);
594
+ document.removeEventListener("click", handleClick, true);
538
595
  if (rafRef.current) cancelAnimationFrame(rafRef.current);
539
596
  };
540
- }, [isActive, handleMouseMove, handleClick, handleKeyDown, handleScroll]);
541
- if (!isActive || !hoveredElement || !hoveredRect) return null;
597
+ }, [isActive, isTouchMode, handleMouseMove, handleClick, handleTouchEnd, handlePointerDown, handleKeyDown, handleScroll]);
598
+ const highlightRect = isTouchMode ? selectedRect : hoveredRect;
599
+ const showHighlight = isTouchMode ? !!selectedSection : !!hoveredElement && !!hoveredRect;
600
+ if (!isActive) return null;
542
601
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
543
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
602
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
544
603
  "div",
545
604
  {
546
605
  "data-bug-reporter": true,
547
606
  role: "alert",
548
607
  "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: [
608
+ 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 flex items-center justify-center gap-3",
609
+ children: isTouchMode ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
610
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { children: "Tap the section with the bug" }),
611
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
612
+ "button",
613
+ {
614
+ onClick: onCancel,
615
+ className: "px-3 py-1 min-h-[44px] bg-white/20 rounded-md text-sm font-medium",
616
+ children: "Cancel"
617
+ }
618
+ )
619
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
551
620
  "Click on the section with the bug. Press ",
552
621
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("kbd", { className: "px-1.5 py-0.5 bg-indigo-800 rounded text-xs mx-1", children: "Esc" }),
553
622
  " to cancel."
554
- ]
623
+ ] })
555
624
  }
556
625
  ),
557
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
626
+ showHighlight && highlightRect && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
558
627
  "div",
559
628
  {
560
629
  ref: overlayRef,
561
630
  "data-bug-reporter": true,
562
631
  className: "fixed pointer-events-none z-[9998] border-2 border-indigo-500 rounded-sm transition-all duration-150 ease-out",
563
632
  style: {
564
- top: hoveredRect.top - 2,
565
- left: hoveredRect.left - 2,
566
- width: hoveredRect.width + 4,
567
- height: hoveredRect.height + 4,
633
+ top: highlightRect.top - 2,
634
+ left: highlightRect.left - 2,
635
+ width: highlightRect.width + 4,
636
+ height: highlightRect.height + 4,
568
637
  backgroundColor: "rgba(99, 102, 241, 0.08)"
569
638
  }
570
639
  }
571
640
  ),
572
- isActive && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
641
+ isTouchMode && selectedSection && !isCapturing3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
642
+ "div",
643
+ {
644
+ "data-bug-reporter": true,
645
+ className: "fixed bottom-0 left-0 right-0 z-[10000] bg-white border-t border-gray-200 shadow-lg",
646
+ style: { paddingBottom: "env(safe-area-inset-bottom, 0px)" },
647
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex items-center justify-between px-4 py-3 gap-3", children: [
648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-sm font-medium text-gray-900 truncate", children: "Capture this section?" }),
649
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-2 shrink-0", children: [
650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
651
+ "button",
652
+ {
653
+ onClick: () => {
654
+ setSelectedSection(null);
655
+ setSelectedRect(null);
656
+ setSelectedTarget(null);
657
+ touchCoordsRef.current = null;
658
+ },
659
+ className: "px-4 min-h-[44px] rounded-md border border-gray-300 text-sm font-medium text-gray-700",
660
+ children: "Cancel"
661
+ }
662
+ ),
663
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
664
+ "button",
665
+ {
666
+ onClick: handleConfirmCapture,
667
+ className: "px-4 min-h-[44px] rounded-md bg-indigo-600 text-white text-sm font-medium",
668
+ children: "Capture"
669
+ }
670
+ )
671
+ ] })
672
+ ] })
673
+ }
674
+ ),
675
+ !isTouchMode && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `* { cursor: crosshair !important; }` })
573
676
  ] });
574
677
  }
575
678
 
@@ -814,7 +917,7 @@ function ReportModal({
814
917
  onClose();
815
918
  }
816
919
  function handleKeyDown(e) {
817
- if (e.key === "Enter" && !e.shiftKey) {
920
+ if (e.key === "Enter" && !e.shiftKey && !isTouchCapable()) {
818
921
  e.preventDefault();
819
922
  sendMessage();
820
923
  }