@mhosaic/feedback 0.23.0 → 0.24.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.
@@ -50,7 +50,10 @@ function createApiClient(options) {
50
50
  form.append(field, String(payload[field]));
51
51
  }
52
52
  form.append("technical_context", JSON.stringify(payload.technical_context));
53
- if (payload.screenshot) form.append("screenshot", payload.screenshot, "screenshot.png");
53
+ const screenshots = payload.screenshots?.length ? payload.screenshots : payload.screenshot ? [payload.screenshot] : [];
54
+ screenshots.forEach((blob, i) => {
55
+ form.append("screenshot", blob, i === 0 ? "screenshot.png" : `screenshot-${i + 1}.png`);
56
+ });
54
57
  if (payload.synthetic) form.append("synthetic", "true");
55
58
  if (payload.user?.id) {
56
59
  form.append("user", JSON.stringify(payload.user));
@@ -572,6 +575,7 @@ var DEFAULT_STRINGS = {
572
575
  "form.screenshot.annotate": "Annotate",
573
576
  "form.screenshot.error_type": "Unsupported file type. Use PNG, JPEG or WebP.",
574
577
  "form.screenshot.error_size": "File too large (max {max} MB).",
578
+ "form.screenshot.error_count": "Too many screenshots (max {max}).",
575
579
  "form.context.label": "Page",
576
580
  "type.bug": "Bug",
577
581
  "type.feature": "Feature request",
@@ -722,6 +726,7 @@ var FRENCH_STRINGS = {
722
726
  "form.screenshot.annotate": "Annoter",
723
727
  "form.screenshot.error_type": "Format non support\xE9. Utilisez PNG, JPEG ou WebP.",
724
728
  "form.screenshot.error_size": "Fichier trop volumineux (max {max} Mo).",
729
+ "form.screenshot.error_count": "Trop de captures d\u2019\xE9cran (max {max}).",
725
730
  "form.context.label": "Page",
726
731
  "type.bug": "Bogue",
727
732
  "type.feature": "Suggestion",
@@ -959,6 +964,7 @@ var ALLOWED_IMAGE_TYPES = [
959
964
  "image/webp"
960
965
  ];
961
966
  var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
967
+ var MAX_SCREENSHOTS = 5;
962
968
  function validateScreenshotFile(file) {
963
969
  if (!ALLOWED_IMAGE_TYPES.includes(
964
970
  file.type
@@ -1128,8 +1134,8 @@ function ReportDetailView({
1128
1134
  /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
1129
1135
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1130
1136
  /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
1131
- detail.screenshot_url && (() => {
1132
- const safeHref = safeExternalHref(detail.screenshot_url);
1137
+ (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1138
+ const safeHref = safeExternalHref(url);
1133
1139
  return safeHref ? /* @__PURE__ */ jsx2(
1134
1140
  "a",
1135
1141
  {
@@ -1137,10 +1143,10 @@ function ReportDetailView({
1137
1143
  href: safeHref,
1138
1144
  target: "_blank",
1139
1145
  rel: "noopener noreferrer",
1140
- children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1146
+ children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" })
1141
1147
  }
1142
- ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" }) });
1143
- })(),
1148
+ ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" }) });
1149
+ }),
1144
1150
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1145
1151
  detail.comments.length === 0 ? /* @__PURE__ */ jsx2("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx2("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(CommentBubble, { comment: c, strings }) })) }),
1146
1152
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
@@ -2519,45 +2525,58 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2519
2525
  const [feedbackType, setFeedbackType] = useState5("bug");
2520
2526
  const [severity, setSeverity] = useState5("medium");
2521
2527
  const [localError, setLocalError] = useState5("");
2522
- const [screenshotBlob, setScreenshotBlob] = useState5(null);
2523
- const [screenshotPreview, setScreenshotPreview] = useState5(null);
2528
+ const [shots, setShots] = useState5([]);
2524
2529
  const [isDragOver, setIsDragOver] = useState5(false);
2525
- const [annotatorOpen, setAnnotatorOpen] = useState5(false);
2530
+ const [annotatingIndex, setAnnotatingIndex] = useState5(null);
2526
2531
  const fileInputRef = useRef4(null);
2527
2532
  const dropZoneRef = useRef4(null);
2528
2533
  const submitting = status === "submitting";
2529
2534
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
2530
2535
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
2536
+ const shotsRef = useRef4(shots);
2537
+ shotsRef.current = shots;
2531
2538
  useEffect5(() => {
2532
2539
  return () => {
2533
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2540
+ shotsRef.current.forEach((s) => URL.revokeObjectURL(s.preview));
2534
2541
  };
2535
- }, [screenshotPreview]);
2536
- const acceptFile = (file) => {
2542
+ }, []);
2543
+ const acceptFiles = (files) => {
2537
2544
  setLocalError("");
2538
- if (file instanceof File) {
2539
- const err = validateScreenshotFile(file);
2540
- if (err) {
2541
- setLocalError(
2542
- err.kind === "type" ? strings["form.screenshot.error_type"] : strings["form.screenshot.error_size"].replace("{max}", String(err.maxMb))
2543
- );
2544
- return;
2545
+ const remaining = MAX_SCREENSHOTS - shots.length;
2546
+ const batch = files.slice(0, Math.max(0, remaining));
2547
+ for (const file of batch) {
2548
+ if (file instanceof File) {
2549
+ const err = validateScreenshotFile(file);
2550
+ if (err) {
2551
+ setLocalError(
2552
+ err.kind === "type" ? strings["form.screenshot.error_type"] : strings["form.screenshot.error_size"].replace("{max}", String(err.maxMb))
2553
+ );
2554
+ return;
2555
+ }
2545
2556
  }
2546
2557
  }
2547
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2548
- setScreenshotBlob(file);
2549
- setScreenshotPreview(URL.createObjectURL(file));
2558
+ if (batch.length) {
2559
+ const incoming = batch.map((blob) => ({ blob, preview: URL.createObjectURL(blob) }));
2560
+ setShots((prev) => [...prev, ...incoming]);
2561
+ }
2562
+ if (files.length > batch.length) {
2563
+ setLocalError(
2564
+ strings["form.screenshot.error_count"].replace("{max}", String(MAX_SCREENSHOTS))
2565
+ );
2566
+ }
2550
2567
  };
2551
- const clearScreenshot = () => {
2552
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2553
- setScreenshotPreview(null);
2554
- setScreenshotBlob(null);
2568
+ const removeShot = (index) => {
2569
+ const shot = shots[index];
2570
+ if (shot) URL.revokeObjectURL(shot.preview);
2571
+ setShots((prev) => prev.filter((_, i) => i !== index));
2555
2572
  setLocalError("");
2556
2573
  if (fileInputRef.current) fileInputRef.current.value = "";
2557
2574
  };
2558
2575
  const handleFileInputChange = (e) => {
2559
- const file = e.target.files?.[0];
2560
- if (file) acceptFile(file);
2576
+ const input = e.target;
2577
+ const files = Array.from(input.files ?? []);
2578
+ if (files.length) acceptFiles(files);
2579
+ input.value = "";
2561
2580
  };
2562
2581
  const handleDragOver = (e) => {
2563
2582
  e.preventDefault();
@@ -2573,8 +2592,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2573
2592
  e.preventDefault();
2574
2593
  e.stopPropagation();
2575
2594
  setIsDragOver(false);
2576
- const file = e.dataTransfer?.files?.[0];
2577
- if (file) acceptFile(file);
2595
+ const files = Array.from(e.dataTransfer?.files ?? []);
2596
+ if (files.length) acceptFiles(files);
2578
2597
  };
2579
2598
  useEffect5(() => {
2580
2599
  const zone = dropZoneRef.current;
@@ -2587,7 +2606,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2587
2606
  const file = item.getAsFile();
2588
2607
  if (file) {
2589
2608
  e.preventDefault();
2590
- acceptFile(file);
2609
+ acceptFiles([file]);
2591
2610
  return;
2592
2611
  }
2593
2612
  }
@@ -2595,12 +2614,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2595
2614
  };
2596
2615
  zone.addEventListener("paste", onPaste);
2597
2616
  return () => zone.removeEventListener("paste", onPaste);
2598
- }, [screenshotPreview]);
2617
+ }, [shots.length]);
2599
2618
  const handleAnnotated = (annotated) => {
2600
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2601
- setScreenshotBlob(annotated);
2602
- setScreenshotPreview(URL.createObjectURL(annotated));
2603
- setAnnotatorOpen(false);
2619
+ const index = annotatingIndex;
2620
+ if (index === null || !shots[index]) {
2621
+ setAnnotatingIndex(null);
2622
+ return;
2623
+ }
2624
+ URL.revokeObjectURL(shots[index].preview);
2625
+ const replacement = { blob: annotated, preview: URL.createObjectURL(annotated) };
2626
+ setShots((prev) => prev.map((s, i) => i === index ? replacement : s));
2627
+ setAnnotatingIndex(null);
2604
2628
  };
2605
2629
  const handleSubmit = (e) => {
2606
2630
  e.preventDefault();
@@ -2614,8 +2638,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2614
2638
  feedback_type: feedbackType,
2615
2639
  severity
2616
2640
  };
2617
- if (screenshotBlob) {
2618
- values.screenshot = screenshotBlob;
2641
+ if (shots.length) {
2642
+ values.screenshots = shots.map((s) => s.blob);
2619
2643
  values.capture_method = "manual";
2620
2644
  }
2621
2645
  onSubmit(values);
@@ -2668,21 +2692,22 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2668
2692
  ref: fileInputRef,
2669
2693
  type: "file",
2670
2694
  accept: "image/png,image/jpeg,image/webp",
2695
+ multiple: true,
2671
2696
  class: "mfb-sr-only",
2672
2697
  onChange: handleFileInputChange,
2673
2698
  "aria-hidden": "true",
2674
2699
  tabIndex: -1
2675
2700
  }
2676
2701
  ),
2677
- screenshotPreview ? /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
2678
- /* @__PURE__ */ jsx8("img", { src: screenshotPreview, alt: "" }),
2702
+ shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
2703
+ /* @__PURE__ */ jsx8("img", { src: shot.preview, alt: "" }),
2679
2704
  /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview-actions", children: [
2680
2705
  /* @__PURE__ */ jsxs8(
2681
2706
  "button",
2682
2707
  {
2683
2708
  type: "button",
2684
2709
  class: "btn btn--primary screenshot-annotate",
2685
- onClick: () => setAnnotatorOpen(true),
2710
+ onClick: () => setAnnotatingIndex(index),
2686
2711
  children: [
2687
2712
  /* @__PURE__ */ jsxs8("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2688
2713
  /* @__PURE__ */ jsx8("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
@@ -2692,12 +2717,13 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2692
2717
  ]
2693
2718
  }
2694
2719
  ),
2695
- /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
2720
+ /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
2696
2721
  /* @__PURE__ */ jsx8("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx8("path", { d: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" }) }),
2697
2722
  strings["form.screenshot.remove"]
2698
2723
  ] })
2699
2724
  ] })
2700
- ] }) : /* @__PURE__ */ jsxs8(
2725
+ ] }, shot.preview)) }),
2726
+ shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs8(
2701
2727
  "div",
2702
2728
  {
2703
2729
  ref: dropZoneRef,
@@ -2742,12 +2768,12 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2742
2768
  /* @__PURE__ */ jsx8("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
2743
2769
  /* @__PURE__ */ jsx8("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
2744
2770
  ] }),
2745
- annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx8(
2771
+ annotatingIndex !== null && shots[annotatingIndex] && /* @__PURE__ */ jsx8(
2746
2772
  Annotator,
2747
2773
  {
2748
- imageBlob: screenshotBlob,
2774
+ imageBlob: shots[annotatingIndex].blob,
2749
2775
  strings,
2750
- onCancel: () => setAnnotatorOpen(false),
2776
+ onCancel: () => setAnnotatingIndex(null),
2751
2777
  onSave: handleAnnotated
2752
2778
  }
2753
2779
  )
@@ -3382,6 +3408,12 @@ var WIDGET_STYLES = `
3382
3408
  .screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
3383
3409
  .screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
3384
3410
 
3411
+ .screenshot-strip {
3412
+ display: flex;
3413
+ flex-direction: column;
3414
+ gap: var(--mfb-space-2);
3415
+ margin-bottom: var(--mfb-space-2);
3416
+ }
3385
3417
  .screenshot-preview {
3386
3418
  position: relative;
3387
3419
  border: 1px solid var(--mfb-border);
@@ -3399,6 +3431,10 @@ var WIDGET_STYLES = `
3399
3431
  object-fit: contain;
3400
3432
  background: #1a1a1a;
3401
3433
  }
3434
+ /* In the multi-shot strip each preview stays compact so several fit. */
3435
+ .screenshot-strip .screenshot-preview img {
3436
+ max-height: 120px;
3437
+ }
3402
3438
  .screenshot-preview-actions {
3403
3439
  display: flex;
3404
3440
  gap: var(--mfb-space-2);
@@ -4935,7 +4971,8 @@ function createFeedback(config) {
4935
4971
  document.body.appendChild(host);
4936
4972
  }
4937
4973
  async function buildAndSubmit(values) {
4938
- const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
4974
+ const attached = values.screenshots?.length ? values.screenshots : values.screenshot ? [values.screenshot] : void 0;
4975
+ const manualScreenshots = values.synthetic ? void 0 : attached;
4939
4976
  const technical_context = capture.snapshot();
4940
4977
  if (user) {
4941
4978
  const { userHash: _hash, exp: _exp, ...safeUser } = user;
@@ -4944,7 +4981,7 @@ function createFeedback(config) {
4944
4981
  if (metadata && Object.keys(metadata).length > 0) {
4945
4982
  technical_context.metadata = { ...metadata };
4946
4983
  }
4947
- const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
4984
+ const captureMethod = manualScreenshots?.length ? values.capture_method ?? "manual" : "none";
4948
4985
  const payload = {
4949
4986
  description: values.description,
4950
4987
  feedback_type: values.feedback_type ?? "bug",
@@ -4955,10 +4992,13 @@ function createFeedback(config) {
4955
4992
  capture_method: captureMethod,
4956
4993
  technical_context
4957
4994
  };
4958
- if ("0.23.0") {
4959
- payload.widget_version = "0.23.0";
4995
+ if ("0.24.0") {
4996
+ payload.widget_version = "0.24.0";
4997
+ }
4998
+ if (manualScreenshots?.length) {
4999
+ payload.screenshots = manualScreenshots;
5000
+ payload.screenshot = manualScreenshots[0];
4960
5001
  }
4961
- if (manualScreenshot) payload.screenshot = manualScreenshot;
4962
5002
  if (values.synthetic) payload.synthetic = true;
4963
5003
  const pagePath = currentPagePath(config);
4964
5004
  if (pagePath) payload.page_path = pagePath;
@@ -5044,7 +5084,8 @@ function createFeedback(config) {
5044
5084
  ...partial.feedback_type !== void 0 && { feedback_type: partial.feedback_type },
5045
5085
  ...partial.severity !== void 0 && { severity: partial.severity },
5046
5086
  ...partial.synthetic !== void 0 && { synthetic: partial.synthetic },
5047
- ...partial.screenshot !== void 0 && { screenshot: partial.screenshot }
5087
+ ...partial.screenshot !== void 0 && { screenshot: partial.screenshot },
5088
+ ...partial.screenshots !== void 0 && { screenshots: partial.screenshots }
5048
5089
  });
5049
5090
  },
5050
5091
  identify(u) {
@@ -5076,4 +5117,4 @@ function createFeedback(config) {
5076
5117
  export {
5077
5118
  createFeedback
5078
5119
  };
5079
- //# sourceMappingURL=chunk-NCNRU3SR.mjs.map
5120
+ //# sourceMappingURL=chunk-WIQ7MTPI.mjs.map