@mhosaic/feedback 0.23.0 → 0.25.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
@@ -1099,8 +1105,9 @@ function ReportDetailView({
1099
1105
  ] });
1100
1106
  }
1101
1107
  const isMine = detail.is_mine ?? canModerate;
1102
- const showCloseCta = isMine && detail.status === "awaiting_validation";
1103
- const showComposeBox = isMine;
1108
+ const canAct = detail.can_moderate ?? isMine;
1109
+ const showCloseCta = canAct && detail.status === "awaiting_validation";
1110
+ const showComposeBox = canAct;
1104
1111
  return /* @__PURE__ */ jsxs2("div", { class: `report-detail variant-${variant}`, children: [
1105
1112
  variant === "modal" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header", children: [
1106
1113
  /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
@@ -1128,8 +1135,8 @@ function ReportDetailView({
1128
1135
  /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
1129
1136
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1130
1137
  /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
1131
- detail.screenshot_url && (() => {
1132
- const safeHref = safeExternalHref(detail.screenshot_url);
1138
+ (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1139
+ const safeHref = safeExternalHref(url);
1133
1140
  return safeHref ? /* @__PURE__ */ jsx2(
1134
1141
  "a",
1135
1142
  {
@@ -1137,10 +1144,10 @@ function ReportDetailView({
1137
1144
  href: safeHref,
1138
1145
  target: "_blank",
1139
1146
  rel: "noopener noreferrer",
1140
- children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1147
+ children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" })
1141
1148
  }
1142
- ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" }) });
1143
- })(),
1149
+ ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" }) });
1150
+ }),
1144
1151
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1145
1152
  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
1153
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
@@ -1484,7 +1491,7 @@ function BoardView({ api, externalId, strings, getCurrentPage }) {
1484
1491
  reportId: selectedRow.id,
1485
1492
  strings,
1486
1493
  onBack: () => setSelectedId(null),
1487
- canModerate: selectedRow.is_mine,
1494
+ canModerate: selectedRow.can_moderate ?? selectedRow.is_mine,
1488
1495
  variant: "board"
1489
1496
  },
1490
1497
  detailKey
@@ -2519,45 +2526,58 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2519
2526
  const [feedbackType, setFeedbackType] = useState5("bug");
2520
2527
  const [severity, setSeverity] = useState5("medium");
2521
2528
  const [localError, setLocalError] = useState5("");
2522
- const [screenshotBlob, setScreenshotBlob] = useState5(null);
2523
- const [screenshotPreview, setScreenshotPreview] = useState5(null);
2529
+ const [shots, setShots] = useState5([]);
2524
2530
  const [isDragOver, setIsDragOver] = useState5(false);
2525
- const [annotatorOpen, setAnnotatorOpen] = useState5(false);
2531
+ const [annotatingIndex, setAnnotatingIndex] = useState5(null);
2526
2532
  const fileInputRef = useRef4(null);
2527
2533
  const dropZoneRef = useRef4(null);
2528
2534
  const submitting = status === "submitting";
2529
2535
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
2530
2536
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
2537
+ const shotsRef = useRef4(shots);
2538
+ shotsRef.current = shots;
2531
2539
  useEffect5(() => {
2532
2540
  return () => {
2533
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2541
+ shotsRef.current.forEach((s) => URL.revokeObjectURL(s.preview));
2534
2542
  };
2535
- }, [screenshotPreview]);
2536
- const acceptFile = (file) => {
2543
+ }, []);
2544
+ const acceptFiles = (files) => {
2537
2545
  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;
2546
+ const remaining = MAX_SCREENSHOTS - shots.length;
2547
+ const batch = files.slice(0, Math.max(0, remaining));
2548
+ for (const file of batch) {
2549
+ if (file instanceof File) {
2550
+ const err = validateScreenshotFile(file);
2551
+ if (err) {
2552
+ setLocalError(
2553
+ err.kind === "type" ? strings["form.screenshot.error_type"] : strings["form.screenshot.error_size"].replace("{max}", String(err.maxMb))
2554
+ );
2555
+ return;
2556
+ }
2545
2557
  }
2546
2558
  }
2547
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2548
- setScreenshotBlob(file);
2549
- setScreenshotPreview(URL.createObjectURL(file));
2559
+ if (batch.length) {
2560
+ const incoming = batch.map((blob) => ({ blob, preview: URL.createObjectURL(blob) }));
2561
+ setShots((prev) => [...prev, ...incoming]);
2562
+ }
2563
+ if (files.length > batch.length) {
2564
+ setLocalError(
2565
+ strings["form.screenshot.error_count"].replace("{max}", String(MAX_SCREENSHOTS))
2566
+ );
2567
+ }
2550
2568
  };
2551
- const clearScreenshot = () => {
2552
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2553
- setScreenshotPreview(null);
2554
- setScreenshotBlob(null);
2569
+ const removeShot = (index) => {
2570
+ const shot = shots[index];
2571
+ if (shot) URL.revokeObjectURL(shot.preview);
2572
+ setShots((prev) => prev.filter((_, i) => i !== index));
2555
2573
  setLocalError("");
2556
2574
  if (fileInputRef.current) fileInputRef.current.value = "";
2557
2575
  };
2558
2576
  const handleFileInputChange = (e) => {
2559
- const file = e.target.files?.[0];
2560
- if (file) acceptFile(file);
2577
+ const input = e.target;
2578
+ const files = Array.from(input.files ?? []);
2579
+ if (files.length) acceptFiles(files);
2580
+ input.value = "";
2561
2581
  };
2562
2582
  const handleDragOver = (e) => {
2563
2583
  e.preventDefault();
@@ -2573,8 +2593,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2573
2593
  e.preventDefault();
2574
2594
  e.stopPropagation();
2575
2595
  setIsDragOver(false);
2576
- const file = e.dataTransfer?.files?.[0];
2577
- if (file) acceptFile(file);
2596
+ const files = Array.from(e.dataTransfer?.files ?? []);
2597
+ if (files.length) acceptFiles(files);
2578
2598
  };
2579
2599
  useEffect5(() => {
2580
2600
  const zone = dropZoneRef.current;
@@ -2587,7 +2607,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2587
2607
  const file = item.getAsFile();
2588
2608
  if (file) {
2589
2609
  e.preventDefault();
2590
- acceptFile(file);
2610
+ acceptFiles([file]);
2591
2611
  return;
2592
2612
  }
2593
2613
  }
@@ -2595,12 +2615,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2595
2615
  };
2596
2616
  zone.addEventListener("paste", onPaste);
2597
2617
  return () => zone.removeEventListener("paste", onPaste);
2598
- }, [screenshotPreview]);
2618
+ }, [shots.length]);
2599
2619
  const handleAnnotated = (annotated) => {
2600
- if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
2601
- setScreenshotBlob(annotated);
2602
- setScreenshotPreview(URL.createObjectURL(annotated));
2603
- setAnnotatorOpen(false);
2620
+ const index = annotatingIndex;
2621
+ if (index === null || !shots[index]) {
2622
+ setAnnotatingIndex(null);
2623
+ return;
2624
+ }
2625
+ URL.revokeObjectURL(shots[index].preview);
2626
+ const replacement = { blob: annotated, preview: URL.createObjectURL(annotated) };
2627
+ setShots((prev) => prev.map((s, i) => i === index ? replacement : s));
2628
+ setAnnotatingIndex(null);
2604
2629
  };
2605
2630
  const handleSubmit = (e) => {
2606
2631
  e.preventDefault();
@@ -2614,8 +2639,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2614
2639
  feedback_type: feedbackType,
2615
2640
  severity
2616
2641
  };
2617
- if (screenshotBlob) {
2618
- values.screenshot = screenshotBlob;
2642
+ if (shots.length) {
2643
+ values.screenshots = shots.map((s) => s.blob);
2619
2644
  values.capture_method = "manual";
2620
2645
  }
2621
2646
  onSubmit(values);
@@ -2668,21 +2693,22 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2668
2693
  ref: fileInputRef,
2669
2694
  type: "file",
2670
2695
  accept: "image/png,image/jpeg,image/webp",
2696
+ multiple: true,
2671
2697
  class: "mfb-sr-only",
2672
2698
  onChange: handleFileInputChange,
2673
2699
  "aria-hidden": "true",
2674
2700
  tabIndex: -1
2675
2701
  }
2676
2702
  ),
2677
- screenshotPreview ? /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
2678
- /* @__PURE__ */ jsx8("img", { src: screenshotPreview, alt: "" }),
2703
+ shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
2704
+ /* @__PURE__ */ jsx8("img", { src: shot.preview, alt: "" }),
2679
2705
  /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview-actions", children: [
2680
2706
  /* @__PURE__ */ jsxs8(
2681
2707
  "button",
2682
2708
  {
2683
2709
  type: "button",
2684
2710
  class: "btn btn--primary screenshot-annotate",
2685
- onClick: () => setAnnotatorOpen(true),
2711
+ onClick: () => setAnnotatingIndex(index),
2686
2712
  children: [
2687
2713
  /* @__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
2714
  /* @__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 +2718,13 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2692
2718
  ]
2693
2719
  }
2694
2720
  ),
2695
- /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
2721
+ /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
2696
2722
  /* @__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
2723
  strings["form.screenshot.remove"]
2698
2724
  ] })
2699
2725
  ] })
2700
- ] }) : /* @__PURE__ */ jsxs8(
2726
+ ] }, shot.preview)) }),
2727
+ shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs8(
2701
2728
  "div",
2702
2729
  {
2703
2730
  ref: dropZoneRef,
@@ -2742,12 +2769,12 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2742
2769
  /* @__PURE__ */ jsx8("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
2743
2770
  /* @__PURE__ */ jsx8("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
2744
2771
  ] }),
2745
- annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx8(
2772
+ annotatingIndex !== null && shots[annotatingIndex] && /* @__PURE__ */ jsx8(
2746
2773
  Annotator,
2747
2774
  {
2748
- imageBlob: screenshotBlob,
2775
+ imageBlob: shots[annotatingIndex].blob,
2749
2776
  strings,
2750
- onCancel: () => setAnnotatorOpen(false),
2777
+ onCancel: () => setAnnotatingIndex(null),
2751
2778
  onSave: handleAnnotated
2752
2779
  }
2753
2780
  )
@@ -3382,6 +3409,12 @@ var WIDGET_STYLES = `
3382
3409
  .screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
3383
3410
  .screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
3384
3411
 
3412
+ .screenshot-strip {
3413
+ display: flex;
3414
+ flex-direction: column;
3415
+ gap: var(--mfb-space-2);
3416
+ margin-bottom: var(--mfb-space-2);
3417
+ }
3385
3418
  .screenshot-preview {
3386
3419
  position: relative;
3387
3420
  border: 1px solid var(--mfb-border);
@@ -3399,6 +3432,10 @@ var WIDGET_STYLES = `
3399
3432
  object-fit: contain;
3400
3433
  background: #1a1a1a;
3401
3434
  }
3435
+ /* In the multi-shot strip each preview stays compact so several fit. */
3436
+ .screenshot-strip .screenshot-preview img {
3437
+ max-height: 120px;
3438
+ }
3402
3439
  .screenshot-preview-actions {
3403
3440
  display: flex;
3404
3441
  gap: var(--mfb-space-2);
@@ -4935,7 +4972,8 @@ function createFeedback(config) {
4935
4972
  document.body.appendChild(host);
4936
4973
  }
4937
4974
  async function buildAndSubmit(values) {
4938
- const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
4975
+ const attached = values.screenshots?.length ? values.screenshots : values.screenshot ? [values.screenshot] : void 0;
4976
+ const manualScreenshots = values.synthetic ? void 0 : attached;
4939
4977
  const technical_context = capture.snapshot();
4940
4978
  if (user) {
4941
4979
  const { userHash: _hash, exp: _exp, ...safeUser } = user;
@@ -4944,7 +4982,7 @@ function createFeedback(config) {
4944
4982
  if (metadata && Object.keys(metadata).length > 0) {
4945
4983
  technical_context.metadata = { ...metadata };
4946
4984
  }
4947
- const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
4985
+ const captureMethod = manualScreenshots?.length ? values.capture_method ?? "manual" : "none";
4948
4986
  const payload = {
4949
4987
  description: values.description,
4950
4988
  feedback_type: values.feedback_type ?? "bug",
@@ -4955,10 +4993,13 @@ function createFeedback(config) {
4955
4993
  capture_method: captureMethod,
4956
4994
  technical_context
4957
4995
  };
4958
- if ("0.23.0") {
4959
- payload.widget_version = "0.23.0";
4996
+ if ("0.25.0") {
4997
+ payload.widget_version = "0.25.0";
4998
+ }
4999
+ if (manualScreenshots?.length) {
5000
+ payload.screenshots = manualScreenshots;
5001
+ payload.screenshot = manualScreenshots[0];
4960
5002
  }
4961
- if (manualScreenshot) payload.screenshot = manualScreenshot;
4962
5003
  if (values.synthetic) payload.synthetic = true;
4963
5004
  const pagePath = currentPagePath(config);
4964
5005
  if (pagePath) payload.page_path = pagePath;
@@ -5044,7 +5085,8 @@ function createFeedback(config) {
5044
5085
  ...partial.feedback_type !== void 0 && { feedback_type: partial.feedback_type },
5045
5086
  ...partial.severity !== void 0 && { severity: partial.severity },
5046
5087
  ...partial.synthetic !== void 0 && { synthetic: partial.synthetic },
5047
- ...partial.screenshot !== void 0 && { screenshot: partial.screenshot }
5088
+ ...partial.screenshot !== void 0 && { screenshot: partial.screenshot },
5089
+ ...partial.screenshots !== void 0 && { screenshots: partial.screenshots }
5048
5090
  });
5049
5091
  },
5050
5092
  identify(u) {
@@ -5076,4 +5118,4 @@ function createFeedback(config) {
5076
5118
  export {
5077
5119
  createFeedback
5078
5120
  };
5079
- //# sourceMappingURL=chunk-NCNRU3SR.mjs.map
5121
+ //# sourceMappingURL=chunk-BGS3FOQY.mjs.map