@mhosaic/feedback 0.28.0 → 0.29.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
  )
@@ -3096,12 +3363,34 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
3096
3363
  // src/widget/MineList.tsx
3097
3364
  import { jsx as jsx10, jsxs as jsxs11 } from "preact/jsx-runtime";
3098
3365
  var POLL_MS4 = 3e4;
3099
- function MineList({ api, externalId, strings, onSelect }) {
3366
+ function MineList({ api, externalId, strings, onSelect, onOpenSeq }) {
3100
3367
  const [rows, setRows] = useState6(null);
3101
3368
  const [error, setError] = useState6(null);
3102
3369
  const [refreshing, setRefreshing] = useState6(false);
3103
3370
  const [filter, setFilter] = useState6("all");
3371
+ const [jumpValue, setJumpValue] = useState6("");
3372
+ const [jumpError, setJumpError] = useState6(false);
3373
+ const [jumping, setJumping] = useState6(false);
3104
3374
  const mountedRef = useRef5(true);
3375
+ const handleJump = async (e) => {
3376
+ e.preventDefault();
3377
+ if (!onOpenSeq || jumping) return;
3378
+ const seq = parseInt(jumpValue.replace(/^#/, "").trim(), 10);
3379
+ if (!Number.isInteger(seq) || seq <= 0) {
3380
+ setJumpError(true);
3381
+ return;
3382
+ }
3383
+ setJumping(true);
3384
+ setJumpError(false);
3385
+ try {
3386
+ const found = await onOpenSeq(seq);
3387
+ if (!mountedRef.current) return;
3388
+ if (found) setJumpValue("");
3389
+ else setJumpError(true);
3390
+ } finally {
3391
+ if (mountedRef.current) setJumping(false);
3392
+ }
3393
+ };
3105
3394
  const fetchRows = async () => {
3106
3395
  setRefreshing(true);
3107
3396
  setError(null);
@@ -3134,19 +3423,40 @@ function MineList({ api, externalId, strings, onSelect }) {
3134
3423
  return /* @__PURE__ */ jsxs11("div", { class: "mine-list", children: [
3135
3424
  /* @__PURE__ */ jsxs11("div", { class: "mine-list-header", children: [
3136
3425
  /* @__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
- )
3426
+ /* @__PURE__ */ jsxs11("div", { class: "mine-list-header-actions", children: [
3427
+ onOpenSeq && /* @__PURE__ */ jsxs11("form", { class: "mine-jump", onSubmit: handleJump, children: [
3428
+ /* @__PURE__ */ jsx10(
3429
+ "input",
3430
+ {
3431
+ type: "text",
3432
+ inputMode: "numeric",
3433
+ value: jumpValue,
3434
+ placeholder: strings["mine.jump.placeholder"],
3435
+ "aria-label": strings["mine.jump.aria"],
3436
+ onInput: (e) => {
3437
+ setJumpValue(e.target.value);
3438
+ setJumpError(false);
3439
+ },
3440
+ disabled: jumping
3441
+ }
3442
+ ),
3443
+ /* @__PURE__ */ jsx10("button", { type: "submit", class: "btn", disabled: jumping || !jumpValue.trim(), children: strings["mine.jump.go"] })
3444
+ ] }),
3445
+ /* @__PURE__ */ jsx10(
3446
+ "button",
3447
+ {
3448
+ type: "button",
3449
+ class: "btn",
3450
+ onClick: () => {
3451
+ void fetchRows();
3452
+ },
3453
+ disabled: refreshing,
3454
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
3455
+ }
3456
+ )
3457
+ ] })
3149
3458
  ] }),
3459
+ jumpError && /* @__PURE__ */ jsx10("div", { class: "mine-jump-error error", role: "alert", children: strings["mine.jump.not_found"] }),
3150
3460
  rows && rows.length > 0 && /* @__PURE__ */ jsx10(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
3151
3461
  isLoading && /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] }),
3152
3462
  error && /* @__PURE__ */ jsx10("div", { class: "error", children: error }),
@@ -4137,6 +4447,31 @@ var WIDGET_STYLES = `
4137
4447
  justify-content: space-between;
4138
4448
  }
4139
4449
  .mine-list-header h2 { margin: 0; font-size: var(--mfb-text-lg); font-weight: 600; letter-spacing: -0.01em; }
4450
+ /* #85 \u2014 header actions: jump-by-#seq box + refresh. */
4451
+ .mine-list-header-actions {
4452
+ display: flex;
4453
+ align-items: center;
4454
+ gap: 6px;
4455
+ }
4456
+ .mine-jump {
4457
+ display: flex;
4458
+ align-items: center;
4459
+ gap: 4px;
4460
+ }
4461
+ .mine-jump input {
4462
+ font: inherit;
4463
+ font-size: 13px;
4464
+ width: 56px;
4465
+ padding: 5px 8px;
4466
+ border: 1px solid var(--mfb-border);
4467
+ border-radius: var(--mfb-radius);
4468
+ background: var(--mfb-surface);
4469
+ color: inherit;
4470
+ }
4471
+ .mine-jump-error {
4472
+ margin-top: 6px;
4473
+ font-size: 12px;
4474
+ }
4140
4475
  .mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
4141
4476
  .mine-empty {
4142
4477
  text-align: center;
@@ -4342,12 +4677,77 @@ var WIDGET_STYLES = `
4342
4677
  .comment-author--staff { color: #1e40af; }
4343
4678
  .comment-author--system { color: var(--mfb-text-muted); }
4344
4679
  .comment-body { white-space: pre-wrap; }
4680
+ /* #91 \u2014 image attachments rendered inside a comment bubble. */
4681
+ .comment-attachments {
4682
+ display: flex;
4683
+ flex-wrap: wrap;
4684
+ gap: 6px;
4685
+ margin-top: 6px;
4686
+ }
4687
+ .comment-attachment {
4688
+ display: block;
4689
+ line-height: 0;
4690
+ border-radius: 8px;
4691
+ overflow: hidden;
4692
+ border: 1px solid var(--mfb-border);
4693
+ }
4694
+ .comment-attachment img {
4695
+ width: 96px;
4696
+ height: 96px;
4697
+ object-fit: cover;
4698
+ display: block;
4699
+ }
4345
4700
  .comment-time {
4346
4701
  font-size: 10px;
4347
4702
  margin-top: 4px;
4348
4703
  opacity: 0.7;
4349
4704
  }
4350
4705
 
4706
+ /* F4 \u2014 author edits the description in place. */
4707
+ .report-detail-description-row {
4708
+ display: flex;
4709
+ align-items: flex-start;
4710
+ gap: 8px;
4711
+ }
4712
+ .report-detail-description-row .report-detail-description {
4713
+ flex: 1;
4714
+ margin: 0;
4715
+ }
4716
+ .report-detail-edit {
4717
+ display: flex;
4718
+ flex-direction: column;
4719
+ gap: 6px;
4720
+ }
4721
+ .report-detail-edit-input {
4722
+ font: inherit;
4723
+ font-size: 13px;
4724
+ padding: 8px 10px;
4725
+ border: 1px solid var(--mfb-border);
4726
+ border-radius: var(--mfb-radius);
4727
+ background: var(--mfb-surface);
4728
+ color: inherit;
4729
+ min-height: 72px;
4730
+ resize: vertical;
4731
+ }
4732
+ .report-detail-edit-actions {
4733
+ display: flex;
4734
+ justify-content: flex-end;
4735
+ gap: 6px;
4736
+ }
4737
+ .report-detail-edit.btn,
4738
+ button.report-detail-edit {
4739
+ flex: 0 0 auto;
4740
+ font-size: 12px;
4741
+ padding: 4px 8px;
4742
+ }
4743
+ /* #85 \u2014 shareable-permalink affordance under the description. */
4744
+ .report-detail-permalink {
4745
+ margin: -2px 0 6px;
4746
+ }
4747
+ .report-detail-copy-link {
4748
+ font-size: 12px;
4749
+ padding: 4px 8px;
4750
+ }
4351
4751
  .report-compose {
4352
4752
  display: flex;
4353
4753
  flex-direction: column;
@@ -4370,6 +4770,50 @@ var WIDGET_STYLES = `
4370
4770
  justify-content: flex-end;
4371
4771
  gap: 6px;
4372
4772
  }
4773
+ /* #91 \u2014 pending image attachments in the reply compose box. */
4774
+ .compose-attachments {
4775
+ display: flex;
4776
+ flex-wrap: wrap;
4777
+ gap: 6px;
4778
+ }
4779
+ .compose-attachment {
4780
+ position: relative;
4781
+ width: 64px;
4782
+ height: 64px;
4783
+ border-radius: 8px;
4784
+ overflow: hidden;
4785
+ border: 1px solid var(--mfb-border);
4786
+ }
4787
+ .compose-attachment img {
4788
+ width: 100%;
4789
+ height: 100%;
4790
+ object-fit: cover;
4791
+ display: block;
4792
+ }
4793
+ .compose-attachment-remove {
4794
+ position: absolute;
4795
+ top: 2px;
4796
+ right: 2px;
4797
+ width: 20px;
4798
+ height: 20px;
4799
+ display: grid;
4800
+ place-items: center;
4801
+ padding: 0;
4802
+ background: rgba(255, 255, 255, 0.92);
4803
+ border: 1px solid var(--mfb-border);
4804
+ border-radius: 999px;
4805
+ font-size: 15px;
4806
+ line-height: 1;
4807
+ cursor: pointer;
4808
+ color: #111827;
4809
+ }
4810
+ .compose-attachment-remove:hover { background: #fff; }
4811
+ .compose-attach-btn { margin-right: auto; }
4812
+ .compose-attach-error {
4813
+ margin: 0;
4814
+ font-size: 12px;
4815
+ color: var(--mfb-danger, #b91c1c);
4816
+ }
4373
4817
 
4374
4818
  /* v0.15.3 \u2014 teammate-viewing notice. Shown in place of the compose
4375
4819
  * box when a non-submitter opens a project-wide row from the Board
@@ -5342,6 +5786,23 @@ function mountWidget(options) {
5342
5786
  }
5343
5787
  }, []);
5344
5788
  const externalId = options.getExternalId?.();
5789
+ const openSeq = (seq) => {
5790
+ if (!options.api || !externalId) return Promise.resolve(false);
5791
+ return options.api.getReportBySeq(seq, externalId).then((r) => {
5792
+ rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id });
5793
+ return true;
5794
+ }).catch(() => false);
5795
+ };
5796
+ const buildPermalink = options.deepLinkParam === false ? void 0 : (id) => {
5797
+ const param = options.deepLinkParam || "mhfeedback";
5798
+ try {
5799
+ const u = new URL(window.location.href);
5800
+ u.searchParams.set(param, id);
5801
+ return u.toString();
5802
+ } catch {
5803
+ return "";
5804
+ }
5805
+ };
5345
5806
  const pageActivityEnabled = options.showPageActivity !== false;
5346
5807
  const pageSignal = usePageOpenCount({
5347
5808
  ...options.api !== void 0 && { api: options.api },
@@ -5566,7 +6027,8 @@ function mountWidget(options) {
5566
6027
  api: options.api,
5567
6028
  externalId,
5568
6029
  strings: options.strings,
5569
- onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
6030
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id }),
6031
+ onOpenSeq: openSeq
5570
6032
  }
5571
6033
  ),
5572
6034
  (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5577,12 +6039,8 @@ function mountWidget(options) {
5577
6039
  reportId: state.selectedReportId,
5578
6040
  strings: options.strings,
5579
6041
  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
- }
6042
+ onOpenSeq: openSeq,
6043
+ ...buildPermalink && { buildPermalink }
5586
6044
  }
5587
6045
  ),
5588
6046
  state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5612,6 +6070,25 @@ function mountWidget(options) {
5612
6070
  ] });
5613
6071
  }
5614
6072
  rerender(currentState);
6073
+ function openReportRef(ref) {
6074
+ const api = options.api;
6075
+ const externalId = options.getExternalId?.();
6076
+ if (!api || !externalId || !ref) return;
6077
+ const seq = /^\d+$/.test(ref) ? parseInt(ref, 10) : null;
6078
+ const resolve = seq !== null ? api.getReportBySeq(seq, externalId) : api.getReport(ref, externalId);
6079
+ void resolve.then(
6080
+ (r) => rerender({ ...currentState, open: true, tab: "mine", selectedReportId: r.id })
6081
+ ).catch(() => {
6082
+ });
6083
+ }
6084
+ if (options.deepLinkParam !== false) {
6085
+ try {
6086
+ const param = options.deepLinkParam || "mhfeedback";
6087
+ const value = new URLSearchParams(window.location.search).get(param);
6088
+ if (value) openReportRef(value.trim());
6089
+ } catch {
6090
+ }
6091
+ }
5615
6092
  return {
5616
6093
  open() {
5617
6094
  openWidget(options.getExternalId?.());
@@ -5698,8 +6175,8 @@ function createFeedback(config) {
5698
6175
  capture_method: captureMethod,
5699
6176
  technical_context
5700
6177
  };
5701
- if ("0.28.0") {
5702
- payload.widget_version = "0.28.0";
6178
+ if ("0.29.0") {
6179
+ payload.widget_version = "0.29.0";
5703
6180
  }
5704
6181
  if (manualScreenshots?.length) {
5705
6182
  payload.screenshots = manualScreenshots;
@@ -5825,4 +6302,4 @@ function createFeedback(config) {
5825
6302
  export {
5826
6303
  createFeedback
5827
6304
  };
5828
- //# sourceMappingURL=chunk-FD3CJBMR.mjs.map
6305
+ //# sourceMappingURL=chunk-7FGWO6KO.mjs.map