@mhosaic/feedback 0.28.0 → 0.30.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.
@@ -167,10 +167,18 @@ function createApiClient(options) {
167
167
  );
168
168
  return readJsonObject(response, "getReportBySeq");
169
169
  }
170
- async function addComment(reportId, externalId, body, clientNonce) {
171
- const response = await fetcher(
172
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
173
- {
170
+ async function addComment(reportId, externalId, body, clientNonce, attachments) {
171
+ let init;
172
+ if (attachments && attachments.length > 0) {
173
+ const form = new FormData();
174
+ form.append("body", body);
175
+ if (clientNonce !== void 0) form.append("client_nonce", clientNonce);
176
+ attachments.forEach(
177
+ (blob, i) => form.append("attachment", blob, `attachment-${i + 1}.png`)
178
+ );
179
+ init = { method: "POST", headers: widgetHeaders(externalId), body: form };
180
+ } else {
181
+ init = {
174
182
  method: "POST",
175
183
  headers: {
176
184
  ...widgetHeaders(externalId),
@@ -180,7 +188,11 @@ function createApiClient(options) {
180
188
  body,
181
189
  ...clientNonce !== void 0 && { client_nonce: clientNonce }
182
190
  })
183
- }
191
+ };
192
+ }
193
+ const response = await fetcher(
194
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
195
+ init
184
196
  );
185
197
  if (!response.ok) {
186
198
  const text = await response.text().catch(() => "");
@@ -188,6 +200,24 @@ function createApiClient(options) {
188
200
  }
189
201
  return response.json();
190
202
  }
203
+ async function editDescription(reportId, externalId, description) {
204
+ const response = await fetcher(
205
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
206
+ {
207
+ method: "PATCH",
208
+ headers: {
209
+ ...widgetHeaders(externalId),
210
+ "Content-Type": "application/json"
211
+ },
212
+ body: JSON.stringify({ description })
213
+ }
214
+ );
215
+ if (!response.ok) {
216
+ const text = await response.text().catch(() => "");
217
+ throw new Error(`editDescription failed: ${response.status} ${text}`);
218
+ }
219
+ return response.json();
220
+ }
191
221
  async function closeAsResolved(reportId, externalId) {
192
222
  const response = await fetcher(
193
223
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
@@ -291,6 +321,7 @@ function createApiClient(options) {
291
321
  getReport,
292
322
  getReportBySeq,
293
323
  addComment,
324
+ editDescription,
294
325
  closeAsResolved,
295
326
  reopenUnresolved,
296
327
  listBoard,
@@ -697,6 +728,10 @@ var DEFAULT_STRINGS = {
697
728
  "mine.refresh": "Refresh",
698
729
  "mine.loading": "Loading\u2026",
699
730
  "mine.error": "Could not load your reports.",
731
+ "mine.jump.placeholder": "#",
732
+ "mine.jump.go": "Open",
733
+ "mine.jump.aria": "Open a report by number",
734
+ "mine.jump.not_found": "No report with that number.",
700
735
  "error.rate_limited": "Too many requests \u2014 retry in {seconds}s.",
701
736
  "error.rate_limited_generic": "Too many requests \u2014 try again in a moment.",
702
737
  "mine.replies_one": "1 reply",
@@ -712,6 +747,16 @@ var DEFAULT_STRINGS = {
712
747
  "detail.compose_placeholder": "Add a follow-up reply\u2026",
713
748
  "detail.compose_send": "Reply",
714
749
  "detail.compose_sending": "Sending\u2026",
750
+ "detail.attach_add": "Add image",
751
+ "detail.attach_remove": "Remove",
752
+ "detail.attachment_alt": "Attached image",
753
+ "detail.copy_link": "Copy link",
754
+ "detail.copied": "Link copied",
755
+ "detail.edit": "Edit",
756
+ "detail.edit_save": "Save",
757
+ "detail.edit_cancel": "Cancel",
758
+ "detail.edit_saving": "Saving\u2026",
759
+ "detail.edit_failed": "Could not save your changes.",
715
760
  "detail.close_cta": "Mark as resolved",
716
761
  "detail.close_busy": "Marking\u2026",
717
762
  "detail.reopen_cta": "Still not fixed",
@@ -869,6 +914,10 @@ var FRENCH_STRINGS = {
869
914
  "mine.refresh": "Actualiser",
870
915
  "mine.loading": "Chargement\u2026",
871
916
  "mine.error": "Impossible de charger vos rapports.",
917
+ "mine.jump.placeholder": "#",
918
+ "mine.jump.go": "Ouvrir",
919
+ "mine.jump.aria": "Ouvrir un rapport par num\xE9ro",
920
+ "mine.jump.not_found": "Aucun rapport avec ce num\xE9ro.",
872
921
  "error.rate_limited": "Trop de requ\xEAtes \u2014 r\xE9essaie dans {seconds} s.",
873
922
  "error.rate_limited_generic": "Trop de requ\xEAtes \u2014 r\xE9essaie dans un instant.",
874
923
  "mine.replies_one": "1 r\xE9ponse",
@@ -884,6 +933,16 @@ var FRENCH_STRINGS = {
884
933
  "detail.compose_placeholder": "Ajouter une r\xE9ponse\u2026",
885
934
  "detail.compose_send": "R\xE9pondre",
886
935
  "detail.compose_sending": "Envoi\u2026",
936
+ "detail.attach_add": "Ajouter une image",
937
+ "detail.attach_remove": "Retirer",
938
+ "detail.attachment_alt": "Image jointe",
939
+ "detail.copy_link": "Copier le lien",
940
+ "detail.copied": "Lien copi\xE9",
941
+ "detail.edit": "Modifier",
942
+ "detail.edit_save": "Enregistrer",
943
+ "detail.edit_cancel": "Annuler",
944
+ "detail.edit_saving": "Enregistrement\u2026",
945
+ "detail.edit_failed": "Impossible d\u2019enregistrer vos modifications.",
887
946
  "detail.close_cta": "Marquer comme r\xE9solu",
888
947
  "detail.close_busy": "Validation\u2026",
889
948
  "detail.reopen_cta": "Toujours pas r\xE9gl\xE9",
@@ -1094,9 +1153,21 @@ function CommentBubble({ comment, strings, onOpenSeq }) {
1094
1153
  const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1095
1154
  const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1096
1155
  const label = comment.author_label || strings[labelKey];
1156
+ const images = (comment.attachments ?? []).filter((a) => a.url);
1097
1157
  return /* @__PURE__ */ jsxs2("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1098
1158
  !isMine && label && /* @__PURE__ */ jsx("div", { class: `comment-author comment-author--${variant}`, children: label }),
1099
- /* @__PURE__ */ jsx("div", { class: "comment-body", children: linkifySeq(comment.body, onOpenSeq) }),
1159
+ comment.body && /* @__PURE__ */ jsx("div", { class: "comment-body", children: linkifySeq(comment.body, onOpenSeq) }),
1160
+ images.length > 0 && /* @__PURE__ */ jsx("div", { class: "comment-attachments", children: images.map((a) => /* @__PURE__ */ jsx(
1161
+ "a",
1162
+ {
1163
+ class: "comment-attachment",
1164
+ href: a.url,
1165
+ target: "_blank",
1166
+ rel: "noopener noreferrer",
1167
+ children: /* @__PURE__ */ jsx("img", { src: a.url, alt: strings["detail.attachment_alt"], loading: "lazy" })
1168
+ },
1169
+ a.id
1170
+ )) }),
1100
1171
  /* @__PURE__ */ jsx("div", { class: "comment-time", children: formatTime(comment.created_at) })
1101
1172
  ] });
1102
1173
  }
@@ -1162,15 +1233,67 @@ function ReportDetailView({
1162
1233
  onBack,
1163
1234
  canModerate = true,
1164
1235
  variant = "modal",
1165
- onOpenSeq
1236
+ onOpenSeq,
1237
+ buildPermalink
1166
1238
  }) {
1167
1239
  const [detail, setDetail] = useState(null);
1168
1240
  const [error, setError] = useState(null);
1241
+ const [copied, setCopied] = useState(false);
1242
+ const [editing, setEditing] = useState(false);
1243
+ const [editBody, setEditBody] = useState("");
1244
+ const [savingEdit, setSavingEdit] = useState(false);
1245
+ const [editError, setEditError] = useState(false);
1169
1246
  const [composeBody, setComposeBody] = useState("");
1170
1247
  const [sending, setSending] = useState(false);
1171
1248
  const [closing, setClosing] = useState(false);
1172
1249
  const [reopening, setReopening] = useState(false);
1250
+ const [composeShots, setComposeShots] = useState([]);
1251
+ const [attachError, setAttachError] = useState("");
1252
+ const fileInputRef = useRef(null);
1173
1253
  const mountedRef = useRef(true);
1254
+ const shotsRef = useRef(composeShots);
1255
+ shotsRef.current = composeShots;
1256
+ useEffect(() => {
1257
+ return () => shotsRef.current.forEach((s) => URL.revokeObjectURL(s.preview));
1258
+ }, []);
1259
+ const acceptComposeFiles = (files) => {
1260
+ setAttachError("");
1261
+ const remaining = MAX_SCREENSHOTS - composeShots.length;
1262
+ const batch = files.slice(0, Math.max(0, remaining));
1263
+ for (const file of batch) {
1264
+ const err = validateScreenshotFile(file);
1265
+ if (err) {
1266
+ setAttachError(
1267
+ err.kind === "type" ? strings["form.screenshot.error_type"] : strings["form.screenshot.error_size"].replace("{max}", String(err.maxMb))
1268
+ );
1269
+ return;
1270
+ }
1271
+ }
1272
+ if (batch.length) {
1273
+ const incoming = batch.map((blob) => ({
1274
+ blob,
1275
+ preview: URL.createObjectURL(blob)
1276
+ }));
1277
+ setComposeShots((prev) => [...prev, ...incoming]);
1278
+ }
1279
+ if (files.length > batch.length) {
1280
+ setAttachError(
1281
+ strings["form.screenshot.error_count"].replace("{max}", String(MAX_SCREENSHOTS))
1282
+ );
1283
+ }
1284
+ };
1285
+ const handleAttachChange = (e) => {
1286
+ const input = e.target;
1287
+ const files = Array.from(input.files ?? []);
1288
+ if (files.length) acceptComposeFiles(files);
1289
+ input.value = "";
1290
+ };
1291
+ const removeComposeShot = (index) => {
1292
+ const shot = composeShots[index];
1293
+ if (shot) URL.revokeObjectURL(shot.preview);
1294
+ setComposeShots((prev) => prev.filter((_, i) => i !== index));
1295
+ setAttachError("");
1296
+ };
1174
1297
  const fetchDetail = async () => {
1175
1298
  try {
1176
1299
  const next = await api.getReport(reportId, externalId);
@@ -1195,13 +1318,23 @@ function ReportDetailView({
1195
1318
  };
1196
1319
  }, [reportId, externalId]);
1197
1320
  const handleSend = async () => {
1198
- if (!composeBody.trim() || sending) return;
1321
+ if (!composeBody.trim() && composeShots.length === 0 || sending) return;
1199
1322
  setSending(true);
1200
1323
  try {
1201
1324
  const nonce = `${reportId}:${Date.now()}`;
1202
- const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
1325
+ const attachments = composeShots.map((s) => s.blob);
1326
+ const created = await api.addComment(
1327
+ reportId,
1328
+ externalId,
1329
+ composeBody.trim(),
1330
+ nonce,
1331
+ attachments.length ? attachments : void 0
1332
+ );
1203
1333
  if (!mountedRef.current) return;
1204
1334
  setComposeBody("");
1335
+ composeShots.forEach((s) => URL.revokeObjectURL(s.preview));
1336
+ setComposeShots([]);
1337
+ setAttachError("");
1205
1338
  setDetail(
1206
1339
  (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
1207
1340
  );
@@ -1214,6 +1347,47 @@ function ReportDetailView({
1214
1347
  if (mountedRef.current) setSending(false);
1215
1348
  }
1216
1349
  };
1350
+ const handleCopyLink = async () => {
1351
+ if (!buildPermalink || !detail) return;
1352
+ try {
1353
+ await navigator.clipboard.writeText(buildPermalink(detail.id));
1354
+ if (!mountedRef.current) return;
1355
+ setCopied(true);
1356
+ setTimeout(() => {
1357
+ if (mountedRef.current) setCopied(false);
1358
+ }, 2e3);
1359
+ } catch {
1360
+ }
1361
+ };
1362
+ const startEdit = () => {
1363
+ if (!detail) return;
1364
+ setEditBody(detail.description);
1365
+ setEditError(false);
1366
+ setEditing(true);
1367
+ };
1368
+ const cancelEdit = () => {
1369
+ setEditing(false);
1370
+ setEditError(false);
1371
+ };
1372
+ const handleSaveEdit = async () => {
1373
+ if (!detail || savingEdit) return;
1374
+ const next = editBody.trim();
1375
+ if (!next) return;
1376
+ setSavingEdit(true);
1377
+ setEditError(false);
1378
+ try {
1379
+ const updated = await api.editDescription(reportId, externalId, next);
1380
+ if (!mountedRef.current) return;
1381
+ setDetail(updated);
1382
+ setEditing(false);
1383
+ } catch (err) {
1384
+ if (typeof console !== "undefined") console.warn("[mhosaic] editDescription:", err);
1385
+ if (!mountedRef.current) return;
1386
+ setEditError(true);
1387
+ } finally {
1388
+ if (mountedRef.current) setSavingEdit(false);
1389
+ }
1390
+ };
1217
1391
  const handleClose = async () => {
1218
1392
  if (closing || reopening) return;
1219
1393
  setClosing(true);
@@ -1299,7 +1473,63 @@ function ReportDetailView({
1299
1473
  ] }),
1300
1474
  /* @__PURE__ */ jsxs3("div", { class: "report-detail-body", children: [
1301
1475
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1302
- /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: linkifySeq(detail.description, onOpenSeq) }),
1476
+ editing ? /* @__PURE__ */ jsxs3("div", { class: "report-detail-edit", children: [
1477
+ /* @__PURE__ */ jsx2(
1478
+ "textarea",
1479
+ {
1480
+ class: "report-detail-edit-input",
1481
+ value: editBody,
1482
+ onInput: (e) => {
1483
+ setEditBody(e.target.value);
1484
+ setEditError(false);
1485
+ },
1486
+ disabled: savingEdit
1487
+ }
1488
+ ),
1489
+ editError && /* @__PURE__ */ jsx2("p", { class: "error", role: "alert", children: strings["detail.edit_failed"] }),
1490
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-edit-actions", children: [
1491
+ /* @__PURE__ */ jsx2(
1492
+ "button",
1493
+ {
1494
+ type: "button",
1495
+ class: "btn btn--ghost",
1496
+ onClick: cancelEdit,
1497
+ disabled: savingEdit,
1498
+ children: strings["detail.edit_cancel"]
1499
+ }
1500
+ ),
1501
+ /* @__PURE__ */ jsx2(
1502
+ "button",
1503
+ {
1504
+ type: "button",
1505
+ class: "btn btn--primary",
1506
+ onClick: handleSaveEdit,
1507
+ disabled: savingEdit || !editBody.trim(),
1508
+ children: savingEdit ? strings["detail.edit_saving"] : strings["detail.edit_save"]
1509
+ }
1510
+ )
1511
+ ] })
1512
+ ] }) : /* @__PURE__ */ jsxs3("div", { class: "report-detail-description-row", children: [
1513
+ /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: linkifySeq(detail.description, onOpenSeq) }),
1514
+ isMine && /* @__PURE__ */ jsx2(
1515
+ "button",
1516
+ {
1517
+ type: "button",
1518
+ class: "btn btn--ghost report-detail-edit",
1519
+ onClick: startEdit,
1520
+ children: strings["detail.edit"]
1521
+ }
1522
+ )
1523
+ ] }),
1524
+ buildPermalink && /* @__PURE__ */ jsx2("div", { class: "report-detail-permalink", children: /* @__PURE__ */ jsx2(
1525
+ "button",
1526
+ {
1527
+ type: "button",
1528
+ class: "btn btn--ghost report-detail-copy-link",
1529
+ onClick: handleCopyLink,
1530
+ children: copied ? strings["detail.copied"] : strings["detail.copy_link"]
1531
+ }
1532
+ ) }),
1303
1533
  (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1304
1534
  const safeHref = safeExternalHref(url);
1305
1535
  return safeHref ? /* @__PURE__ */ jsx2(
@@ -1334,7 +1564,44 @@ function ReportDetailView({
1334
1564
  disabled: sending
1335
1565
  }
1336
1566
  ),
1567
+ composeShots.length > 0 && /* @__PURE__ */ jsx2("div", { class: "compose-attachments", children: composeShots.map((s, i) => /* @__PURE__ */ jsxs3("div", { class: "compose-attachment", children: [
1568
+ /* @__PURE__ */ jsx2("img", { src: s.preview, alt: strings["detail.attachment_alt"] }),
1569
+ /* @__PURE__ */ jsx2(
1570
+ "button",
1571
+ {
1572
+ type: "button",
1573
+ class: "compose-attachment-remove",
1574
+ "aria-label": strings["detail.attach_remove"],
1575
+ onClick: () => removeComposeShot(i),
1576
+ disabled: sending,
1577
+ children: "\xD7"
1578
+ }
1579
+ )
1580
+ ] }, s.preview)) }),
1581
+ attachError && /* @__PURE__ */ jsx2("p", { class: "compose-attach-error", role: "alert", children: attachError }),
1582
+ /* @__PURE__ */ jsx2(
1583
+ "input",
1584
+ {
1585
+ ref: fileInputRef,
1586
+ type: "file",
1587
+ accept: "image/png,image/jpeg,image/webp",
1588
+ multiple: true,
1589
+ class: "compose-attach-input",
1590
+ onChange: handleAttachChange,
1591
+ hidden: true
1592
+ }
1593
+ ),
1337
1594
  /* @__PURE__ */ jsxs3("div", { class: "report-compose-actions", children: [
1595
+ /* @__PURE__ */ jsx2(
1596
+ "button",
1597
+ {
1598
+ type: "button",
1599
+ class: "btn btn--ghost compose-attach-btn",
1600
+ onClick: () => fileInputRef.current?.click(),
1601
+ disabled: sending || composeShots.length >= MAX_SCREENSHOTS,
1602
+ children: strings["detail.attach_add"]
1603
+ }
1604
+ ),
1338
1605
  showCloseCta && /* @__PURE__ */ jsx2(
1339
1606
  "button",
1340
1607
  {
@@ -1361,7 +1628,7 @@ function ReportDetailView({
1361
1628
  type: "button",
1362
1629
  class: "btn btn--primary",
1363
1630
  onClick: handleSend,
1364
- disabled: !composeBody.trim() || sending,
1631
+ disabled: !composeBody.trim() && composeShots.length === 0 || sending,
1365
1632
  children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
1366
1633
  }
1367
1634
  )
@@ -1540,6 +1807,7 @@ function StatusHistorySection({ rows, strings }) {
1540
1807
  // src/widget/BoardView.tsx
1541
1808
  import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs4 } from "preact/jsx-runtime";
1542
1809
  var POLL_MS2 = 3e4;
1810
+ var SEARCH_DEBOUNCE_MS = 300;
1543
1811
  var SORT_OPTIONS = ["recent", "oldest", "updated", "severity", "status"];
1544
1812
  var STATUSES = [
1545
1813
  "new",
@@ -1569,6 +1837,22 @@ function BoardView({
1569
1837
  const [selectedId, setSelectedId] = useState2(initialSelectedId ?? null);
1570
1838
  const [detailKey, setDetailKey] = useState2(0);
1571
1839
  const [reloadTick, setReloadTick] = useState2(0);
1840
+ const [qDraft, setQDraft] = useState2(filters.q ?? "");
1841
+ const debouncedQ = useDebouncedValue(qDraft, SEARCH_DEBOUNCE_MS);
1842
+ useEffect2(() => {
1843
+ setFilters((f) => {
1844
+ if ((f.q ?? "") === debouncedQ) return f;
1845
+ if (debouncedQ === "") {
1846
+ const { q: _drop, ...rest } = f;
1847
+ void _drop;
1848
+ return rest;
1849
+ }
1850
+ return { ...f, q: debouncedQ };
1851
+ });
1852
+ }, [debouncedQ]);
1853
+ useEffect2(() => {
1854
+ setQDraft(filters.q ?? "");
1855
+ }, [filters.q]);
1572
1856
  const activeFilterCount = useMemo(() => {
1573
1857
  return (filters.status?.length ?? 0) + (filters.type?.length ?? 0) + (filters.severity?.length ?? 0) + (filters.q ? 1 : 0) + (filters.mine ? 1 : 0);
1574
1858
  }, [filters]);
@@ -1622,6 +1906,8 @@ function BoardView({
1622
1906
  {
1623
1907
  filters,
1624
1908
  onChange: setFilters,
1909
+ searchDraft: qDraft,
1910
+ onSearchDraftChange: setQDraft,
1625
1911
  activeCount: activeFilterCount,
1626
1912
  strings,
1627
1913
  thisPage,
@@ -1748,6 +2034,8 @@ function BoardKpiStrip({
1748
2034
  function BoardFilters({
1749
2035
  filters,
1750
2036
  onChange,
2037
+ searchDraft,
2038
+ onSearchDraftChange,
1751
2039
  activeCount,
1752
2040
  strings,
1753
2041
  thisPage,
@@ -1781,7 +2069,6 @@ function BoardFilters({
1781
2069
  }
1782
2070
  };
1783
2071
  const setOrdering = (value) => onChange({ ...filters, ordering: value });
1784
- const setSearch = (q) => onChange({ ...filters, q });
1785
2072
  const toggleMine = () => {
1786
2073
  if (filters.mine) {
1787
2074
  const { mine: _drop, ...rest } = filters;
@@ -1870,8 +2157,8 @@ function BoardFilters({
1870
2157
  type: "search",
1871
2158
  class: "board-filter-search",
1872
2159
  placeholder: strings["board.filter.search.placeholder"],
1873
- value: filters.q ?? "",
1874
- onInput: (e) => setSearch(e.target.value)
2160
+ value: searchDraft,
2161
+ onInput: (e) => onSearchDraftChange(e.target.value)
1875
2162
  }
1876
2163
  ),
1877
2164
  /* @__PURE__ */ jsxs4("label", { class: "board-filter-toggle", children: [
@@ -1988,6 +2275,14 @@ function DetailEmptyIllustration() {
1988
2275
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "38", x2: "32", y2: "38", stroke: "currentColor", "stroke-opacity": "0.14", "stroke-width": "2", "stroke-linecap": "round" })
1989
2276
  ] });
1990
2277
  }
2278
+ function useDebouncedValue(value, ms) {
2279
+ const [debounced, setDebounced] = useState2(value);
2280
+ useEffect2(() => {
2281
+ const t = setTimeout(() => setDebounced(value), ms);
2282
+ return () => clearTimeout(t);
2283
+ }, [value, ms]);
2284
+ return debounced;
2285
+ }
1991
2286
  function filtersHash(f) {
1992
2287
  return JSON.stringify({
1993
2288
  s: f.status?.slice().sort(),
@@ -3096,12 +3391,34 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
3096
3391
  // src/widget/MineList.tsx
3097
3392
  import { jsx as jsx10, jsxs as jsxs11 } from "preact/jsx-runtime";
3098
3393
  var POLL_MS4 = 3e4;
3099
- function MineList({ api, externalId, strings, onSelect }) {
3394
+ function MineList({ api, externalId, strings, onSelect, onOpenSeq }) {
3100
3395
  const [rows, setRows] = useState6(null);
3101
3396
  const [error, setError] = useState6(null);
3102
3397
  const [refreshing, setRefreshing] = useState6(false);
3103
3398
  const [filter, setFilter] = useState6("all");
3399
+ const [jumpValue, setJumpValue] = useState6("");
3400
+ const [jumpError, setJumpError] = useState6(false);
3401
+ const [jumping, setJumping] = useState6(false);
3104
3402
  const mountedRef = useRef5(true);
3403
+ const handleJump = async (e) => {
3404
+ e.preventDefault();
3405
+ if (!onOpenSeq || jumping) return;
3406
+ const seq = parseInt(jumpValue.replace(/^#/, "").trim(), 10);
3407
+ if (!Number.isInteger(seq) || seq <= 0) {
3408
+ setJumpError(true);
3409
+ return;
3410
+ }
3411
+ setJumping(true);
3412
+ setJumpError(false);
3413
+ try {
3414
+ const found = await onOpenSeq(seq);
3415
+ if (!mountedRef.current) return;
3416
+ if (found) setJumpValue("");
3417
+ else setJumpError(true);
3418
+ } finally {
3419
+ if (mountedRef.current) setJumping(false);
3420
+ }
3421
+ };
3105
3422
  const fetchRows = async () => {
3106
3423
  setRefreshing(true);
3107
3424
  setError(null);
@@ -3134,19 +3451,40 @@ function MineList({ api, externalId, strings, onSelect }) {
3134
3451
  return /* @__PURE__ */ jsxs11("div", { class: "mine-list", children: [
3135
3452
  /* @__PURE__ */ jsxs11("div", { class: "mine-list-header", children: [
3136
3453
  /* @__PURE__ */ jsx10("h2", { children: strings["tab.mine"] }),
3137
- /* @__PURE__ */ jsx10(
3138
- "button",
3139
- {
3140
- type: "button",
3141
- class: "btn",
3142
- onClick: () => {
3143
- void fetchRows();
3144
- },
3145
- disabled: refreshing,
3146
- children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
3147
- }
3148
- )
3454
+ /* @__PURE__ */ jsxs11("div", { class: "mine-list-header-actions", children: [
3455
+ onOpenSeq && /* @__PURE__ */ jsxs11("form", { class: "mine-jump", onSubmit: handleJump, children: [
3456
+ /* @__PURE__ */ jsx10(
3457
+ "input",
3458
+ {
3459
+ type: "text",
3460
+ inputMode: "numeric",
3461
+ value: jumpValue,
3462
+ placeholder: strings["mine.jump.placeholder"],
3463
+ "aria-label": strings["mine.jump.aria"],
3464
+ onInput: (e) => {
3465
+ setJumpValue(e.target.value);
3466
+ setJumpError(false);
3467
+ },
3468
+ disabled: jumping
3469
+ }
3470
+ ),
3471
+ /* @__PURE__ */ jsx10("button", { type: "submit", class: "btn", disabled: jumping || !jumpValue.trim(), children: strings["mine.jump.go"] })
3472
+ ] }),
3473
+ /* @__PURE__ */ jsx10(
3474
+ "button",
3475
+ {
3476
+ type: "button",
3477
+ class: "btn",
3478
+ onClick: () => {
3479
+ void fetchRows();
3480
+ },
3481
+ disabled: refreshing,
3482
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
3483
+ }
3484
+ )
3485
+ ] })
3149
3486
  ] }),
3487
+ jumpError && /* @__PURE__ */ jsx10("div", { class: "mine-jump-error error", role: "alert", children: strings["mine.jump.not_found"] }),
3150
3488
  rows && rows.length > 0 && /* @__PURE__ */ jsx10(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
3151
3489
  isLoading && /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] }),
3152
3490
  error && /* @__PURE__ */ jsx10("div", { class: "error", children: error }),
@@ -4137,6 +4475,31 @@ var WIDGET_STYLES = `
4137
4475
  justify-content: space-between;
4138
4476
  }
4139
4477
  .mine-list-header h2 { margin: 0; font-size: var(--mfb-text-lg); font-weight: 600; letter-spacing: -0.01em; }
4478
+ /* #85 \u2014 header actions: jump-by-#seq box + refresh. */
4479
+ .mine-list-header-actions {
4480
+ display: flex;
4481
+ align-items: center;
4482
+ gap: 6px;
4483
+ }
4484
+ .mine-jump {
4485
+ display: flex;
4486
+ align-items: center;
4487
+ gap: 4px;
4488
+ }
4489
+ .mine-jump input {
4490
+ font: inherit;
4491
+ font-size: 13px;
4492
+ width: 56px;
4493
+ padding: 5px 8px;
4494
+ border: 1px solid var(--mfb-border);
4495
+ border-radius: var(--mfb-radius);
4496
+ background: var(--mfb-surface);
4497
+ color: inherit;
4498
+ }
4499
+ .mine-jump-error {
4500
+ margin-top: 6px;
4501
+ font-size: 12px;
4502
+ }
4140
4503
  .mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
4141
4504
  .mine-empty {
4142
4505
  text-align: center;
@@ -4342,12 +4705,77 @@ var WIDGET_STYLES = `
4342
4705
  .comment-author--staff { color: #1e40af; }
4343
4706
  .comment-author--system { color: var(--mfb-text-muted); }
4344
4707
  .comment-body { white-space: pre-wrap; }
4708
+ /* #91 \u2014 image attachments rendered inside a comment bubble. */
4709
+ .comment-attachments {
4710
+ display: flex;
4711
+ flex-wrap: wrap;
4712
+ gap: 6px;
4713
+ margin-top: 6px;
4714
+ }
4715
+ .comment-attachment {
4716
+ display: block;
4717
+ line-height: 0;
4718
+ border-radius: 8px;
4719
+ overflow: hidden;
4720
+ border: 1px solid var(--mfb-border);
4721
+ }
4722
+ .comment-attachment img {
4723
+ width: 96px;
4724
+ height: 96px;
4725
+ object-fit: cover;
4726
+ display: block;
4727
+ }
4345
4728
  .comment-time {
4346
4729
  font-size: 10px;
4347
4730
  margin-top: 4px;
4348
4731
  opacity: 0.7;
4349
4732
  }
4350
4733
 
4734
+ /* F4 \u2014 author edits the description in place. */
4735
+ .report-detail-description-row {
4736
+ display: flex;
4737
+ align-items: flex-start;
4738
+ gap: 8px;
4739
+ }
4740
+ .report-detail-description-row .report-detail-description {
4741
+ flex: 1;
4742
+ margin: 0;
4743
+ }
4744
+ .report-detail-edit {
4745
+ display: flex;
4746
+ flex-direction: column;
4747
+ gap: 6px;
4748
+ }
4749
+ .report-detail-edit-input {
4750
+ font: inherit;
4751
+ font-size: 13px;
4752
+ padding: 8px 10px;
4753
+ border: 1px solid var(--mfb-border);
4754
+ border-radius: var(--mfb-radius);
4755
+ background: var(--mfb-surface);
4756
+ color: inherit;
4757
+ min-height: 72px;
4758
+ resize: vertical;
4759
+ }
4760
+ .report-detail-edit-actions {
4761
+ display: flex;
4762
+ justify-content: flex-end;
4763
+ gap: 6px;
4764
+ }
4765
+ .report-detail-edit.btn,
4766
+ button.report-detail-edit {
4767
+ flex: 0 0 auto;
4768
+ font-size: 12px;
4769
+ padding: 4px 8px;
4770
+ }
4771
+ /* #85 \u2014 shareable-permalink affordance under the description. */
4772
+ .report-detail-permalink {
4773
+ margin: -2px 0 6px;
4774
+ }
4775
+ .report-detail-copy-link {
4776
+ font-size: 12px;
4777
+ padding: 4px 8px;
4778
+ }
4351
4779
  .report-compose {
4352
4780
  display: flex;
4353
4781
  flex-direction: column;
@@ -4370,6 +4798,50 @@ var WIDGET_STYLES = `
4370
4798
  justify-content: flex-end;
4371
4799
  gap: 6px;
4372
4800
  }
4801
+ /* #91 \u2014 pending image attachments in the reply compose box. */
4802
+ .compose-attachments {
4803
+ display: flex;
4804
+ flex-wrap: wrap;
4805
+ gap: 6px;
4806
+ }
4807
+ .compose-attachment {
4808
+ position: relative;
4809
+ width: 64px;
4810
+ height: 64px;
4811
+ border-radius: 8px;
4812
+ overflow: hidden;
4813
+ border: 1px solid var(--mfb-border);
4814
+ }
4815
+ .compose-attachment img {
4816
+ width: 100%;
4817
+ height: 100%;
4818
+ object-fit: cover;
4819
+ display: block;
4820
+ }
4821
+ .compose-attachment-remove {
4822
+ position: absolute;
4823
+ top: 2px;
4824
+ right: 2px;
4825
+ width: 20px;
4826
+ height: 20px;
4827
+ display: grid;
4828
+ place-items: center;
4829
+ padding: 0;
4830
+ background: rgba(255, 255, 255, 0.92);
4831
+ border: 1px solid var(--mfb-border);
4832
+ border-radius: 999px;
4833
+ font-size: 15px;
4834
+ line-height: 1;
4835
+ cursor: pointer;
4836
+ color: #111827;
4837
+ }
4838
+ .compose-attachment-remove:hover { background: #fff; }
4839
+ .compose-attach-btn { margin-right: auto; }
4840
+ .compose-attach-error {
4841
+ margin: 0;
4842
+ font-size: 12px;
4843
+ color: var(--mfb-danger, #b91c1c);
4844
+ }
4373
4845
 
4374
4846
  /* v0.15.3 \u2014 teammate-viewing notice. Shown in place of the compose
4375
4847
  * box when a non-submitter opens a project-wide row from the Board
@@ -5342,6 +5814,23 @@ function mountWidget(options) {
5342
5814
  }
5343
5815
  }, []);
5344
5816
  const externalId = options.getExternalId?.();
5817
+ const openSeq = (seq) => {
5818
+ if (!options.api || !externalId) return Promise.resolve(false);
5819
+ return options.api.getReportBySeq(seq, externalId).then((r) => {
5820
+ rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id });
5821
+ return true;
5822
+ }).catch(() => false);
5823
+ };
5824
+ const buildPermalink = options.deepLinkParam === false ? void 0 : (id) => {
5825
+ const param = options.deepLinkParam || "mhfeedback";
5826
+ try {
5827
+ const u = new URL(window.location.href);
5828
+ u.searchParams.set(param, id);
5829
+ return u.toString();
5830
+ } catch {
5831
+ return "";
5832
+ }
5833
+ };
5345
5834
  const pageActivityEnabled = options.showPageActivity !== false;
5346
5835
  const pageSignal = usePageOpenCount({
5347
5836
  ...options.api !== void 0 && { api: options.api },
@@ -5566,7 +6055,8 @@ function mountWidget(options) {
5566
6055
  api: options.api,
5567
6056
  externalId,
5568
6057
  strings: options.strings,
5569
- onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
6058
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id }),
6059
+ onOpenSeq: openSeq
5570
6060
  }
5571
6061
  ),
5572
6062
  (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5577,12 +6067,8 @@ function mountWidget(options) {
5577
6067
  reportId: state.selectedReportId,
5578
6068
  strings: options.strings,
5579
6069
  onBack: () => rerender(clearSelected({ ...currentState })),
5580
- onOpenSeq: (seq) => {
5581
- void options.api?.getReportBySeq(seq, externalId).then(
5582
- (r) => rerender({ ...currentState, selectedReportId: r.id })
5583
- ).catch(() => {
5584
- });
5585
- }
6070
+ onOpenSeq: openSeq,
6071
+ ...buildPermalink && { buildPermalink }
5586
6072
  }
5587
6073
  ),
5588
6074
  state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5612,6 +6098,25 @@ function mountWidget(options) {
5612
6098
  ] });
5613
6099
  }
5614
6100
  rerender(currentState);
6101
+ function openReportRef(ref) {
6102
+ const api = options.api;
6103
+ const externalId = options.getExternalId?.();
6104
+ if (!api || !externalId || !ref) return;
6105
+ const seq = /^\d+$/.test(ref) ? parseInt(ref, 10) : null;
6106
+ const resolve = seq !== null ? api.getReportBySeq(seq, externalId) : api.getReport(ref, externalId);
6107
+ void resolve.then(
6108
+ (r) => rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id })
6109
+ ).catch(() => {
6110
+ });
6111
+ }
6112
+ if (options.deepLinkParam !== false) {
6113
+ try {
6114
+ const param = options.deepLinkParam || "mhfeedback";
6115
+ const value = new URLSearchParams(window.location.search).get(param);
6116
+ if (value) openReportRef(value.trim());
6117
+ } catch {
6118
+ }
6119
+ }
5615
6120
  return {
5616
6121
  open() {
5617
6122
  openWidget(options.getExternalId?.());
@@ -5698,8 +6203,8 @@ function createFeedback(config) {
5698
6203
  capture_method: captureMethod,
5699
6204
  technical_context
5700
6205
  };
5701
- if ("0.28.0") {
5702
- payload.widget_version = "0.28.0";
6206
+ if ("0.30.0") {
6207
+ payload.widget_version = "0.30.0";
5703
6208
  }
5704
6209
  if (manualScreenshots?.length) {
5705
6210
  payload.screenshots = manualScreenshots;
@@ -5825,4 +6330,4 @@ function createFeedback(config) {
5825
6330
  export {
5826
6331
  createFeedback
5827
6332
  };
5828
- //# sourceMappingURL=chunk-FD3CJBMR.mjs.map
6333
+ //# sourceMappingURL=chunk-5F36UFXG.mjs.map