@mhosaic/feedback 0.27.1 → 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.
@@ -32,6 +32,42 @@ function assertSafeEndpoint(endpoint) {
32
32
  `[mhosaic-feedback] \`endpoint\` must use https:// (got ${parsed.protocol}//${parsed.hostname}). http:// is only allowed for localhost in dev.`
33
33
  );
34
34
  }
35
+ var RateLimitError = class extends Error {
36
+ retryAfterSeconds;
37
+ constructor(retryAfterSeconds) {
38
+ super(`rate limited; retry after ${retryAfterSeconds}s`);
39
+ this.name = "RateLimitError";
40
+ this.retryAfterSeconds = retryAfterSeconds;
41
+ }
42
+ };
43
+ function parseRetryAfter(response) {
44
+ const raw = response.headers.get("Retry-After");
45
+ const n = raw ? Number.parseInt(raw, 10) : NaN;
46
+ return Number.isFinite(n) && n >= 0 ? n : 0;
47
+ }
48
+ async function ensureReadable(response, label) {
49
+ if (response.status === 429) throw new RateLimitError(parseRetryAfter(response));
50
+ if (!response.ok) {
51
+ const text = await response.text().catch(() => "");
52
+ throw new Error(`${label} failed: ${response.status} ${text}`);
53
+ }
54
+ }
55
+ async function readJsonArray(response, label) {
56
+ await ensureReadable(response, label);
57
+ const data = await response.json().catch(() => void 0);
58
+ if (!Array.isArray(data)) {
59
+ throw new Error(`${label}: unexpected response shape (expected an array)`);
60
+ }
61
+ return data;
62
+ }
63
+ async function readJsonObject(response, label) {
64
+ await ensureReadable(response, label);
65
+ const data = await response.json().catch(() => void 0);
66
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
67
+ throw new Error(`${label}: unexpected response shape (expected an object)`);
68
+ }
69
+ return data;
70
+ }
35
71
  function createApiClient(options) {
36
72
  const endpoint = (options.endpoint ?? "").replace(/\/+$/, "");
37
73
  if (!endpoint) {
@@ -107,11 +143,7 @@ function createApiClient(options) {
107
143
  headers: widgetHeaders(externalId)
108
144
  });
109
145
  if (response.status === 404) return [];
110
- if (!response.ok) {
111
- const text = await response.text().catch(() => "");
112
- throw new Error(`listMine failed: ${response.status} ${text}`);
113
- }
114
- return response.json();
146
+ return readJsonArray(response, "listMine");
115
147
  }
116
148
  async function listChangelog(externalId) {
117
149
  const response = await fetcher(
@@ -119,27 +151,34 @@ function createApiClient(options) {
119
151
  { method: "GET", headers: widgetHeaders(externalId) }
120
152
  );
121
153
  if (response.status === 404) return [];
122
- if (!response.ok) {
123
- const text = await response.text().catch(() => "");
124
- throw new Error(`listChangelog failed: ${response.status} ${text}`);
125
- }
126
- return response.json();
154
+ return readJsonArray(response, "listChangelog");
127
155
  }
128
156
  async function getReport(reportId, externalId) {
129
157
  const response = await fetcher(
130
158
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
131
159
  { method: "GET", headers: widgetHeaders(externalId) }
132
160
  );
133
- if (!response.ok) {
134
- const text = await response.text().catch(() => "");
135
- throw new Error(`getReport failed: ${response.status} ${text}`);
136
- }
137
- return response.json();
161
+ return readJsonObject(response, "getReport");
138
162
  }
139
- async function addComment(reportId, externalId, body, clientNonce) {
163
+ async function getReportBySeq(seq, externalId) {
140
164
  const response = await fetcher(
141
- `${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
142
- {
165
+ `${endpoint}/api/feedback/v1/reports/widget/by-seq/${seq}/`,
166
+ { method: "GET", headers: widgetHeaders(externalId) }
167
+ );
168
+ return readJsonObject(response, "getReportBySeq");
169
+ }
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 = {
143
182
  method: "POST",
144
183
  headers: {
145
184
  ...widgetHeaders(externalId),
@@ -149,7 +188,11 @@ function createApiClient(options) {
149
188
  body,
150
189
  ...clientNonce !== void 0 && { client_nonce: clientNonce }
151
190
  })
152
- }
191
+ };
192
+ }
193
+ const response = await fetcher(
194
+ `${endpoint}/api/feedback/v1/reports/widget/${reportId}/comments/`,
195
+ init
153
196
  );
154
197
  if (!response.ok) {
155
198
  const text = await response.text().catch(() => "");
@@ -157,6 +200,24 @@ function createApiClient(options) {
157
200
  }
158
201
  return response.json();
159
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
+ }
160
221
  async function closeAsResolved(reportId, externalId) {
161
222
  const response = await fetcher(
162
223
  `${endpoint}/api/feedback/v1/reports/widget/${reportId}/`,
@@ -215,11 +276,11 @@ function createApiClient(options) {
215
276
  if (response.status === 404) {
216
277
  return { count: 0, next: null, previous: null, results: [] };
217
278
  }
218
- if (!response.ok) {
219
- const text = await response.text().catch(() => "");
220
- throw new Error(`listBoard failed: ${response.status} ${text}`);
279
+ const page = await readJsonObject(response, "listBoard");
280
+ if (!Array.isArray(page.results)) {
281
+ throw new Error("listBoard: unexpected response shape (missing results[])");
221
282
  }
222
- return response.json();
283
+ return page;
223
284
  }
224
285
  async function fetchBoardKpis(externalId, filters) {
225
286
  const response = await fetcher(
@@ -229,11 +290,7 @@ function createApiClient(options) {
229
290
  if (response.status === 404) {
230
291
  return { total: 0, by_status: {}, resolution_rate: 0, scope: "mine" };
231
292
  }
232
- if (!response.ok) {
233
- const text = await response.text().catch(() => "");
234
- throw new Error(`fetchBoardKpis failed: ${response.status} ${text}`);
235
- }
236
- return response.json();
293
+ return readJsonObject(response, "fetchBoardKpis");
237
294
  }
238
295
  async function checkVisibility(externalId, email) {
239
296
  const response = await fetcher(
@@ -262,7 +319,9 @@ function createApiClient(options) {
262
319
  listMine,
263
320
  listChangelog,
264
321
  getReport,
322
+ getReportBySeq,
265
323
  addComment,
324
+ editDescription,
266
325
  closeAsResolved,
267
326
  reopenUnresolved,
268
327
  listBoard,
@@ -573,6 +632,11 @@ var DEFAULT_STRINGS = {
573
632
  "form.close": "Close",
574
633
  "form.submitting": "Sending\u2026",
575
634
  "form.success": "Thanks \u2014 your feedback was sent.",
635
+ "form.success.seq": "Thanks \u2014 report #{seq} was sent. \u2713",
636
+ "form.discard.title": "Unsaved changes",
637
+ "form.discard.body": "Discard your feedback?",
638
+ "form.discard.keep": "Keep editing",
639
+ "form.discard.confirm": "Discard",
576
640
  "form.error": "Could not send. Please try again.",
577
641
  "form.description.required": "Please describe the issue before sending.",
578
642
  "form.screenshot.label": "Screenshot",
@@ -585,6 +649,7 @@ var DEFAULT_STRINGS = {
585
649
  "form.screenshot.error_size": "File too large (max {max} MB).",
586
650
  "form.screenshot.error_count": "Too many screenshots (max {max}).",
587
651
  "form.context.label": "Page",
652
+ "form.capture.notice": "Console, network activity and errors are captured automatically to help us debug.",
588
653
  "type.bug": "Bug",
589
654
  "type.feature": "Feature request",
590
655
  "type.question": "Question",
@@ -663,6 +728,12 @@ var DEFAULT_STRINGS = {
663
728
  "mine.refresh": "Refresh",
664
729
  "mine.loading": "Loading\u2026",
665
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.",
735
+ "error.rate_limited": "Too many requests \u2014 retry in {seconds}s.",
736
+ "error.rate_limited_generic": "Too many requests \u2014 try again in a moment.",
666
737
  "mine.replies_one": "1 reply",
667
738
  "mine.replies_many": "{count} replies",
668
739
  "mine.filter.empty": "No reports match this filter.",
@@ -676,6 +747,16 @@ var DEFAULT_STRINGS = {
676
747
  "detail.compose_placeholder": "Add a follow-up reply\u2026",
677
748
  "detail.compose_send": "Reply",
678
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.",
679
760
  "detail.close_cta": "Mark as resolved",
680
761
  "detail.close_busy": "Marking\u2026",
681
762
  "detail.reopen_cta": "Still not fixed",
@@ -737,6 +818,11 @@ var FRENCH_STRINGS = {
737
818
  "form.close": "Fermer",
738
819
  "form.submitting": "Envoi\u2026",
739
820
  "form.success": "Merci \u2014 votre commentaire a \xE9t\xE9 envoy\xE9.",
821
+ "form.success.seq": "Merci \u2014 rapport n\xB0{seq} envoy\xE9. \u2713",
822
+ "form.discard.title": "Modifications non enregistr\xE9es",
823
+ "form.discard.body": "Abandonner votre commentaire ?",
824
+ "form.discard.keep": "Continuer la saisie",
825
+ "form.discard.confirm": "Abandonner",
740
826
  "form.error": "\xC9chec d\u2019envoi. Veuillez r\xE9essayer.",
741
827
  "form.description.required": "D\xE9crivez le probl\xE8me avant d\u2019envoyer.",
742
828
  "form.screenshot.label": "Capture d\u2019\xE9cran",
@@ -749,6 +835,7 @@ var FRENCH_STRINGS = {
749
835
  "form.screenshot.error_size": "Fichier trop volumineux (max {max} Mo).",
750
836
  "form.screenshot.error_count": "Trop de captures d\u2019\xE9cran (max {max}).",
751
837
  "form.context.label": "Page",
838
+ "form.capture.notice": "La console, l\u2019activit\xE9 r\xE9seau et les erreurs sont captur\xE9es automatiquement pour faciliter le diagnostic.",
752
839
  "type.bug": "Bogue",
753
840
  "type.feature": "Suggestion",
754
841
  "type.question": "Question",
@@ -827,6 +914,12 @@ var FRENCH_STRINGS = {
827
914
  "mine.refresh": "Actualiser",
828
915
  "mine.loading": "Chargement\u2026",
829
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.",
921
+ "error.rate_limited": "Trop de requ\xEAtes \u2014 r\xE9essaie dans {seconds} s.",
922
+ "error.rate_limited_generic": "Trop de requ\xEAtes \u2014 r\xE9essaie dans un instant.",
830
923
  "mine.replies_one": "1 r\xE9ponse",
831
924
  "mine.replies_many": "{count} r\xE9ponses",
832
925
  "mine.filter.empty": "Aucun rapport ne correspond \xE0 ce filtre.",
@@ -840,6 +933,16 @@ var FRENCH_STRINGS = {
840
933
  "detail.compose_placeholder": "Ajouter une r\xE9ponse\u2026",
841
934
  "detail.compose_send": "R\xE9pondre",
842
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.",
843
946
  "detail.close_cta": "Marquer comme r\xE9solu",
844
947
  "detail.close_busy": "Validation\u2026",
845
948
  "detail.reopen_cta": "Toujours pas r\xE9gl\xE9",
@@ -995,21 +1098,76 @@ function startPoll(tick, intervalMs) {
995
1098
  };
996
1099
  }
997
1100
 
1101
+ // src/widget/rateLimit.ts
1102
+ function rateLimitMessage(err, strings) {
1103
+ if (!(err instanceof RateLimitError)) return null;
1104
+ return err.retryAfterSeconds > 0 ? strings["error.rate_limited"].replace(
1105
+ "{seconds}",
1106
+ String(err.retryAfterSeconds)
1107
+ ) : strings["error.rate_limited_generic"];
1108
+ }
1109
+
998
1110
  // src/widget/ReportDetailView.tsx
999
1111
  import { useEffect, useRef, useState } from "preact/hooks";
1000
1112
 
1113
+ // src/widget/linkifySeq.tsx
1114
+ import { jsxs } from "preact/jsx-runtime";
1115
+ var SEQ_RE = /#(\d+)/g;
1116
+ function linkifySeq(text, onOpenSeq) {
1117
+ const value = text ?? "";
1118
+ if (!value || !onOpenSeq) return [value];
1119
+ const parts = [];
1120
+ let last = 0;
1121
+ SEQ_RE.lastIndex = 0;
1122
+ let m;
1123
+ while ((m = SEQ_RE.exec(value)) !== null) {
1124
+ if (m.index > last) parts.push(value.slice(last, m.index));
1125
+ const seq = Number(m[1]);
1126
+ parts.push(
1127
+ /* @__PURE__ */ jsxs(
1128
+ "button",
1129
+ {
1130
+ type: "button",
1131
+ class: "seq-link",
1132
+ onClick: () => onOpenSeq(seq),
1133
+ "aria-label": `Open report #${seq}`,
1134
+ children: [
1135
+ "#",
1136
+ seq
1137
+ ]
1138
+ }
1139
+ )
1140
+ );
1141
+ last = m.index + m[0].length;
1142
+ }
1143
+ if (last < value.length) parts.push(value.slice(last));
1144
+ return parts.length ? parts : [value];
1145
+ }
1146
+
1001
1147
  // src/widget/CommentBubble.tsx
1002
- import { jsx, jsxs } from "preact/jsx-runtime";
1003
- function CommentBubble({ comment, strings }) {
1148
+ import { jsx, jsxs as jsxs2 } from "preact/jsx-runtime";
1149
+ function CommentBubble({ comment, strings, onOpenSeq }) {
1004
1150
  const isMine = comment.is_mine;
1005
1151
  const isAgent = !isMine && comment.author_source === "mcp";
1006
1152
  const isSystem = !isMine && comment.author_source === "system";
1007
1153
  const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1008
1154
  const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1009
1155
  const label = comment.author_label || strings[labelKey];
1010
- return /* @__PURE__ */ jsxs("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1156
+ const images = (comment.attachments ?? []).filter((a) => a.url);
1157
+ return /* @__PURE__ */ jsxs2("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1011
1158
  !isMine && label && /* @__PURE__ */ jsx("div", { class: `comment-author comment-author--${variant}`, children: label }),
1012
- /* @__PURE__ */ jsx("div", { class: "comment-body", children: comment.body }),
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
+ )) }),
1013
1171
  /* @__PURE__ */ jsx("div", { class: "comment-time", children: formatTime(comment.created_at) })
1014
1172
  ] });
1015
1173
  }
@@ -1065,7 +1223,7 @@ function safeExternalHref(url) {
1065
1223
  }
1066
1224
 
1067
1225
  // src/widget/ReportDetailView.tsx
1068
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
1226
+ import { Fragment, jsx as jsx2, jsxs as jsxs3 } from "preact/jsx-runtime";
1069
1227
  var POLL_MS = 3e4;
1070
1228
  function ReportDetailView({
1071
1229
  api,
@@ -1074,15 +1232,68 @@ function ReportDetailView({
1074
1232
  strings,
1075
1233
  onBack,
1076
1234
  canModerate = true,
1077
- variant = "modal"
1235
+ variant = "modal",
1236
+ onOpenSeq,
1237
+ buildPermalink
1078
1238
  }) {
1079
1239
  const [detail, setDetail] = useState(null);
1080
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);
1081
1246
  const [composeBody, setComposeBody] = useState("");
1082
1247
  const [sending, setSending] = useState(false);
1083
1248
  const [closing, setClosing] = useState(false);
1084
1249
  const [reopening, setReopening] = useState(false);
1250
+ const [composeShots, setComposeShots] = useState([]);
1251
+ const [attachError, setAttachError] = useState("");
1252
+ const fileInputRef = useRef(null);
1085
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
+ };
1086
1297
  const fetchDetail = async () => {
1087
1298
  try {
1088
1299
  const next = await api.getReport(reportId, externalId);
@@ -1093,7 +1304,8 @@ function ReportDetailView({
1093
1304
  } catch (err) {
1094
1305
  if (typeof console !== "undefined") console.warn("[mhosaic] getReport:", err);
1095
1306
  if (!mountedRef.current) return false;
1096
- setError("load_failed");
1307
+ const rl = rateLimitMessage(err, strings);
1308
+ setError(rl ?? "load_failed");
1097
1309
  return false;
1098
1310
  }
1099
1311
  };
@@ -1106,13 +1318,23 @@ function ReportDetailView({
1106
1318
  };
1107
1319
  }, [reportId, externalId]);
1108
1320
  const handleSend = async () => {
1109
- if (!composeBody.trim() || sending) return;
1321
+ if (!composeBody.trim() && composeShots.length === 0 || sending) return;
1110
1322
  setSending(true);
1111
1323
  try {
1112
1324
  const nonce = `${reportId}:${Date.now()}`;
1113
- 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
+ );
1114
1333
  if (!mountedRef.current) return;
1115
1334
  setComposeBody("");
1335
+ composeShots.forEach((s) => URL.revokeObjectURL(s.preview));
1336
+ setComposeShots([]);
1337
+ setAttachError("");
1116
1338
  setDetail(
1117
1339
  (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
1118
1340
  );
@@ -1125,6 +1347,47 @@ function ReportDetailView({
1125
1347
  if (mountedRef.current) setSending(false);
1126
1348
  }
1127
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
+ };
1128
1391
  const handleClose = async () => {
1129
1392
  if (closing || reopening) return;
1130
1393
  setClosing(true);
@@ -1161,10 +1424,20 @@ function ReportDetailView({
1161
1424
  return /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] });
1162
1425
  }
1163
1426
  if (!detail) {
1164
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-empty-state", role: "alert", children: [
1427
+ if (error && error !== "load_failed") {
1428
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-empty-state", role: "alert", children: [
1429
+ /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-body", children: error }),
1430
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "btn", onClick: () => void fetchDetail(), children: strings["board.retry"] }),
1431
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1432
+ "\u2190 ",
1433
+ strings["detail.load_failed.cta"]
1434
+ ] })
1435
+ ] });
1436
+ }
1437
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-empty-state", role: "alert", children: [
1165
1438
  /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-title", children: strings["detail.load_failed.title"] }),
1166
1439
  /* @__PURE__ */ jsx2("p", { class: "report-detail-empty-state-body", children: strings["detail.load_failed.body"] }),
1167
- /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
1440
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1168
1441
  "\u2190 ",
1169
1442
  strings["detail.load_failed.cta"]
1170
1443
  ] })
@@ -1174,16 +1447,16 @@ function ReportDetailView({
1174
1447
  const canAct = detail.can_moderate ?? isMine;
1175
1448
  const showCloseCta = canAct && detail.status === "awaiting_validation";
1176
1449
  const showComposeBox = canAct;
1177
- return /* @__PURE__ */ jsxs2("div", { class: `report-detail variant-${variant}`, children: [
1178
- variant === "modal" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header", children: [
1179
- /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
1450
+ return /* @__PURE__ */ jsxs3("div", { class: `report-detail variant-${variant}`, children: [
1451
+ variant === "modal" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header", children: [
1452
+ /* @__PURE__ */ jsxs3("button", { type: "button", class: "btn", onClick: onBack, children: [
1180
1453
  "\u2190 ",
1181
1454
  strings["detail.back"]
1182
1455
  ] }),
1183
1456
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1184
1457
  ] }),
1185
- variant === "board" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header report-detail-header--board", children: [
1186
- /* @__PURE__ */ jsxs2(
1458
+ variant === "board" && /* @__PURE__ */ jsxs3("div", { class: "report-detail-header report-detail-header--board", children: [
1459
+ /* @__PURE__ */ jsxs3(
1187
1460
  "button",
1188
1461
  {
1189
1462
  type: "button",
@@ -1198,9 +1471,65 @@ function ReportDetailView({
1198
1471
  ),
1199
1472
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1200
1473
  ] }),
1201
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
1474
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-body", children: [
1202
1475
  /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1203
- /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
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
+ ) }),
1204
1533
  (detail.screenshots?.length ? detail.screenshots.map((s) => s.url) : [detail.screenshot_url]).filter((url) => Boolean(url)).map((url) => {
1205
1534
  const safeHref = safeExternalHref(url);
1206
1535
  return safeHref ? /* @__PURE__ */ jsx2(
@@ -1215,10 +1544,17 @@ function ReportDetailView({
1215
1544
  ) : /* @__PURE__ */ jsx2("div", { class: "report-detail-screenshot", children: /* @__PURE__ */ jsx2("img", { src: url, alt: "", loading: "lazy" }) });
1216
1545
  }),
1217
1546
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1218
- 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 }) })) }),
1547
+ 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(
1548
+ CommentBubble,
1549
+ {
1550
+ comment: c,
1551
+ strings,
1552
+ ...onOpenSeq && { onOpenSeq }
1553
+ }
1554
+ ) })) }),
1219
1555
  detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
1220
1556
  detail.technical_context && /* @__PURE__ */ jsx2(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
1221
- showComposeBox ? /* @__PURE__ */ jsxs2("div", { class: "report-compose", children: [
1557
+ showComposeBox ? /* @__PURE__ */ jsxs3("div", { class: "report-compose", children: [
1222
1558
  /* @__PURE__ */ jsx2(
1223
1559
  "textarea",
1224
1560
  {
@@ -1228,7 +1564,44 @@ function ReportDetailView({
1228
1564
  disabled: sending
1229
1565
  }
1230
1566
  ),
1231
- /* @__PURE__ */ jsxs2("div", { class: "report-compose-actions", children: [
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
+ ),
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
+ ),
1232
1605
  showCloseCta && /* @__PURE__ */ jsx2(
1233
1606
  "button",
1234
1607
  {
@@ -1255,7 +1628,7 @@ function ReportDetailView({
1255
1628
  type: "button",
1256
1629
  class: "btn btn--primary",
1257
1630
  onClick: handleSend,
1258
- disabled: !composeBody.trim() || sending,
1631
+ disabled: !composeBody.trim() && composeShots.length === 0 || sending,
1259
1632
  children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
1260
1633
  }
1261
1634
  )
@@ -1278,19 +1651,19 @@ function appendComment(current, next) {
1278
1651
  function ContextBlock({ detail, strings }) {
1279
1652
  const captureKey = `detail.context.capture.${detail.capture_method}`;
1280
1653
  const captureLabel = strings[captureKey] ?? detail.capture_method;
1281
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context", children: [
1282
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-pills", children: [
1654
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-context", children: [
1655
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-pills", children: [
1283
1656
  /* @__PURE__ */ jsx2("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1284
1657
  /* @__PURE__ */ jsx2("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1285
1658
  /* @__PURE__ */ jsx2("span", { class: "pill pill-capture", children: captureLabel })
1286
1659
  ] }),
1287
- /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", children: [
1660
+ /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-line", children: [
1288
1661
  /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1289
1662
  /* @__PURE__ */ jsx2("span", { children: formatSubmittedAt(detail.created_at) })
1290
1663
  ] }),
1291
1664
  detail.page_url && (() => {
1292
1665
  const safeHref = safeExternalHref(detail.page_url);
1293
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1666
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1294
1667
  /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1295
1668
  safeHref ? /* @__PURE__ */ jsx2(
1296
1669
  "a",
@@ -1327,9 +1700,9 @@ function TechnicalContextDrawer({ ctx, strings }) {
1327
1700
  if (!hasAny) return null;
1328
1701
  const errorCount = errors.length;
1329
1702
  const summary = errorCount > 0 ? `${strings["detail.tech.title"]} \xB7 ${errorCount} ${errorCount === 1 ? strings["detail.tech.errors_one"] : strings["detail.tech.errors_many"]}` : strings["detail.tech.title"];
1330
- return /* @__PURE__ */ jsxs2("details", { class: "report-detail-tech", children: [
1703
+ return /* @__PURE__ */ jsxs3("details", { class: "report-detail-tech", children: [
1331
1704
  /* @__PURE__ */ jsx2("summary", { children: summary }),
1332
- /* @__PURE__ */ jsxs2("div", { class: "tech-body", children: [
1705
+ /* @__PURE__ */ jsxs3("div", { class: "tech-body", children: [
1333
1706
  device && /* @__PURE__ */ jsx2(DeviceSection, { device, strings }),
1334
1707
  errors.length > 0 && /* @__PURE__ */ jsx2(ErrorsSection, { errors, strings }),
1335
1708
  consoleLogs.length > 0 && /* @__PURE__ */ jsx2(ConsoleSection, { logs: consoleLogs, strings }),
@@ -1354,9 +1727,9 @@ function DeviceSection({
1354
1727
  if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
1355
1728
  if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
1356
1729
  if (parts.length === 0) return null;
1357
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1730
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1358
1731
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.device"] }),
1359
- /* @__PURE__ */ jsx2("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs2(Fragment, { children: [
1732
+ /* @__PURE__ */ jsx2("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs3(Fragment, { children: [
1360
1733
  /* @__PURE__ */ jsx2("dt", { children: p.label }),
1361
1734
  /* @__PURE__ */ jsx2("dd", { children: p.value })
1362
1735
  ] })) })
@@ -1366,9 +1739,9 @@ function ErrorsSection({
1366
1739
  errors,
1367
1740
  strings
1368
1741
  }) {
1369
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1742
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1370
1743
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.errors"] }),
1371
- /* @__PURE__ */ jsx2("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs2("li", { children: [
1744
+ /* @__PURE__ */ jsx2("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs3("li", { children: [
1372
1745
  /* @__PURE__ */ jsx2("div", { class: "tech-errors-msg", children: e.message }),
1373
1746
  e.stack && /* @__PURE__ */ jsx2("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
1374
1747
  ] })) })
@@ -1378,9 +1751,9 @@ function ConsoleSection({
1378
1751
  logs,
1379
1752
  strings
1380
1753
  }) {
1381
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1754
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1382
1755
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.console"] }),
1383
- /* @__PURE__ */ jsx2("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs2("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1756
+ /* @__PURE__ */ jsx2("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs3("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1384
1757
  /* @__PURE__ */ jsx2("span", { class: "tech-console-level", children: l.level }),
1385
1758
  /* @__PURE__ */ jsx2("span", { class: "tech-console-msg", children: l.message })
1386
1759
  ] })) })
@@ -1390,15 +1763,15 @@ function NetworkSection({
1390
1763
  rows,
1391
1764
  strings
1392
1765
  }) {
1393
- return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1766
+ return /* @__PURE__ */ jsxs3("div", { class: "tech-section", children: [
1394
1767
  /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.network"] }),
1395
1768
  /* @__PURE__ */ jsx2("ul", { class: "tech-network", children: rows.map((n) => {
1396
1769
  const failed = n.status >= 400 || n.status === 0;
1397
- return /* @__PURE__ */ jsxs2("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1770
+ return /* @__PURE__ */ jsxs3("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1398
1771
  /* @__PURE__ */ jsx2("span", { class: "tech-network-status", children: n.status || "\u2014" }),
1399
1772
  /* @__PURE__ */ jsx2("span", { class: "tech-network-method", children: n.method }),
1400
1773
  /* @__PURE__ */ jsx2("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
1401
- /* @__PURE__ */ jsxs2("span", { class: "tech-network-time", children: [
1774
+ /* @__PURE__ */ jsxs3("span", { class: "tech-network-time", children: [
1402
1775
  Math.round(n.durationMs),
1403
1776
  "ms"
1404
1777
  ] })
@@ -1412,15 +1785,15 @@ function truncateStack(stack) {
1412
1785
  return lines.slice(0, 12).join("\n") + "\n \u2026";
1413
1786
  }
1414
1787
  function StatusHistorySection({ rows, strings }) {
1415
- return /* @__PURE__ */ jsxs2("div", { class: "report-detail-history", children: [
1788
+ return /* @__PURE__ */ jsxs3("div", { class: "report-detail-history", children: [
1416
1789
  /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.history"] }),
1417
1790
  /* @__PURE__ */ jsx2("ol", { class: "status-history", children: rows.map((r) => {
1418
1791
  const from = strings[`status.${r.from_status}`] ?? r.from_status;
1419
1792
  const to = strings[`status.${r.to_status}`] ?? r.to_status;
1420
1793
  const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
1421
- return /* @__PURE__ */ jsxs2("li", { class: "status-history-row", children: [
1794
+ return /* @__PURE__ */ jsxs3("li", { class: "status-history-row", children: [
1422
1795
  /* @__PURE__ */ jsx2("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
1423
- /* @__PURE__ */ jsxs2("span", { class: "status-history-transition", children: [
1796
+ /* @__PURE__ */ jsxs3("span", { class: "status-history-transition", children: [
1424
1797
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
1425
1798
  /* @__PURE__ */ jsx2("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
1426
1799
  /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
@@ -1432,7 +1805,7 @@ function StatusHistorySection({ rows, strings }) {
1432
1805
  }
1433
1806
 
1434
1807
  // src/widget/BoardView.tsx
1435
- import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1808
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs4 } from "preact/jsx-runtime";
1436
1809
  var POLL_MS2 = 3e4;
1437
1810
  var SORT_OPTIONS = ["recent", "oldest", "updated", "severity", "status"];
1438
1811
  var STATUSES = [
@@ -1470,11 +1843,10 @@ function BoardView({
1470
1843
  if (!thisPage) return;
1471
1844
  return onLocationChange(() => setReloadTick((t) => t + 1));
1472
1845
  }, [thisPage]);
1473
- const fetchFilters = useMemo(
1474
- () => thisPage ? { ...filters, pagePath: currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {}) } : filters,
1475
- // eslint-disable-next-line react-hooks/exhaustive-deps
1476
- [filtersHash(filters), thisPage, reloadTick]
1477
- );
1846
+ const fetchFilters = useMemo(() => {
1847
+ const searching = !!filters.q?.trim();
1848
+ return thisPage && !searching ? { ...filters, pagePath: currentPagePath(getCurrentPage !== void 0 ? { getCurrentPage } : {}) } : filters;
1849
+ }, [filtersHash(filters), thisPage, reloadTick]);
1478
1850
  useEffect2(() => {
1479
1851
  let cancelled = false;
1480
1852
  setLoading(true);
@@ -1491,7 +1863,7 @@ function BoardView({
1491
1863
  return true;
1492
1864
  } catch (e) {
1493
1865
  if (cancelled) return false;
1494
- setError(e instanceof Error ? e.message : String(e));
1866
+ setError(rateLimitMessage(e, strings) ?? strings["board.list.error"]);
1495
1867
  return false;
1496
1868
  } finally {
1497
1869
  if (!cancelled) setLoading(false);
@@ -1510,7 +1882,7 @@ function BoardView({
1510
1882
  setSelectedId(row.id);
1511
1883
  setDetailKey((k) => k + 1);
1512
1884
  };
1513
- return /* @__PURE__ */ jsxs3("div", { class: "board-view", children: [
1885
+ return /* @__PURE__ */ jsxs4("div", { class: "board-view", children: [
1514
1886
  /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis, thisPage }),
1515
1887
  /* @__PURE__ */ jsx3(
1516
1888
  BoardFilters,
@@ -1523,10 +1895,10 @@ function BoardView({
1523
1895
  onToggleThisPage: () => setThisPage((v) => !v)
1524
1896
  }
1525
1897
  ),
1526
- /* @__PURE__ */ jsxs3("div", { class: "board-body", children: [
1527
- /* @__PURE__ */ jsxs3("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
1528
- error && /* @__PURE__ */ jsxs3("div", { class: "board-error", role: "alert", children: [
1529
- /* @__PURE__ */ jsx3("span", { children: strings["board.list.error"] }),
1898
+ /* @__PURE__ */ jsxs4("div", { class: "board-body", children: [
1899
+ /* @__PURE__ */ jsxs4("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
1900
+ error && /* @__PURE__ */ jsxs4("div", { class: "board-error", role: "alert", children: [
1901
+ /* @__PURE__ */ jsx3("span", { children: error }),
1530
1902
  /* @__PURE__ */ jsx3(
1531
1903
  "button",
1532
1904
  {
@@ -1571,10 +1943,17 @@ function BoardView({
1571
1943
  strings,
1572
1944
  onBack: () => setSelectedId(null),
1573
1945
  canModerate: selectedRow.can_moderate ?? selectedRow.is_mine,
1574
- variant: "board"
1946
+ variant: "board",
1947
+ onOpenSeq: (seq) => {
1948
+ void api.getReportBySeq(seq, externalId).then((r) => {
1949
+ setSelectedId(r.id);
1950
+ setDetailKey((k) => k + 1);
1951
+ }).catch(() => {
1952
+ });
1953
+ }
1575
1954
  },
1576
1955
  detailKey
1577
- ) : /* @__PURE__ */ jsxs3("div", { class: "board-detail-empty", children: [
1956
+ ) : /* @__PURE__ */ jsxs4("div", { class: "board-detail-empty", children: [
1578
1957
  /* @__PURE__ */ jsx3(DetailEmptyIllustration, {}),
1579
1958
  /* @__PURE__ */ jsx3("p", { children: strings["board.detail.empty"] })
1580
1959
  ] }) })
@@ -1587,10 +1966,10 @@ function BoardHeader({
1587
1966
  thisPage
1588
1967
  }) {
1589
1968
  const scopeLabel = thisPage ? strings["board.scope.onThisPage"] : kpis?.scope === "project" ? strings["board.scope.project"] : strings["board.scope.mine"];
1590
- return /* @__PURE__ */ jsxs3("header", { class: "board-header", children: [
1591
- /* @__PURE__ */ jsxs3("div", { class: "board-header-title", children: [
1969
+ return /* @__PURE__ */ jsxs4("header", { class: "board-header", children: [
1970
+ /* @__PURE__ */ jsxs4("div", { class: "board-header-title", children: [
1592
1971
  /* @__PURE__ */ jsx3("span", { class: "board-header-emoji", "aria-hidden": "true", children: "\u{1F4CB}" }),
1593
- /* @__PURE__ */ jsxs3("div", { children: [
1972
+ /* @__PURE__ */ jsxs4("div", { children: [
1594
1973
  /* @__PURE__ */ jsx3("h2", { class: "board-header-h", children: strings["tab.board"] }),
1595
1974
  /* @__PURE__ */ jsx3("p", { class: "board-header-sub", children: kpis ? `${kpis.total} \xB7 ${scopeLabel}` : scopeLabel })
1596
1975
  ] })
@@ -1628,7 +2007,7 @@ function BoardKpiStrip({
1628
2007
  tone: "tone-rate"
1629
2008
  }
1630
2009
  ];
1631
- return /* @__PURE__ */ jsx3("div", { class: `board-kpi-strip ${kpis ? "is-ready" : "is-loading"}`, "aria-live": "polite", children: cells.map((c) => /* @__PURE__ */ jsxs3("div", { class: `board-kpi-card ${c.tone}`, children: [
2010
+ return /* @__PURE__ */ jsx3("div", { class: `board-kpi-strip ${kpis ? "is-ready" : "is-loading"}`, "aria-live": "polite", children: cells.map((c) => /* @__PURE__ */ jsxs4("div", { class: `board-kpi-card ${c.tone}`, children: [
1632
2011
  /* @__PURE__ */ jsx3("div", { class: "board-kpi-value", children: c.value }),
1633
2012
  /* @__PURE__ */ jsx3("div", { class: "board-kpi-label", children: c.label })
1634
2013
  ] }, c.key)) });
@@ -1680,8 +2059,8 @@ function BoardFilters({
1680
2059
  }
1681
2060
  };
1682
2061
  const clear = () => onChange({});
1683
- return /* @__PURE__ */ jsxs3("div", { class: "board-filters", children: [
1684
- /* @__PURE__ */ jsxs3("div", { class: "board-scope", role: "group", "aria-label": strings["board.scope.thisPage"], children: [
2062
+ return /* @__PURE__ */ jsxs4("div", { class: "board-filters", children: [
2063
+ /* @__PURE__ */ jsxs4("div", { class: "board-scope", role: "group", "aria-label": strings["board.scope.thisPage"], children: [
1685
2064
  /* @__PURE__ */ jsx3(
1686
2065
  "button",
1687
2066
  {
@@ -1713,7 +2092,7 @@ function BoardFilters({
1713
2092
  children: SORT_OPTIONS.map((o) => /* @__PURE__ */ jsx3("option", { value: o, children: strings[`board.sort.${o}`] }, o))
1714
2093
  }
1715
2094
  ),
1716
- /* @__PURE__ */ jsxs3(
2095
+ /* @__PURE__ */ jsxs4(
1717
2096
  "select",
1718
2097
  {
1719
2098
  class: "board-filter-select",
@@ -1726,7 +2105,7 @@ function BoardFilters({
1726
2105
  ]
1727
2106
  }
1728
2107
  ),
1729
- /* @__PURE__ */ jsxs3(
2108
+ /* @__PURE__ */ jsxs4(
1730
2109
  "select",
1731
2110
  {
1732
2111
  class: "board-filter-select",
@@ -1739,7 +2118,7 @@ function BoardFilters({
1739
2118
  ]
1740
2119
  }
1741
2120
  ),
1742
- /* @__PURE__ */ jsxs3(
2121
+ /* @__PURE__ */ jsxs4(
1743
2122
  "select",
1744
2123
  {
1745
2124
  class: "board-filter-select",
@@ -1762,7 +2141,7 @@ function BoardFilters({
1762
2141
  onInput: (e) => setSearch(e.target.value)
1763
2142
  }
1764
2143
  ),
1765
- /* @__PURE__ */ jsxs3("label", { class: "board-filter-toggle", children: [
2144
+ /* @__PURE__ */ jsxs4("label", { class: "board-filter-toggle", children: [
1766
2145
  /* @__PURE__ */ jsx3(
1767
2146
  "input",
1768
2147
  {
@@ -1773,7 +2152,7 @@ function BoardFilters({
1773
2152
  ),
1774
2153
  /* @__PURE__ */ jsx3("span", { children: strings["board.filter.mine"] })
1775
2154
  ] }),
1776
- activeCount > 0 && /* @__PURE__ */ jsxs3("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
2155
+ activeCount > 0 && /* @__PURE__ */ jsxs4("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1777
2156
  /* @__PURE__ */ jsx3(CloseIcon, {}),
1778
2157
  strings["board.filter.clear"]
1779
2158
  ] })
@@ -1787,7 +2166,7 @@ function BoardList({
1787
2166
  total,
1788
2167
  thisPage
1789
2168
  }) {
1790
- return /* @__PURE__ */ jsxs3(Fragment2, { children: [
2169
+ return /* @__PURE__ */ jsxs4(Fragment2, { children: [
1791
2170
  /* @__PURE__ */ jsx3("div", { class: "board-list-count", children: strings[thisPage ? "board.list.count.page" : "board.list.count"].replace("{n}", String(rows.length)).replace("{total}", String(total)) }),
1792
2171
  /* @__PURE__ */ jsx3("ul", { class: "board-list", role: "list", children: rows.map((row) => /* @__PURE__ */ jsx3(
1793
2172
  BoardRowCard,
@@ -1808,7 +2187,7 @@ function BoardRowCard({
1808
2187
  strings
1809
2188
  }) {
1810
2189
  const author = row.is_mine ? strings["board.list.you"] : row.author_label || "\u2014";
1811
- return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3(
2190
+ return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs4(
1812
2191
  "button",
1813
2192
  {
1814
2193
  type: "button",
@@ -1816,14 +2195,14 @@ function BoardRowCard({
1816
2195
  onClick: () => onPick(row),
1817
2196
  "aria-pressed": selected,
1818
2197
  children: [
1819
- /* @__PURE__ */ jsxs3("div", { class: "board-row-badges", children: [
2198
+ /* @__PURE__ */ jsxs4("div", { class: "board-row-badges", children: [
1820
2199
  /* @__PURE__ */ jsx3("span", { class: `badge status-${row.status}`, children: strings[`status.${row.status}`] ?? row.status }),
1821
2200
  /* @__PURE__ */ jsx3("span", { class: `badge severity-${row.severity}`, children: strings[`severity.${row.severity}`] ?? row.severity })
1822
2201
  ] }),
1823
2202
  /* @__PURE__ */ jsx3("div", { class: "board-row-description", children: row.description }),
1824
- /* @__PURE__ */ jsxs3("div", { class: "board-row-meta", children: [
2203
+ /* @__PURE__ */ jsxs4("div", { class: "board-row-meta", children: [
1825
2204
  /* @__PURE__ */ jsx3("span", { class: "board-row-author", children: author }),
1826
- row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { class: "board-row-comments", children: [
2205
+ row.comment_count > 0 && /* @__PURE__ */ jsxs4("span", { class: "board-row-comments", children: [
1827
2206
  /* @__PURE__ */ jsx3(CommentIcon, {}),
1828
2207
  " ",
1829
2208
  row.comment_count
@@ -1839,7 +2218,7 @@ function BoardEmpty({
1839
2218
  thisPage,
1840
2219
  onAllPages
1841
2220
  }) {
1842
- return /* @__PURE__ */ jsxs3("div", { class: "board-empty", children: [
2221
+ return /* @__PURE__ */ jsxs4("div", { class: "board-empty", children: [
1843
2222
  /* @__PURE__ */ jsx3(EmptyIllustration, {}),
1844
2223
  /* @__PURE__ */ jsx3("h3", { children: strings[thisPage ? "board.list.empty.page.title" : "board.list.empty.title"] }),
1845
2224
  /* @__PURE__ */ jsx3("p", { children: strings[thisPage ? "board.list.empty.page.description" : "board.list.empty.description"] }),
@@ -1847,7 +2226,7 @@ function BoardEmpty({
1847
2226
  ] });
1848
2227
  }
1849
2228
  function BoardListSkeleton() {
1850
- return /* @__PURE__ */ jsx3("ul", { class: "board-list board-list-skeleton", "aria-hidden": "true", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3("div", { class: "board-row skeleton-row", children: [
2229
+ return /* @__PURE__ */ jsx3("ul", { class: "board-list board-list-skeleton", "aria-hidden": "true", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs4("div", { class: "board-row skeleton-row", children: [
1851
2230
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-badges" }),
1852
2231
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text" }),
1853
2232
  /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text short" })
@@ -1857,19 +2236,19 @@ function CommentIcon() {
1857
2236
  return /* @__PURE__ */ jsx3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
1858
2237
  }
1859
2238
  function CloseIcon() {
1860
- return /* @__PURE__ */ jsxs3("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2239
+ return /* @__PURE__ */ jsxs4("svg", { width: "12", height: "12", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
1861
2240
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1862
2241
  /* @__PURE__ */ jsx3("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1863
2242
  ] });
1864
2243
  }
1865
2244
  function EmptyIllustration() {
1866
- return /* @__PURE__ */ jsxs3("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
2245
+ return /* @__PURE__ */ jsxs4("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1867
2246
  /* @__PURE__ */ jsx3("circle", { cx: "32", cy: "32", r: "28", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2" }),
1868
2247
  /* @__PURE__ */ jsx3("path", { d: "M22 32h20M32 22v20", stroke: "currentColor", "stroke-opacity": "0.35", "stroke-width": "2", "stroke-linecap": "round" })
1869
2248
  ] });
1870
2249
  }
1871
2250
  function DetailEmptyIllustration() {
1872
- return /* @__PURE__ */ jsxs3("svg", { width: "80", height: "80", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
2251
+ return /* @__PURE__ */ jsxs4("svg", { width: "80", height: "80", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1873
2252
  /* @__PURE__ */ jsx3("rect", { x: "10", y: "12", width: "44", height: "36", rx: "4", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2" }),
1874
2253
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "22", x2: "46", y2: "22", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2", "stroke-linecap": "round" }),
1875
2254
  /* @__PURE__ */ jsx3("line", { x1: "18", y1: "30", x2: "38", y2: "30", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2", "stroke-linecap": "round" }),
@@ -1905,7 +2284,7 @@ function formatRelative(iso) {
1905
2284
  import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "preact/hooks";
1906
2285
 
1907
2286
  // src/widget/ReportRow.tsx
1908
- import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
2287
+ import { jsx as jsx4, jsxs as jsxs5 } from "preact/jsx-runtime";
1909
2288
  function statusClassName(status) {
1910
2289
  return `pill pill-status pill-status--${status}`;
1911
2290
  }
@@ -1933,16 +2312,16 @@ function repliesLabel(count, strings) {
1933
2312
  }
1934
2313
  function ReportRow({ row, strings, onClick }) {
1935
2314
  const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1936
- return /* @__PURE__ */ jsxs4("button", { type: "button", class: "mine-row", onClick, children: [
1937
- /* @__PURE__ */ jsxs4("div", { class: "mine-row-pills", children: [
2315
+ return /* @__PURE__ */ jsxs5("button", { type: "button", class: "mine-row", onClick, children: [
2316
+ /* @__PURE__ */ jsxs5("div", { class: "mine-row-pills", children: [
1938
2317
  /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1939
2318
  /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1940
2319
  /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1941
2320
  ] }),
1942
2321
  /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1943
- /* @__PURE__ */ jsxs4("div", { class: "mine-row-meta", children: [
2322
+ /* @__PURE__ */ jsxs5("div", { class: "mine-row-meta", children: [
1944
2323
  /* @__PURE__ */ jsx4("span", { children: formatRelative2(row.updated_at || row.created_at) }),
1945
- row.comment_count > 0 && /* @__PURE__ */ jsxs4("span", { children: [
2324
+ row.comment_count > 0 && /* @__PURE__ */ jsxs5("span", { children: [
1946
2325
  "\xB7 ",
1947
2326
  repliesLabel(row.comment_count, strings)
1948
2327
  ] })
@@ -1951,7 +2330,7 @@ function ReportRow({ row, strings, onClick }) {
1951
2330
  }
1952
2331
 
1953
2332
  // src/widget/ChangelogList.tsx
1954
- import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
2333
+ import { jsx as jsx5, jsxs as jsxs6 } from "preact/jsx-runtime";
1955
2334
  var POLL_MS3 = 3e4;
1956
2335
  function isoWeekKey(iso) {
1957
2336
  const d = new Date(iso);
@@ -2020,8 +2399,8 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2020
2399
  const groups = useMemo2(() => rows ? groupRowsByWeek(rows) : [], [rows]);
2021
2400
  const isLoading = rows === null && !error;
2022
2401
  const isEmpty = rows !== null && rows.length === 0;
2023
- return /* @__PURE__ */ jsxs5("div", { class: "mine-list", children: [
2024
- /* @__PURE__ */ jsxs5("div", { class: "mine-list-header", children: [
2402
+ return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
2403
+ /* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
2025
2404
  /* @__PURE__ */ jsx5("h2", { children: strings["tab.changelog"] }),
2026
2405
  /* @__PURE__ */ jsx5(
2027
2406
  "button",
@@ -2038,12 +2417,12 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2038
2417
  ] }),
2039
2418
  isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
2040
2419
  error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
2041
- isEmpty && /* @__PURE__ */ jsxs5("div", { class: "mine-empty", children: [
2420
+ isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
2042
2421
  /* @__PURE__ */ jsx5("strong", { children: strings["changelog.empty.title"] }),
2043
2422
  /* @__PURE__ */ jsx5("p", { children: strings["changelog.empty.body"] })
2044
2423
  ] }),
2045
- groups.length > 0 && /* @__PURE__ */ jsx5("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs5("section", { class: "changelog-group", children: [
2046
- /* @__PURE__ */ jsxs5("header", { class: "changelog-group-header", children: [
2424
+ groups.length > 0 && /* @__PURE__ */ jsx5("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs6("section", { class: "changelog-group", children: [
2425
+ /* @__PURE__ */ jsxs6("header", { class: "changelog-group-header", children: [
2047
2426
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
2048
2427
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
2049
2428
  /* @__PURE__ */ jsx5("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
@@ -2055,9 +2434,9 @@ function ChangelogList({ api, externalId, strings, onSelect }) {
2055
2434
  }
2056
2435
 
2057
2436
  // src/widget/Fab.tsx
2058
- import { jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
2437
+ import { jsx as jsx6, jsxs as jsxs7 } from "preact/jsx-runtime";
2059
2438
  function BugIcon() {
2060
- return /* @__PURE__ */ jsxs6(
2439
+ return /* @__PURE__ */ jsxs7(
2061
2440
  "svg",
2062
2441
  {
2063
2442
  width: "20",
@@ -2087,7 +2466,7 @@ function BugIcon() {
2087
2466
  );
2088
2467
  }
2089
2468
  function Fab({ label, onClick, count }) {
2090
- return /* @__PURE__ */ jsxs6("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: [
2469
+ return /* @__PURE__ */ jsxs7("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: [
2091
2470
  /* @__PURE__ */ jsx6(BugIcon, {}),
2092
2471
  count !== void 0 && count > 0 && /* @__PURE__ */ jsx6("span", { class: "fab-badge", "aria-hidden": "true", children: count > 9 ? "9+" : String(count) })
2093
2472
  ] });
@@ -2098,7 +2477,7 @@ import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } fro
2098
2477
 
2099
2478
  // src/widget/Annotator.tsx
2100
2479
  import { useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState4 } from "preact/hooks";
2101
- import { jsx as jsx7, jsxs as jsxs7 } from "preact/jsx-runtime";
2480
+ import { jsx as jsx7, jsxs as jsxs8 } from "preact/jsx-runtime";
2102
2481
  var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
2103
2482
  var HIGHLIGHT_ALPHA = 0.35;
2104
2483
  function drawShape(ctx, shape, sourceImage) {
@@ -2224,11 +2603,11 @@ var Icon = {
2224
2603
  arrow: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
2225
2604
  pencil: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
2226
2605
  text: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
2227
- highlight: /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2606
+ highlight: /* @__PURE__ */ jsxs8("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2228
2607
  /* @__PURE__ */ jsx7("path", { d: "M3 13l3-3L11 5l-2-2-5 5-3 3v2h2zM10 4l2 2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }),
2229
2608
  /* @__PURE__ */ jsx7("path", { d: "M2 14h12", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" })
2230
2609
  ] }),
2231
- blur: /* @__PURE__ */ jsxs7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2610
+ blur: /* @__PURE__ */ jsxs8("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
2232
2611
  /* @__PURE__ */ jsx7("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
2233
2612
  /* @__PURE__ */ jsx7("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
2234
2613
  /* @__PURE__ */ jsx7("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
@@ -2484,8 +2863,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2484
2863
  onClick: (e) => {
2485
2864
  if (e.target === e.currentTarget) onCancel();
2486
2865
  },
2487
- children: /* @__PURE__ */ jsxs7("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
2488
- /* @__PURE__ */ jsxs7("div", { class: "annotator-header", children: [
2866
+ children: /* @__PURE__ */ jsxs8("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
2867
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-header", children: [
2489
2868
  /* @__PURE__ */ jsx7("span", { children: strings["annotator.title"] }),
2490
2869
  /* @__PURE__ */ jsx7(
2491
2870
  "button",
@@ -2498,7 +2877,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2498
2877
  }
2499
2878
  )
2500
2879
  ] }),
2501
- /* @__PURE__ */ jsxs7("div", { class: "annotator-toolbar", children: [
2880
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-toolbar", children: [
2502
2881
  /* @__PURE__ */ jsx7("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx7(
2503
2882
  "button",
2504
2883
  {
@@ -2513,7 +2892,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2513
2892
  t.id
2514
2893
  )) }),
2515
2894
  /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2516
- /* @__PURE__ */ jsxs7("div", { class: "annotator-colors", children: [
2895
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-colors", children: [
2517
2896
  COLORS.map((c) => /* @__PURE__ */ jsx7(
2518
2897
  "button",
2519
2898
  {
@@ -2537,7 +2916,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2537
2916
  ) })
2538
2917
  ] }),
2539
2918
  /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2540
- /* @__PURE__ */ jsxs7(
2919
+ /* @__PURE__ */ jsxs8(
2541
2920
  "button",
2542
2921
  {
2543
2922
  type: "button",
@@ -2551,7 +2930,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2551
2930
  ]
2552
2931
  }
2553
2932
  ),
2554
- /* @__PURE__ */ jsxs7(
2933
+ /* @__PURE__ */ jsxs8(
2555
2934
  "button",
2556
2935
  {
2557
2936
  type: "button",
@@ -2565,7 +2944,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2565
2944
  ]
2566
2945
  }
2567
2946
  ),
2568
- /* @__PURE__ */ jsxs7(
2947
+ /* @__PURE__ */ jsxs8(
2569
2948
  "button",
2570
2949
  {
2571
2950
  type: "button",
@@ -2579,7 +2958,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2579
2958
  }
2580
2959
  ),
2581
2960
  /* @__PURE__ */ jsx7("span", { class: "annotator-spacer" }),
2582
- /* @__PURE__ */ jsxs7("span", { class: "annotator-count", children: [
2961
+ /* @__PURE__ */ jsxs8("span", { class: "annotator-count", children: [
2583
2962
  shapes.length,
2584
2963
  " ",
2585
2964
  strings["annotator.count_suffix"]
@@ -2596,7 +2975,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2596
2975
  class: "annotator-canvas"
2597
2976
  }
2598
2977
  ) }),
2599
- /* @__PURE__ */ jsxs7("div", { class: "annotator-footer", children: [
2978
+ /* @__PURE__ */ jsxs8("div", { class: "annotator-footer", children: [
2600
2979
  /* @__PURE__ */ jsx7("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
2601
2980
  /* @__PURE__ */ jsx7(
2602
2981
  "button",
@@ -2615,10 +2994,18 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
2615
2994
  }
2616
2995
 
2617
2996
  // src/widget/Form.tsx
2618
- import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
2997
+ import { jsx as jsx8, jsxs as jsxs9 } from "preact/jsx-runtime";
2619
2998
  var TYPES2 = ["bug", "feature", "question", "praise", "typo"];
2620
2999
  var SEVERITIES2 = ["blocker", "high", "medium", "low"];
2621
- function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
3000
+ function Form({
3001
+ strings,
3002
+ onSubmit,
3003
+ onCancel,
3004
+ status,
3005
+ errorMessage,
3006
+ submittedSeq,
3007
+ onDirtyChange
3008
+ }) {
2622
3009
  const [description, setDescription] = useState5("");
2623
3010
  const [feedbackType, setFeedbackType] = useState5("bug");
2624
3011
  const [severity, setSeverity] = useState5("medium");
@@ -2631,6 +3018,10 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2631
3018
  const submitting = status === "submitting";
2632
3019
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
2633
3020
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
3021
+ const isDirty = description.trim() !== "" || shots.length > 0;
3022
+ useEffect5(() => {
3023
+ onDirtyChange?.(isDirty);
3024
+ }, [isDirty]);
2634
3025
  const shotsRef = useRef4(shots);
2635
3026
  shotsRef.current = shots;
2636
3027
  useEffect5(() => {
@@ -2742,9 +3133,9 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2742
3133
  }
2743
3134
  onSubmit(values);
2744
3135
  };
2745
- return /* @__PURE__ */ jsxs8("form", { onSubmit: handleSubmit, children: [
3136
+ return /* @__PURE__ */ jsxs9("form", { onSubmit: handleSubmit, children: [
2746
3137
  /* @__PURE__ */ jsx8("h2", { children: strings["form.title"] }),
2747
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
3138
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2748
3139
  /* @__PURE__ */ jsx8("label", { for: "mfb-desc", children: strings["form.description.label"] }),
2749
3140
  /* @__PURE__ */ jsx8(
2750
3141
  "textarea",
@@ -2756,8 +3147,8 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2756
3147
  }
2757
3148
  )
2758
3149
  ] }),
2759
- /* @__PURE__ */ jsxs8("div", { class: "row", children: [
2760
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
3150
+ /* @__PURE__ */ jsxs9("div", { class: "row", children: [
3151
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2761
3152
  /* @__PURE__ */ jsx8("label", { for: "mfb-type", children: strings["form.type.label"] }),
2762
3153
  /* @__PURE__ */ jsx8(
2763
3154
  "select",
@@ -2769,7 +3160,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2769
3160
  }
2770
3161
  )
2771
3162
  ] }),
2772
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
3163
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2773
3164
  /* @__PURE__ */ jsx8("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
2774
3165
  /* @__PURE__ */ jsx8(
2775
3166
  "select",
@@ -2782,7 +3173,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2782
3173
  )
2783
3174
  ] })
2784
3175
  ] }),
2785
- /* @__PURE__ */ jsxs8("div", { class: "field", children: [
3176
+ /* @__PURE__ */ jsxs9("div", { class: "field", children: [
2786
3177
  /* @__PURE__ */ jsx8("label", { children: strings["form.screenshot.label"] }),
2787
3178
  /* @__PURE__ */ jsx8(
2788
3179
  "input",
@@ -2797,17 +3188,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2797
3188
  tabIndex: -1
2798
3189
  }
2799
3190
  ),
2800
- shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview", children: [
3191
+ shots.length > 0 && /* @__PURE__ */ jsx8("div", { class: "screenshot-strip", children: shots.map((shot, index) => /* @__PURE__ */ jsxs9("div", { class: "screenshot-preview", children: [
2801
3192
  /* @__PURE__ */ jsx8("img", { src: shot.preview, alt: "" }),
2802
- /* @__PURE__ */ jsxs8("div", { class: "screenshot-preview-actions", children: [
2803
- /* @__PURE__ */ jsxs8(
3193
+ /* @__PURE__ */ jsxs9("div", { class: "screenshot-preview-actions", children: [
3194
+ /* @__PURE__ */ jsxs9(
2804
3195
  "button",
2805
3196
  {
2806
3197
  type: "button",
2807
3198
  class: "btn btn--primary screenshot-annotate",
2808
3199
  onClick: () => setAnnotatingIndex(index),
2809
3200
  children: [
2810
- /* @__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: [
3201
+ /* @__PURE__ */ jsxs9("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: [
2811
3202
  /* @__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" }),
2812
3203
  /* @__PURE__ */ jsx8("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
2813
3204
  ] }),
@@ -2815,13 +3206,13 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2815
3206
  ]
2816
3207
  }
2817
3208
  ),
2818
- /* @__PURE__ */ jsxs8("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
3209
+ /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: () => removeShot(index), children: [
2819
3210
  /* @__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" }) }),
2820
3211
  strings["form.screenshot.remove"]
2821
3212
  ] })
2822
3213
  ] })
2823
3214
  ] }, shot.preview)) }),
2824
- shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs8(
3215
+ shots.length < MAX_SCREENSHOTS && /* @__PURE__ */ jsxs9(
2825
3216
  "div",
2826
3217
  {
2827
3218
  ref: dropZoneRef,
@@ -2840,12 +3231,12 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2840
3231
  onDragLeave: handleDragLeave,
2841
3232
  onDrop: handleDrop,
2842
3233
  children: [
2843
- /* @__PURE__ */ jsxs8("svg", { class: "screenshot-icon", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
3234
+ /* @__PURE__ */ jsxs9("svg", { class: "screenshot-icon", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
2844
3235
  /* @__PURE__ */ jsx8("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2845
3236
  /* @__PURE__ */ jsx8("polyline", { points: "17 8 12 3 7 8" }),
2846
3237
  /* @__PURE__ */ jsx8("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
2847
3238
  ] }),
2848
- /* @__PURE__ */ jsxs8("div", { class: "screenshot-cta", children: [
3239
+ /* @__PURE__ */ jsxs9("div", { class: "screenshot-cta", children: [
2849
3240
  /* @__PURE__ */ jsx8("strong", { children: strings["form.screenshot.cta_click"] }),
2850
3241
  ", ",
2851
3242
  strings["form.screenshot.cta_rest"]
@@ -2855,14 +3246,15 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2855
3246
  }
2856
3247
  )
2857
3248
  ] }),
2858
- pageUrl && /* @__PURE__ */ jsxs8("div", { class: "page-context", title: pageUrl, children: [
3249
+ pageUrl && /* @__PURE__ */ jsxs9("div", { class: "page-context", title: pageUrl, children: [
2859
3250
  /* @__PURE__ */ jsx8("span", { class: "page-context-label", children: strings["form.context.label"] }),
2860
3251
  /* @__PURE__ */ jsx8("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
2861
3252
  ] }),
3253
+ /* @__PURE__ */ jsx8("div", { class: "capture-notice", children: strings["form.capture.notice"] }),
2862
3254
  localError && /* @__PURE__ */ jsx8("div", { class: "error", children: localError }),
2863
3255
  status === "error" && errorMessage && /* @__PURE__ */ jsx8("div", { class: "error", children: errorMessage }),
2864
- status === "success" && /* @__PURE__ */ jsx8("div", { class: "success", children: strings["form.success"] }),
2865
- /* @__PURE__ */ jsxs8("div", { class: "actions", children: [
3256
+ status === "success" && /* @__PURE__ */ jsx8("div", { class: "success", children: submittedSeq !== void 0 ? strings["form.success.seq"].replace("{seq}", String(submittedSeq)) : strings["form.success"] }),
3257
+ /* @__PURE__ */ jsxs9("div", { class: "actions", children: [
2866
3258
  /* @__PURE__ */ jsx8("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
2867
3259
  /* @__PURE__ */ jsx8("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
2868
3260
  ] }),
@@ -2882,7 +3274,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
2882
3274
  import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "preact/hooks";
2883
3275
 
2884
3276
  // src/widget/KpiStrip.tsx
2885
- import { jsx as jsx9, jsxs as jsxs9 } from "preact/jsx-runtime";
3277
+ import { jsx as jsx9, jsxs as jsxs10 } from "preact/jsx-runtime";
2886
3278
  function computeKpiCounts(rows) {
2887
3279
  const counts = {
2888
3280
  new: 0,
@@ -2952,7 +3344,7 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
2952
3344
  return /* @__PURE__ */ jsx9("div", { class: "kpi-strip", role: "toolbar", children: cells.map((c) => {
2953
3345
  const active = filter === c.key;
2954
3346
  const toggleTo = active ? "all" : c.key;
2955
- return /* @__PURE__ */ jsxs9(
3347
+ return /* @__PURE__ */ jsxs10(
2956
3348
  "button",
2957
3349
  {
2958
3350
  type: "button",
@@ -2969,14 +3361,36 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
2969
3361
  }
2970
3362
 
2971
3363
  // src/widget/MineList.tsx
2972
- import { jsx as jsx10, jsxs as jsxs10 } from "preact/jsx-runtime";
3364
+ import { jsx as jsx10, jsxs as jsxs11 } from "preact/jsx-runtime";
2973
3365
  var POLL_MS4 = 3e4;
2974
- function MineList({ api, externalId, strings, onSelect }) {
3366
+ function MineList({ api, externalId, strings, onSelect, onOpenSeq }) {
2975
3367
  const [rows, setRows] = useState6(null);
2976
3368
  const [error, setError] = useState6(null);
2977
3369
  const [refreshing, setRefreshing] = useState6(false);
2978
3370
  const [filter, setFilter] = useState6("all");
3371
+ const [jumpValue, setJumpValue] = useState6("");
3372
+ const [jumpError, setJumpError] = useState6(false);
3373
+ const [jumping, setJumping] = useState6(false);
2979
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
+ };
2980
3394
  const fetchRows = async () => {
2981
3395
  setRefreshing(true);
2982
3396
  setError(null);
@@ -2988,7 +3402,7 @@ function MineList({ api, externalId, strings, onSelect }) {
2988
3402
  } catch (err) {
2989
3403
  if (typeof console !== "undefined") console.warn("[mhosaic] listMine:", err);
2990
3404
  if (!mountedRef.current) return false;
2991
- setError(strings["mine.error"]);
3405
+ setError(rateLimitMessage(err, strings) ?? strings["mine.error"]);
2992
3406
  return false;
2993
3407
  } finally {
2994
3408
  if (mountedRef.current) setRefreshing(false);
@@ -3006,26 +3420,47 @@ function MineList({ api, externalId, strings, onSelect }) {
3006
3420
  const isLoading = rows === null && !error;
3007
3421
  const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
3008
3422
  const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
3009
- return /* @__PURE__ */ jsxs10("div", { class: "mine-list", children: [
3010
- /* @__PURE__ */ jsxs10("div", { class: "mine-list-header", children: [
3423
+ return /* @__PURE__ */ jsxs11("div", { class: "mine-list", children: [
3424
+ /* @__PURE__ */ jsxs11("div", { class: "mine-list-header", children: [
3011
3425
  /* @__PURE__ */ jsx10("h2", { children: strings["tab.mine"] }),
3012
- /* @__PURE__ */ jsx10(
3013
- "button",
3014
- {
3015
- type: "button",
3016
- class: "btn",
3017
- onClick: () => {
3018
- void fetchRows();
3019
- },
3020
- disabled: refreshing,
3021
- children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
3022
- }
3023
- )
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
+ ] })
3024
3458
  ] }),
3459
+ jumpError && /* @__PURE__ */ jsx10("div", { class: "mine-jump-error error", role: "alert", children: strings["mine.jump.not_found"] }),
3025
3460
  rows && rows.length > 0 && /* @__PURE__ */ jsx10(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
3026
3461
  isLoading && /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] }),
3027
3462
  error && /* @__PURE__ */ jsx10("div", { class: "error", children: error }),
3028
- isEmpty && /* @__PURE__ */ jsxs10("div", { class: "mine-empty", children: [
3463
+ isEmpty && /* @__PURE__ */ jsxs11("div", { class: "mine-empty", children: [
3029
3464
  /* @__PURE__ */ jsx10("strong", { children: strings["mine.empty.title"] }),
3030
3465
  /* @__PURE__ */ jsx10("p", { children: strings["mine.empty.body"] })
3031
3466
  ] }),
@@ -3036,7 +3471,7 @@ function MineList({ api, externalId, strings, onSelect }) {
3036
3471
 
3037
3472
  // src/widget/Modal.tsx
3038
3473
  import { useEffect as useEffect7, useRef as useRef6 } from "preact/hooks";
3039
- import { jsx as jsx11, jsxs as jsxs11 } from "preact/jsx-runtime";
3474
+ import { jsx as jsx11, jsxs as jsxs12 } from "preact/jsx-runtime";
3040
3475
  function Modal({ onDismiss, children, closeLabel = "Close", expanded = false }) {
3041
3476
  const modalRef = useRef6(null);
3042
3477
  const previouslyFocused = useRef6(null);
@@ -3049,7 +3484,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3049
3484
  return;
3050
3485
  }
3051
3486
  e.stopPropagation();
3052
- onDismiss();
3487
+ onDismiss("escape");
3053
3488
  };
3054
3489
  window.addEventListener("keydown", onKey);
3055
3490
  const first = modalRef.current?.querySelector(
@@ -3068,9 +3503,9 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3068
3503
  class: `backdrop ${expanded ? "is-expanded" : ""}`,
3069
3504
  role: "presentation",
3070
3505
  onClick: (e) => {
3071
- if (e.target === e.currentTarget) onDismiss();
3506
+ if (e.target === e.currentTarget) onDismiss("backdrop");
3072
3507
  },
3073
- children: /* @__PURE__ */ jsxs11(
3508
+ children: /* @__PURE__ */ jsxs12(
3074
3509
  "div",
3075
3510
  {
3076
3511
  ref: modalRef,
@@ -3084,7 +3519,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3084
3519
  type: "button",
3085
3520
  class: "modal-close",
3086
3521
  "aria-label": closeLabel,
3087
- onClick: onDismiss,
3522
+ onClick: () => onDismiss("button"),
3088
3523
  children: "\xD7"
3089
3524
  }
3090
3525
  ),
@@ -3097,7 +3532,7 @@ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false })
3097
3532
  }
3098
3533
 
3099
3534
  // src/widget/PageActivityStrip.tsx
3100
- import { jsx as jsx12, jsxs as jsxs12 } from "preact/jsx-runtime";
3535
+ import { jsx as jsx12, jsxs as jsxs13 } from "preact/jsx-runtime";
3101
3536
  function PageActivityStrip({
3102
3537
  open,
3103
3538
  strings,
@@ -3105,7 +3540,7 @@ function PageActivityStrip({
3105
3540
  }) {
3106
3541
  if (open === 0) return null;
3107
3542
  const text = open === 1 ? strings["page.strip.text.one"] : strings["page.strip.text"].replace("{n}", String(open));
3108
- return /* @__PURE__ */ jsxs12("div", { class: "page-strip", children: [
3543
+ return /* @__PURE__ */ jsxs13("div", { class: "page-strip", children: [
3109
3544
  /* @__PURE__ */ jsx12("span", { children: text }),
3110
3545
  /* @__PURE__ */ jsx12("button", { type: "button", class: "page-strip-view", onClick: onView, children: strings["page.strip.view"] })
3111
3546
  ] });
@@ -3113,7 +3548,7 @@ function PageActivityStrip({
3113
3548
 
3114
3549
  // src/widget/PagePeekPanel.tsx
3115
3550
  import { useEffect as useEffect8, useState as useState7 } from "preact/hooks";
3116
- import { jsx as jsx13, jsxs as jsxs13 } from "preact/jsx-runtime";
3551
+ import { jsx as jsx13, jsxs as jsxs14 } from "preact/jsx-runtime";
3117
3552
  function PagePeekPanel({
3118
3553
  api,
3119
3554
  externalId,
@@ -3141,14 +3576,14 @@ function PagePeekPanel({
3141
3576
  cancelled = true;
3142
3577
  };
3143
3578
  }, [api, externalId]);
3144
- return /* @__PURE__ */ jsxs13("div", { class: "page-peek", role: "region", "aria-label": strings["page.peek.title"], children: [
3579
+ return /* @__PURE__ */ jsxs14("div", { class: "page-peek", role: "region", "aria-label": strings["page.peek.title"], children: [
3145
3580
  /* @__PURE__ */ jsx13("div", { class: "page-peek-title", children: strings["page.peek.title"] }),
3146
- rows === null && !failed && /* @__PURE__ */ jsxs13("div", { class: "page-peek-skeleton", "aria-hidden": "true", children: [
3581
+ rows === null && !failed && /* @__PURE__ */ jsxs14("div", { class: "page-peek-skeleton", "aria-hidden": "true", children: [
3147
3582
  /* @__PURE__ */ jsx13("div", {}),
3148
3583
  " ",
3149
3584
  /* @__PURE__ */ jsx13("div", {})
3150
3585
  ] }),
3151
- rows !== null && rows.map((row) => /* @__PURE__ */ jsxs13(
3586
+ rows !== null && rows.map((row) => /* @__PURE__ */ jsxs14(
3152
3587
  "button",
3153
3588
  {
3154
3589
  type: "button",
@@ -3161,7 +3596,7 @@ function PagePeekPanel({
3161
3596
  },
3162
3597
  row.id
3163
3598
  )),
3164
- /* @__PURE__ */ jsxs13("div", { class: "page-peek-footer", children: [
3599
+ /* @__PURE__ */ jsxs14("div", { class: "page-peek-footer", children: [
3165
3600
  /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-viewall", onClick: onViewAll, children: open === 1 ? strings["page.peek.viewAll.one"] : strings["page.peek.viewAll"].replace("{n}", String(open)) }),
3166
3601
  /* @__PURE__ */ jsx13("button", { type: "button", class: "page-peek-send", onClick: onSend, children: strings["fab.label"] })
3167
3602
  ] })
@@ -3621,6 +4056,46 @@ var WIDGET_STYLES = `
3621
4056
  .error { color: #dc2626; font-size: 13px; }
3622
4057
  .success { color: #059669; font-size: 13px; }
3623
4058
 
4059
+ /* REQ-A7: quiet transparency line under the form. */
4060
+ .capture-notice { color: var(--mfb-muted, #6b7280); font-size: 11px; line-height: 1.4; margin-top: 6px; }
4061
+
4062
+ /* #85: inline "#NN" report reference rendered as a link (a button for a11y). */
4063
+ .seq-link {
4064
+ display: inline;
4065
+ padding: 0;
4066
+ border: none;
4067
+ background: none;
4068
+ color: var(--mfb-accent);
4069
+ font: inherit;
4070
+ cursor: pointer;
4071
+ }
4072
+ .seq-link:hover { text-decoration: underline; }
4073
+
4074
+ /* ---- #89: discard-unsaved-changes confirmation over the form -------- */
4075
+ .discard-confirm {
4076
+ position: absolute;
4077
+ inset: 0;
4078
+ z-index: 5;
4079
+ display: flex;
4080
+ align-items: center;
4081
+ justify-content: center;
4082
+ padding: 16px;
4083
+ background: rgba(15, 23, 42, 0.45);
4084
+ border-radius: inherit;
4085
+ }
4086
+ .discard-confirm-card {
4087
+ max-width: 260px;
4088
+ text-align: center;
4089
+ background: var(--mfb-bg);
4090
+ color: var(--mfb-text);
4091
+ border-radius: var(--mfb-radius-lg);
4092
+ padding: 18px 16px;
4093
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
4094
+ }
4095
+ .discard-confirm-title { font-weight: 600; font-size: 15px; margin: 0 0 4px; }
4096
+ .discard-confirm-body { font-size: 13px; margin: 0 0 14px; opacity: 0.8; }
4097
+ .discard-confirm-actions { display: flex; gap: 8px; justify-content: center; }
4098
+
3624
4099
  /* ---- v0.6.0: manual screenshot upload + annotator -------------------- */
3625
4100
 
3626
4101
  .mfb-sr-only {
@@ -3972,6 +4447,31 @@ var WIDGET_STYLES = `
3972
4447
  justify-content: space-between;
3973
4448
  }
3974
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
+ }
3975
4475
  .mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
3976
4476
  .mine-empty {
3977
4477
  text-align: center;
@@ -4177,12 +4677,77 @@ var WIDGET_STYLES = `
4177
4677
  .comment-author--staff { color: #1e40af; }
4178
4678
  .comment-author--system { color: var(--mfb-text-muted); }
4179
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
+ }
4180
4700
  .comment-time {
4181
4701
  font-size: 10px;
4182
4702
  margin-top: 4px;
4183
4703
  opacity: 0.7;
4184
4704
  }
4185
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
+ }
4186
4751
  .report-compose {
4187
4752
  display: flex;
4188
4753
  flex-direction: column;
@@ -4205,6 +4770,50 @@ var WIDGET_STYLES = `
4205
4770
  justify-content: flex-end;
4206
4771
  gap: 6px;
4207
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
+ }
4208
4817
 
4209
4818
  /* v0.15.3 \u2014 teammate-viewing notice. Shown in place of the compose
4210
4819
  * box when a non-submitter opens a project-wide row from the Board
@@ -5104,7 +5713,7 @@ var WIDGET_STYLES = `
5104
5713
  `;
5105
5714
 
5106
5715
  // src/widget/mount.tsx
5107
- import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs14 } from "preact/jsx-runtime";
5716
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs15 } from "preact/jsx-runtime";
5108
5717
  function computeFabVisible(opts, externalId, visibilityAllowed) {
5109
5718
  if (!opts.showFAB) return false;
5110
5719
  if (opts.getExternalId !== void 0 && !externalId) return false;
@@ -5120,6 +5729,7 @@ function mountWidget(options) {
5120
5729
  shadow.appendChild(mountPoint);
5121
5730
  let currentState = { open: false, status: "idle", tab: "send" };
5122
5731
  let postSubmitTimer = null;
5732
+ let formDirty = false;
5123
5733
  function rerender(state) {
5124
5734
  currentState = state;
5125
5735
  render(h(Root, { state }), mountPoint);
@@ -5145,9 +5755,13 @@ function mountWidget(options) {
5145
5755
  const handleSubmit = useCallback2(async (values) => {
5146
5756
  rerender({ ...currentState, status: "submitting" });
5147
5757
  try {
5148
- await options.onSubmit(values);
5758
+ const result = await options.onSubmit(values);
5149
5759
  pageSignal.refresh();
5150
- rerender({ ...currentState, status: "success" });
5760
+ rerender({
5761
+ ...currentState,
5762
+ status: "success",
5763
+ ...result && result.seq !== void 0 && { submittedSeq: result.seq }
5764
+ });
5151
5765
  if (postSubmitTimer !== null) clearTimeout(postSubmitTimer);
5152
5766
  postSubmitTimer = setTimeout(() => {
5153
5767
  postSubmitTimer = null;
@@ -5172,6 +5786,23 @@ function mountWidget(options) {
5172
5786
  }
5173
5787
  }, []);
5174
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
+ };
5175
5806
  const pageActivityEnabled = options.showPageActivity !== false;
5176
5807
  const pageSignal = usePageOpenCount({
5177
5808
  ...options.api !== void 0 && { api: options.api },
@@ -5214,8 +5845,8 @@ function mountWidget(options) {
5214
5845
  if (peekTimers.current.open) clearTimeout(peekTimers.current.open);
5215
5846
  peekTimers.current.close = setTimeout(() => setPeekOpen(false), 140);
5216
5847
  };
5217
- return /* @__PURE__ */ jsxs14(Fragment3, { children: [
5218
- fabVisible && /* @__PURE__ */ jsxs14(
5848
+ return /* @__PURE__ */ jsxs15(Fragment3, { children: [
5849
+ fabVisible && /* @__PURE__ */ jsxs15(
5219
5850
  "div",
5220
5851
  {
5221
5852
  class: "fab-area",
@@ -5263,14 +5894,62 @@ function mountWidget(options) {
5263
5894
  ]
5264
5895
  }
5265
5896
  ),
5266
- state.open && /* @__PURE__ */ jsxs14(
5897
+ state.open && /* @__PURE__ */ jsxs15(
5267
5898
  Modal,
5268
5899
  {
5269
- onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
5900
+ onDismiss: (reason) => {
5901
+ if (reason !== "button" && formDirty && currentState.tab === "send" && currentState.status !== "submitting") {
5902
+ rerender({ ...currentState, confirmingDiscard: true });
5903
+ return;
5904
+ }
5905
+ formDirty = false;
5906
+ rerender(
5907
+ clearSelected({
5908
+ ...currentState,
5909
+ open: false,
5910
+ status: "idle",
5911
+ confirmingDiscard: false
5912
+ })
5913
+ );
5914
+ },
5270
5915
  closeLabel: options.strings["form.close"],
5271
5916
  expanded: state.tab === "board",
5272
5917
  children: [
5273
- showMineTab && /* @__PURE__ */ jsxs14("div", { class: "tab-strip", role: "tablist", children: [
5918
+ state.confirmingDiscard && /* @__PURE__ */ jsx14("div", { class: "discard-confirm", role: "alertdialog", "aria-modal": "true", children: /* @__PURE__ */ jsxs15("div", { class: "discard-confirm-card", children: [
5919
+ /* @__PURE__ */ jsx14("p", { class: "discard-confirm-title", children: options.strings["form.discard.title"] }),
5920
+ /* @__PURE__ */ jsx14("p", { class: "discard-confirm-body", children: options.strings["form.discard.body"] }),
5921
+ /* @__PURE__ */ jsxs15("div", { class: "discard-confirm-actions", children: [
5922
+ /* @__PURE__ */ jsx14(
5923
+ "button",
5924
+ {
5925
+ type: "button",
5926
+ class: "btn",
5927
+ onClick: () => rerender({ ...currentState, confirmingDiscard: false }),
5928
+ children: options.strings["form.discard.keep"]
5929
+ }
5930
+ ),
5931
+ /* @__PURE__ */ jsx14(
5932
+ "button",
5933
+ {
5934
+ type: "button",
5935
+ class: "btn btn--primary",
5936
+ onClick: () => {
5937
+ formDirty = false;
5938
+ rerender(
5939
+ clearSelected({
5940
+ ...currentState,
5941
+ open: false,
5942
+ status: "idle",
5943
+ confirmingDiscard: false
5944
+ })
5945
+ );
5946
+ },
5947
+ children: options.strings["form.discard.confirm"]
5948
+ }
5949
+ )
5950
+ ] })
5951
+ ] }) }),
5952
+ showMineTab && /* @__PURE__ */ jsxs15("div", { class: "tab-strip", role: "tablist", children: [
5274
5953
  /* @__PURE__ */ jsx14(
5275
5954
  "button",
5276
5955
  {
@@ -5316,7 +5995,7 @@ function mountWidget(options) {
5316
5995
  }
5317
5996
  )
5318
5997
  ] }),
5319
- state.tab === "send" && /* @__PURE__ */ jsxs14(Fragment3, { children: [
5998
+ state.tab === "send" && /* @__PURE__ */ jsxs15(Fragment3, { children: [
5320
5999
  showMineTab && pageActivityEnabled && /* @__PURE__ */ jsx14(
5321
6000
  PageActivityStrip,
5322
6001
  {
@@ -5332,7 +6011,13 @@ function mountWidget(options) {
5332
6011
  onSubmit: handleSubmit,
5333
6012
  onCancel: () => rerender({ ...currentState, open: false, status: "idle" }),
5334
6013
  status: state.status,
5335
- ...state.error !== void 0 && { errorMessage: state.error }
6014
+ ...state.error !== void 0 && { errorMessage: state.error },
6015
+ ...state.submittedSeq !== void 0 && {
6016
+ submittedSeq: state.submittedSeq
6017
+ },
6018
+ onDirtyChange: (d) => {
6019
+ formDirty = d;
6020
+ }
5336
6021
  }
5337
6022
  )
5338
6023
  ] }),
@@ -5342,7 +6027,8 @@ function mountWidget(options) {
5342
6027
  api: options.api,
5343
6028
  externalId,
5344
6029
  strings: options.strings,
5345
- onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
6030
+ onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id }),
6031
+ onOpenSeq: openSeq
5346
6032
  }
5347
6033
  ),
5348
6034
  (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5352,7 +6038,9 @@ function mountWidget(options) {
5352
6038
  externalId,
5353
6039
  reportId: state.selectedReportId,
5354
6040
  strings: options.strings,
5355
- onBack: () => rerender(clearSelected({ ...currentState }))
6041
+ onBack: () => rerender(clearSelected({ ...currentState })),
6042
+ onOpenSeq: openSeq,
6043
+ ...buildPermalink && { buildPermalink }
5356
6044
  }
5357
6045
  ),
5358
6046
  state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx14(
@@ -5382,6 +6070,25 @@ function mountWidget(options) {
5382
6070
  ] });
5383
6071
  }
5384
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
+ }
5385
6092
  return {
5386
6093
  open() {
5387
6094
  openWidget(options.getExternalId?.());
@@ -5468,8 +6175,8 @@ function createFeedback(config) {
5468
6175
  capture_method: captureMethod,
5469
6176
  technical_context
5470
6177
  };
5471
- if ("0.27.1") {
5472
- payload.widget_version = "0.27.1";
6178
+ if ("0.29.0") {
6179
+ payload.widget_version = "0.29.0";
5473
6180
  }
5474
6181
  if (manualScreenshots?.length) {
5475
6182
  payload.screenshots = manualScreenshots;
@@ -5504,9 +6211,8 @@ function createFeedback(config) {
5504
6211
  host,
5505
6212
  strings,
5506
6213
  showFAB: config.showFAB ?? true,
5507
- onSubmit: async (values) => {
5508
- await buildAndSubmit(values);
5509
- },
6214
+ // Return the created report so the modal can acknowledge it by #seq (#82).
6215
+ onSubmit: (values) => buildAndSubmit(values),
5510
6216
  api,
5511
6217
  // Keep this a callback (not a snapshot) so the mount picks up identity
5512
6218
  // changes that happen after createFeedback() — `notifyIdentityChanged()`
@@ -5596,4 +6302,4 @@ function createFeedback(config) {
5596
6302
  export {
5597
6303
  createFeedback
5598
6304
  };
5599
- //# sourceMappingURL=chunk-ZFXGV5W5.mjs.map
6305
+ //# sourceMappingURL=chunk-7FGWO6KO.mjs.map