@mhosaic/feedback 0.10.0 → 0.12.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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ scrubCredentials
3
+ } from "./chunk-DKE6ELJ4.mjs";
4
+
1
5
  // src/api/client.ts
2
6
  var SCALAR_FIELDS = [
3
7
  "description",
@@ -33,9 +37,15 @@ function createApiClient(options) {
33
37
  if (payload.widget_version) {
34
38
  form.append("widget_version", payload.widget_version);
35
39
  }
40
+ const headers = {
41
+ Authorization: `Bearer ${options.apiKey}`
42
+ };
43
+ if (payload.synthetic) {
44
+ headers["X-Mhosaic-Capture-Source"] = "error-tracking";
45
+ }
36
46
  const response = await fetcher(`${endpoint}/api/feedback/v1/reports/`, {
37
47
  method: "POST",
38
- headers: { Authorization: `Bearer ${options.apiKey}` },
48
+ headers,
39
49
  body: form
40
50
  });
41
51
  if (!response.ok) {
@@ -124,11 +134,81 @@ function createApiClient(options) {
124
134
  }
125
135
  return response.json();
126
136
  }
127
- return { submitReport, listMine, listChangelog, getReport, addComment, closeAsResolved };
137
+ function boardQueryString(filters) {
138
+ if (!filters) return "";
139
+ const params = new URLSearchParams();
140
+ filters.status?.forEach((s) => params.append("status", s));
141
+ filters.type?.forEach((t) => params.append("type", t));
142
+ filters.severity?.forEach((s) => params.append("severity", s));
143
+ if (filters.q) params.set("q", filters.q);
144
+ if (filters.mine) params.set("mine", "1");
145
+ if (filters.page && filters.page > 1) params.set("page", String(filters.page));
146
+ const qs = params.toString();
147
+ return qs ? `?${qs}` : "";
148
+ }
149
+ async function listBoard(externalId, filters) {
150
+ const response = await fetcher(
151
+ `${endpoint}/api/feedback/v1/reports/widget/board/${boardQueryString(filters)}`,
152
+ { method: "GET", headers: widgetHeaders(externalId) }
153
+ );
154
+ if (response.status === 404) {
155
+ return { count: 0, next: null, previous: null, results: [] };
156
+ }
157
+ if (!response.ok) {
158
+ const text = await response.text().catch(() => "");
159
+ throw new Error(`listBoard failed: ${response.status} ${text}`);
160
+ }
161
+ return response.json();
162
+ }
163
+ async function fetchBoardKpis(externalId, filters) {
164
+ const response = await fetcher(
165
+ `${endpoint}/api/feedback/v1/reports/widget/board/kpis/${boardQueryString(filters)}`,
166
+ { method: "GET", headers: widgetHeaders(externalId) }
167
+ );
168
+ if (response.status === 404) {
169
+ return { total: 0, by_status: {}, resolution_rate: 0, scope: "mine" };
170
+ }
171
+ if (!response.ok) {
172
+ const text = await response.text().catch(() => "");
173
+ throw new Error(`fetchBoardKpis failed: ${response.status} ${text}`);
174
+ }
175
+ return response.json();
176
+ }
177
+ return {
178
+ submitReport,
179
+ listMine,
180
+ listChangelog,
181
+ getReport,
182
+ addComment,
183
+ closeAsResolved,
184
+ listBoard,
185
+ fetchBoardKpis
186
+ };
128
187
  }
129
188
 
130
189
  // src/capture/urlSanitizer.ts
131
- var SENSITIVE = /token|key|password|secret|auth|session|sig/i;
190
+ var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
191
+ var SENSITIVE_VALUE_PATTERNS = [
192
+ // JWT: three base64url segments separated by dots, header begins with eyJ.
193
+ /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
194
+ // Mhosaic feedback keys (the project's own format).
195
+ /^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
196
+ // GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
197
+ /^gh[pousr]_[A-Za-z0-9]{30,}$/,
198
+ // Stripe live/test secret keys.
199
+ /^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
200
+ // AWS access key id.
201
+ /^AKIA[0-9A-Z]{12,}$/,
202
+ // Slack bot tokens.
203
+ /^xox[abprso]-[A-Za-z0-9-]{12,}$/,
204
+ // Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
205
+ /^[a-f0-9]{40,}$/i,
206
+ // Long opaque base64-ish string (common for OAuth codes & session ids).
207
+ /^[A-Za-z0-9_-]{40,}$/
208
+ ];
209
+ function looksLikeCredential(value) {
210
+ return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
211
+ }
132
212
  function sanitizeUrl(url) {
133
213
  try {
134
214
  const isAbsolute = /^https?:\/\//i.test(url);
@@ -138,9 +218,14 @@ function sanitizeUrl(url) {
138
218
  const u = new URL(url, base);
139
219
  const clean = new URLSearchParams();
140
220
  u.searchParams.forEach((value, name) => {
141
- clean.set(name, SENSITIVE.test(name) ? "[redacted]" : value);
221
+ if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
222
+ clean.set(name, "[redacted]");
223
+ } else {
224
+ clean.set(name, value);
225
+ }
142
226
  });
143
227
  u.search = clean.toString();
228
+ u.hash = u.hash ? "#[redacted]" : "";
144
229
  return u.toString();
145
230
  } catch {
146
231
  return url;
@@ -152,7 +237,16 @@ function collectDevice() {
152
237
  const nav = navigator;
153
238
  const connection = nav.connection?.effectiveType;
154
239
  const deviceMemory = nav.deviceMemory;
155
- const referrer = document.referrer || void 0;
240
+ const rawReferrer = document.referrer || void 0;
241
+ const referrer = rawReferrer !== void 0 ? sanitizeUrl(rawReferrer) : void 0;
242
+ const sanitizedHere = sanitizeUrl(window.location.pathname + window.location.search);
243
+ let pathname;
244
+ try {
245
+ const parsed = new URL(sanitizedHere, window.location.origin);
246
+ pathname = parsed.pathname + parsed.search;
247
+ } catch {
248
+ pathname = window.location.pathname;
249
+ }
156
250
  return {
157
251
  viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio || 1 },
158
252
  screen: { w: window.screen.width, h: window.screen.height },
@@ -165,8 +259,12 @@ function collectDevice() {
165
259
  ...deviceMemory !== void 0 && { deviceMemory },
166
260
  hardwareConcurrency: nav.hardwareConcurrency,
167
261
  ...referrer !== void 0 && { referrer },
168
- title: document.title,
169
- pathname: window.location.pathname
262
+ // Hosts commonly stuff credentials / PII into page titles
263
+ // ("Inbox (3) — bob@x.com", "Reset password — token=abc"). Cap to a
264
+ // sane length and run through the same scrubber the console/error
265
+ // paths use.
266
+ title: scrubCredentials(document.title || "").slice(0, 200),
267
+ pathname
170
268
  };
171
269
  }
172
270
 
@@ -196,7 +294,8 @@ function installConsolePatch(buffer) {
196
294
  originals[level] = original;
197
295
  console[level] = function patched(...args) {
198
296
  try {
199
- const message = args.map(safeStringify).join(" ").slice(0, 2e3);
297
+ const rawMessage = args.map(safeStringify).join(" ");
298
+ const message = scrubCredentials(rawMessage).slice(0, 2e3);
200
299
  const entry = { level, message, ts: Date.now() };
201
300
  if (level === "error") {
202
301
  const stack = new Error().stack;
@@ -229,13 +328,14 @@ function installFetchPatch(buffer, sanitize) {
229
328
  buffer.push({ url: sanitize(url), method, status: response.status, durationMs: Math.round(performance.now() - start), ts: Date.now() });
230
329
  return response;
231
330
  } catch (err) {
331
+ const rawErr = err instanceof Error ? err.message : String(err);
232
332
  buffer.push({
233
333
  url: sanitize(url),
234
334
  method,
235
335
  status: 0,
236
336
  durationMs: Math.round(performance.now() - start),
237
337
  ts: Date.now(),
238
- error: err instanceof Error ? err.message : String(err)
338
+ error: scrubCredentials(rawErr)
239
339
  });
240
340
  throw err;
241
341
  }
@@ -282,9 +382,10 @@ function installErrorHandlers(buffer) {
282
382
  if (typeof window === "undefined") return () => {
283
383
  };
284
384
  const onError = (e) => {
285
- const stack = e.error instanceof Error ? e.error.stack : void 0;
385
+ const rawStack = e.error instanceof Error ? e.error.stack : void 0;
386
+ const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
286
387
  buffer.push({
287
- message: e.message || "Unknown error",
388
+ message: scrubCredentials(e.message || "Unknown error"),
288
389
  ...stack !== void 0 && { stack },
289
390
  ts: Date.now(),
290
391
  source: "window.error"
@@ -292,16 +393,17 @@ function installErrorHandlers(buffer) {
292
393
  };
293
394
  const onRejection = (e) => {
294
395
  const reason = e.reason;
295
- const message = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
396
+ const rawMessage = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
296
397
  try {
297
398
  return JSON.stringify(reason);
298
399
  } catch {
299
400
  return String(reason);
300
401
  }
301
402
  })();
302
- const stack = reason instanceof Error ? reason.stack : void 0;
403
+ const rawStack = reason instanceof Error ? reason.stack : void 0;
404
+ const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
303
405
  buffer.push({
304
- message,
406
+ message: scrubCredentials(rawMessage),
305
407
  ...stack !== void 0 && { stack },
306
408
  ts: Date.now(),
307
409
  source: "unhandledrejection"
@@ -330,7 +432,7 @@ function createPerformanceCollector(slowResourceMs = 1e3) {
330
432
  } else if (entry.entryType === "resource") {
331
433
  const e = entry;
332
434
  if (e.duration > slowResourceMs) {
333
- slowResources.push({ name: e.name, duration: e.duration, initiatorType: e.initiatorType });
435
+ slowResources.push({ name: sanitizeUrl(e.name), duration: e.duration, initiatorType: e.initiatorType });
334
436
  while (slowResources.length > 20) slowResources.shift();
335
437
  }
336
438
  }
@@ -412,32 +514,6 @@ function installCapture(options = {}) {
412
514
  };
413
515
  }
414
516
 
415
- // src/screenshot/index.ts
416
- function isMaskedElement(el) {
417
- return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
418
- }
419
- function prepareMaskMatcher(selectors) {
420
- const joined = selectors.join(",").trim();
421
- if (!joined) return isMaskedElement;
422
- return (el) => isMaskedElement(el) || el.matches(joined);
423
- }
424
- async function takeScreenshot(target, options = {}) {
425
- if (typeof document === "undefined") return null;
426
- try {
427
- const html2canvas = (await import("html2canvas-pro")).default;
428
- const matcher = prepareMaskMatcher(options.mask ?? []);
429
- const canvas = await html2canvas(target, {
430
- ignoreElements: (el) => matcher(el),
431
- useCORS: true,
432
- logging: false,
433
- backgroundColor: null
434
- });
435
- return await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
436
- } catch {
437
- return null;
438
- }
439
- }
440
-
441
517
  // src/widget/i18n.ts
442
518
  var DEFAULT_STRINGS = {
443
519
  "fab.label": "Send feedback",
@@ -461,6 +537,10 @@ var DEFAULT_STRINGS = {
461
537
  "form.screenshot.annotate": "Annotate",
462
538
  "form.screenshot.error_type": "Unsupported file type. Use PNG, JPEG or WebP.",
463
539
  "form.screenshot.error_size": "File too large (max {max} MB).",
540
+ "form.screenshot.or": "or",
541
+ "form.screenshot.capture_page": "Capture this page",
542
+ "form.screenshot.capture_page_hint": "Pick a tab or window to share",
543
+ "form.screenshot.capture_error": "Couldn\u2019t capture the page. Try uploading a screenshot instead.",
464
544
  "form.context.label": "Page",
465
545
  "type.bug": "Bug",
466
546
  "type.feature": "Feature request",
@@ -490,6 +570,33 @@ var DEFAULT_STRINGS = {
490
570
  "tab.send": "Send",
491
571
  "tab.mine": "My reports",
492
572
  "tab.changelog": "This week",
573
+ "tab.board": "Board",
574
+ "board.kpi.total": "Total",
575
+ "board.kpi.new": "New",
576
+ "board.kpi.in_progress": "In progress",
577
+ "board.kpi.awaiting_validation": "Awaiting validation",
578
+ "board.kpi.closed": "Closed",
579
+ "board.kpi.rejected": "Rejected",
580
+ "board.kpi.resolution_rate": "Resolution rate",
581
+ "board.filter.status": "Status",
582
+ "board.filter.type": "Type",
583
+ "board.filter.severity": "Severity",
584
+ "board.filter.search.placeholder": "Search\u2026",
585
+ "board.filter.mine": "Mine only",
586
+ "board.filter.clear": "Clear filters",
587
+ "board.list.empty.title": "Nothing here yet",
588
+ "board.list.empty.description": "When reports land, they\u2019ll show up here.",
589
+ "board.list.loading": "Loading\u2026",
590
+ "board.list.error": "Couldn\u2019t load reports. Try again.",
591
+ "board.list.you": "you",
592
+ "board.list.count": "{n} of {total}",
593
+ "board.detail.empty": "Pick a report on the left to see its full thread.",
594
+ "board.detail.by": "By",
595
+ "board.detail.confirm_resolution": "Confirm resolution",
596
+ "board.detail.status_history": "History",
597
+ "board.scope.project": "Project reports",
598
+ "board.scope.mine": "Your reports",
599
+ "board.back": "Back",
493
600
  "changelog.empty.title": "Nothing resolved yet",
494
601
  "changelog.empty.body": "Once a report you sent is fixed it will appear here, grouped by week.",
495
602
  "changelog.week_of": "Week of {date}",
@@ -567,6 +674,10 @@ var FRENCH_STRINGS = {
567
674
  "form.screenshot.annotate": "Annoter",
568
675
  "form.screenshot.error_type": "Format non support\xE9. Utilisez PNG, JPEG ou WebP.",
569
676
  "form.screenshot.error_size": "Fichier trop volumineux (max {max} Mo).",
677
+ "form.screenshot.or": "ou",
678
+ "form.screenshot.capture_page": "Capturer la page",
679
+ "form.screenshot.capture_page_hint": "Choisissez un onglet ou une fen\xEAtre \xE0 partager",
680
+ "form.screenshot.capture_error": "Impossible de capturer la page. T\xE9l\xE9versez une capture \xE0 la place.",
570
681
  "form.context.label": "Page",
571
682
  "type.bug": "Bogue",
572
683
  "type.feature": "Suggestion",
@@ -596,6 +707,33 @@ var FRENCH_STRINGS = {
596
707
  "tab.send": "Envoyer",
597
708
  "tab.mine": "Mes rapports",
598
709
  "tab.changelog": "Cette semaine",
710
+ "tab.board": "Tableau",
711
+ "board.kpi.total": "Total",
712
+ "board.kpi.new": "Nouveau",
713
+ "board.kpi.in_progress": "En cours",
714
+ "board.kpi.awaiting_validation": "\xC0 valider",
715
+ "board.kpi.closed": "Ferm\xE9",
716
+ "board.kpi.rejected": "Refus\xE9",
717
+ "board.kpi.resolution_rate": "Taux de r\xE9solution",
718
+ "board.filter.status": "Statut",
719
+ "board.filter.type": "Type",
720
+ "board.filter.severity": "S\xE9v\xE9rit\xE9",
721
+ "board.filter.search.placeholder": "Rechercher\u2026",
722
+ "board.filter.mine": "Les miens",
723
+ "board.filter.clear": "Effacer les filtres",
724
+ "board.list.empty.title": "Rien \xE0 voir ici",
725
+ "board.list.empty.description": "Les rapports appara\xEEtront ici d\xE8s qu\u2019ils arrivent.",
726
+ "board.list.loading": "Chargement\u2026",
727
+ "board.list.error": "Impossible de charger les rapports. R\xE9essayez.",
728
+ "board.list.you": "vous",
729
+ "board.list.count": "{n} sur {total}",
730
+ "board.detail.empty": "S\xE9lectionnez un rapport \xE0 gauche pour voir le fil complet.",
731
+ "board.detail.by": "Par",
732
+ "board.detail.confirm_resolution": "Confirmer la r\xE9solution",
733
+ "board.detail.status_history": "Historique",
734
+ "board.scope.project": "Rapports du projet",
735
+ "board.scope.mine": "Vos rapports",
736
+ "board.back": "Retour",
599
737
  "changelog.empty.title": "Rien de r\xE9solu pour l\u2019instant",
600
738
  "changelog.empty.body": "Quand un rapport que vous avez envoy\xE9 est corrig\xE9, il appara\xEEtra ici, regroup\xE9 par semaine.",
601
739
  "changelog.week_of": "Semaine du {date}",
@@ -672,171 +810,984 @@ function resolveStrings(overrides, options = {}) {
672
810
  import { h, render } from "preact";
673
811
  import { useCallback } from "preact/hooks";
674
812
 
675
- // src/widget/ChangelogList.tsx
676
- import { useEffect, useMemo, useRef, useState } from "preact/hooks";
813
+ // src/widget/BoardView.tsx
814
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "preact/hooks";
677
815
 
678
- // src/widget/ReportRow.tsx
816
+ // src/widget/ReportDetailView.tsx
817
+ import { useEffect, useRef, useState } from "preact/hooks";
818
+
819
+ // src/widget/CommentBubble.tsx
679
820
  import { jsx, jsxs } from "preact/jsx-runtime";
680
- function statusClassName(status) {
681
- return `pill pill-status pill-status--${status}`;
682
- }
683
- function severityClassName(severity) {
684
- return `pill pill-severity pill-severity--${severity}`;
685
- }
686
- function typeClassName() {
687
- return "pill pill-type";
688
- }
689
- function formatRelative(iso) {
690
- const then = Date.parse(iso);
691
- if (!Number.isFinite(then)) return "";
692
- const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
693
- if (seconds < 60) return `${seconds}s`;
694
- const minutes = Math.round(seconds / 60);
695
- if (minutes < 60) return `${minutes}m`;
696
- const hours = Math.round(minutes / 60);
697
- if (hours < 48) return `${hours}h`;
698
- const days = Math.round(hours / 24);
699
- return `${days}d`;
700
- }
701
- function repliesLabel(count, strings) {
702
- if (count === 1) return strings["mine.replies_one"];
703
- return strings["mine.replies_many"].replace("{count}", String(count));
704
- }
705
- function ReportRow({ row, strings, onClick }) {
706
- const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
707
- return /* @__PURE__ */ jsxs("button", { type: "button", class: "mine-row", onClick, children: [
708
- /* @__PURE__ */ jsxs("div", { class: "mine-row-pills", children: [
709
- /* @__PURE__ */ jsx("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
710
- /* @__PURE__ */ jsx("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
711
- /* @__PURE__ */ jsx("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
712
- ] }),
713
- /* @__PURE__ */ jsx("div", { class: "mine-row-preview", children: preview }),
714
- /* @__PURE__ */ jsxs("div", { class: "mine-row-meta", children: [
715
- /* @__PURE__ */ jsx("span", { children: formatRelative(row.updated_at || row.created_at) }),
716
- row.comment_count > 0 && /* @__PURE__ */ jsxs("span", { children: [
717
- "\xB7 ",
718
- repliesLabel(row.comment_count, strings)
719
- ] })
720
- ] })
821
+ function CommentBubble({ comment, strings }) {
822
+ const isMine = comment.is_mine;
823
+ const isAgent = !isMine && comment.author_source === "mcp";
824
+ const isSystem = !isMine && comment.author_source === "system";
825
+ const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
826
+ const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
827
+ const label = comment.author_label || strings[labelKey];
828
+ return /* @__PURE__ */ jsxs("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
829
+ !isMine && label && /* @__PURE__ */ jsx("div", { class: `comment-author comment-author--${variant}`, children: label }),
830
+ /* @__PURE__ */ jsx("div", { class: "comment-body", children: comment.body }),
831
+ /* @__PURE__ */ jsx("div", { class: "comment-time", children: formatTime(comment.created_at) })
721
832
  ] });
722
833
  }
723
-
724
- // src/widget/ChangelogList.tsx
725
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
726
- var POLL_MS = 3e4;
727
- function isoWeekKey(iso) {
728
- const d = new Date(iso);
729
- if (Number.isNaN(d.getTime())) return "";
730
- const day = (d.getUTCDay() + 6) % 7;
731
- const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - day));
732
- return monday.toISOString().slice(0, 10);
834
+ function formatTime(iso) {
835
+ try {
836
+ return new Date(iso).toLocaleString(void 0, {
837
+ dateStyle: "short",
838
+ timeStyle: "short"
839
+ });
840
+ } catch {
841
+ return iso;
842
+ }
733
843
  }
734
- function groupRowsByWeek(rows) {
735
- const buckets = /* @__PURE__ */ new Map();
736
- for (const row of rows) {
737
- const key = isoWeekKey(row.resolved_at);
738
- if (!key) continue;
739
- const existing = buckets.get(key);
740
- if (existing) existing.push(row);
741
- else buckets.set(key, [row]);
844
+
845
+ // src/widget/screenshot-utils.ts
846
+ var ALLOWED_IMAGE_TYPES = [
847
+ "image/png",
848
+ "image/jpeg",
849
+ "image/webp"
850
+ ];
851
+ var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
852
+ function validateScreenshotFile(file) {
853
+ if (!ALLOWED_IMAGE_TYPES.includes(
854
+ file.type
855
+ )) {
856
+ return { kind: "type" };
742
857
  }
743
- return Array.from(buckets.entries()).sort(([a], [b]) => a < b ? 1 : -1).map(([weekKey, weekRows]) => ({
744
- weekKey,
745
- label: formatWeekLabel(weekKey),
746
- rows: weekRows
747
- }));
858
+ if (file.size > MAX_SCREENSHOT_BYTES) {
859
+ return { kind: "size", maxMb: MAX_SCREENSHOT_BYTES / (1024 * 1024) };
860
+ }
861
+ return null;
748
862
  }
749
- function formatWeekLabel(weekKey) {
863
+ function truncateUrl(url, maxLength = 80) {
864
+ if (url.length <= maxLength) return url;
865
+ const keepStart = Math.floor((maxLength - 1) / 2);
866
+ const keepEnd = maxLength - 1 - keepStart;
867
+ return `${url.slice(0, keepStart)}\u2026${url.slice(url.length - keepEnd)}`;
868
+ }
869
+ async function captureWithDisplayMedia() {
870
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
871
+ return null;
872
+ }
873
+ let stream = null;
750
874
  try {
751
- return new Date(weekKey).toLocaleDateString(void 0, {
752
- year: "numeric",
753
- month: "short",
754
- day: "numeric"
875
+ stream = await navigator.mediaDevices.getDisplayMedia({
876
+ video: { displaySurface: "browser" },
877
+ audio: false
878
+ // Hint that audio isn't needed; not all browsers honor this but it
879
+ // avoids the audio-share toggle being defaulted on in some.
755
880
  });
756
881
  } catch {
757
- return weekKey;
882
+ return null;
883
+ }
884
+ try {
885
+ const track = stream.getVideoTracks()[0];
886
+ if (!track) return null;
887
+ const ImageCaptureCtor = window.ImageCapture;
888
+ let bitmap = null;
889
+ if (ImageCaptureCtor) {
890
+ try {
891
+ const ic = new ImageCaptureCtor(track);
892
+ bitmap = await ic.grabFrame();
893
+ } catch {
894
+ bitmap = null;
895
+ }
896
+ }
897
+ if (!bitmap) {
898
+ bitmap = await grabFrameViaVideo(stream);
899
+ }
900
+ if (!bitmap) return null;
901
+ const canvas = document.createElement("canvas");
902
+ canvas.width = bitmap.width;
903
+ canvas.height = bitmap.height;
904
+ const ctx = canvas.getContext("2d");
905
+ if (!ctx) return null;
906
+ ctx.drawImage(bitmap, 0, 0);
907
+ return await new Promise(
908
+ (resolve) => canvas.toBlob(resolve, "image/png")
909
+ );
910
+ } finally {
911
+ stream.getTracks().forEach((t) => t.stop());
758
912
  }
759
913
  }
760
- function ChangelogList({ api, externalId, strings, onSelect }) {
761
- const [rows, setRows] = useState(null);
914
+ async function grabFrameViaVideo(stream) {
915
+ const video = document.createElement("video");
916
+ video.muted = true;
917
+ video.playsInline = true;
918
+ video.srcObject = stream;
919
+ try {
920
+ await video.play();
921
+ await new Promise((r) => requestAnimationFrame(() => r()));
922
+ if (!video.videoWidth || !video.videoHeight) return null;
923
+ return await createImageBitmap(video);
924
+ } catch {
925
+ return null;
926
+ }
927
+ }
928
+
929
+ // src/widget/ReportDetailView.tsx
930
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
931
+ var POLL_MS = 3e4;
932
+ function ReportDetailView({
933
+ api,
934
+ externalId,
935
+ reportId,
936
+ strings,
937
+ onBack,
938
+ canModerate = true,
939
+ variant = "modal"
940
+ }) {
941
+ const [detail, setDetail] = useState(null);
762
942
  const [error, setError] = useState(null);
763
- const [refreshing, setRefreshing] = useState(false);
943
+ const [composeBody, setComposeBody] = useState("");
944
+ const [sending, setSending] = useState(false);
945
+ const [closing, setClosing] = useState(false);
764
946
  const mountedRef = useRef(true);
765
- const fetchRows = async () => {
766
- setRefreshing(true);
767
- setError(null);
947
+ const fetchDetail = async () => {
768
948
  try {
769
- const next = await api.listChangelog(externalId);
949
+ const next = await api.getReport(reportId, externalId);
770
950
  if (!mountedRef.current) return;
771
- setRows(next);
951
+ setDetail(next);
952
+ setError(null);
772
953
  } catch (err) {
773
954
  if (!mountedRef.current) return;
774
- setError(err instanceof Error ? err.message : strings["mine.error"]);
775
- } finally {
776
- if (mountedRef.current) setRefreshing(false);
955
+ setError(err instanceof Error ? err.message : "load_failed");
777
956
  }
778
957
  };
779
958
  useEffect(() => {
780
959
  mountedRef.current = true;
781
- void fetchRows();
960
+ void fetchDetail();
782
961
  const timer = setInterval(() => {
783
- void fetchRows();
962
+ void fetchDetail();
784
963
  }, POLL_MS);
785
964
  return () => {
786
965
  mountedRef.current = false;
787
966
  clearInterval(timer);
788
967
  };
789
- }, [externalId]);
790
- const groups = useMemo(() => rows ? groupRowsByWeek(rows) : [], [rows]);
791
- const isLoading = rows === null && !error;
792
- const isEmpty = rows !== null && rows.length === 0;
793
- return /* @__PURE__ */ jsxs2("div", { class: "mine-list", children: [
794
- /* @__PURE__ */ jsxs2("div", { class: "mine-list-header", children: [
795
- /* @__PURE__ */ jsx2("h2", { children: strings["tab.changelog"] }),
796
- /* @__PURE__ */ jsx2(
968
+ }, [reportId, externalId]);
969
+ const handleSend = async () => {
970
+ if (!composeBody.trim() || sending) return;
971
+ setSending(true);
972
+ try {
973
+ const nonce = `${reportId}:${Date.now()}`;
974
+ const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
975
+ if (!mountedRef.current) return;
976
+ setComposeBody("");
977
+ setDetail(
978
+ (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
979
+ );
980
+ void fetchDetail();
981
+ } catch (err) {
982
+ if (!mountedRef.current) return;
983
+ setError(err instanceof Error ? err.message : "comment_failed");
984
+ } finally {
985
+ if (mountedRef.current) setSending(false);
986
+ }
987
+ };
988
+ const handleClose = async () => {
989
+ if (closing) return;
990
+ setClosing(true);
991
+ try {
992
+ const next = await api.closeAsResolved(reportId, externalId);
993
+ if (!mountedRef.current) return;
994
+ setDetail(next);
995
+ } catch (err) {
996
+ if (!mountedRef.current) return;
997
+ setError(err instanceof Error ? err.message : "close_failed");
998
+ } finally {
999
+ if (mountedRef.current) setClosing(false);
1000
+ }
1001
+ };
1002
+ if (!detail && !error) {
1003
+ return /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] });
1004
+ }
1005
+ if (!detail) {
1006
+ return /* @__PURE__ */ jsx2("div", { class: "error", role: "alert", children: error });
1007
+ }
1008
+ const showCloseCta = canModerate && detail.status === "awaiting_validation";
1009
+ return /* @__PURE__ */ jsxs2("div", { class: `report-detail variant-${variant}`, children: [
1010
+ variant === "modal" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header", children: [
1011
+ /* @__PURE__ */ jsxs2("button", { type: "button", class: "btn", onClick: onBack, children: [
1012
+ "\u2190 ",
1013
+ strings["detail.back"]
1014
+ ] }),
1015
+ /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1016
+ ] }),
1017
+ variant === "board" && /* @__PURE__ */ jsxs2("div", { class: "report-detail-header report-detail-header--board", children: [
1018
+ /* @__PURE__ */ jsxs2(
797
1019
  "button",
798
1020
  {
799
1021
  type: "button",
800
- class: "btn",
801
- onClick: () => {
802
- void fetchRows();
803
- },
804
- disabled: refreshing,
805
- children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1022
+ class: "btn btn--ghost report-detail-back",
1023
+ onClick: onBack,
1024
+ "aria-label": strings["board.back"],
1025
+ children: [
1026
+ "\u2190 ",
1027
+ strings["board.back"]
1028
+ ]
806
1029
  }
807
- )
808
- ] }),
809
- isLoading && /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] }),
810
- error && /* @__PURE__ */ jsx2("div", { class: "error", children: error }),
811
- isEmpty && /* @__PURE__ */ jsxs2("div", { class: "mine-empty", children: [
812
- /* @__PURE__ */ jsx2("strong", { children: strings["changelog.empty.title"] }),
813
- /* @__PURE__ */ jsx2("p", { children: strings["changelog.empty.body"] })
1030
+ ),
1031
+ /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
814
1032
  ] }),
815
- groups.length > 0 && /* @__PURE__ */ jsx2("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs2("section", { class: "changelog-group", children: [
816
- /* @__PURE__ */ jsxs2("header", { class: "changelog-group-header", children: [
817
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
818
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
819
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
820
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-count", children: strings[g.rows.length === 1 ? "changelog.resolved_one" : "changelog.resolved_many"].replace("{count}", String(g.rows.length)) })
821
- ] }),
822
- /* @__PURE__ */ jsx2("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
823
- ] }, g.weekKey)) })
824
- ] });
825
- }
826
-
827
- // src/widget/Fab.tsx
828
- import { jsx as jsx3 } from "preact/jsx-runtime";
829
- function ChatBubbleIcon() {
830
- return /* @__PURE__ */ jsx3(
831
- "svg",
832
- {
833
- width: "24",
834
- height: "24",
835
- viewBox: "0 0 24 24",
1033
+ /* @__PURE__ */ jsxs2("div", { class: "report-detail-body", children: [
1034
+ /* @__PURE__ */ jsx2(ContextBlock, { detail, strings }),
1035
+ /* @__PURE__ */ jsx2("p", { class: "report-detail-description", children: detail.description }),
1036
+ detail.screenshot_url && /* @__PURE__ */ jsx2(
1037
+ "a",
1038
+ {
1039
+ class: "report-detail-screenshot",
1040
+ href: detail.screenshot_url,
1041
+ target: "_blank",
1042
+ rel: "noopener noreferrer",
1043
+ children: /* @__PURE__ */ jsx2("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
1044
+ }
1045
+ ),
1046
+ /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1047
+ 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 }) })) }),
1048
+ detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx2(StatusHistorySection, { rows: detail.status_history, strings }),
1049
+ detail.technical_context && /* @__PURE__ */ jsx2(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
1050
+ /* @__PURE__ */ jsxs2("div", { class: "report-compose", children: [
1051
+ /* @__PURE__ */ jsx2(
1052
+ "textarea",
1053
+ {
1054
+ value: composeBody,
1055
+ placeholder: strings["detail.compose_placeholder"],
1056
+ onInput: (e) => setComposeBody(e.target.value),
1057
+ disabled: sending
1058
+ }
1059
+ ),
1060
+ /* @__PURE__ */ jsxs2("div", { class: "report-compose-actions", children: [
1061
+ showCloseCta && /* @__PURE__ */ jsx2(
1062
+ "button",
1063
+ {
1064
+ type: "button",
1065
+ class: "btn",
1066
+ onClick: handleClose,
1067
+ disabled: closing,
1068
+ children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
1069
+ }
1070
+ ),
1071
+ /* @__PURE__ */ jsx2(
1072
+ "button",
1073
+ {
1074
+ type: "button",
1075
+ class: "btn btn--primary",
1076
+ onClick: handleSend,
1077
+ disabled: !composeBody.trim() || sending,
1078
+ children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
1079
+ }
1080
+ )
1081
+ ] })
1082
+ ] }),
1083
+ error && /* @__PURE__ */ jsx2("div", { class: "error", children: error })
1084
+ ] })
1085
+ ] });
1086
+ }
1087
+ function appendComment(current, next) {
1088
+ if (current.some((c) => c.id === next.id)) return current;
1089
+ return [...current, next];
1090
+ }
1091
+ function ContextBlock({ detail, strings }) {
1092
+ const captureKey = `detail.context.capture.${detail.capture_method}`;
1093
+ const captureLabel = strings[captureKey] ?? detail.capture_method;
1094
+ return /* @__PURE__ */ jsxs2("div", { class: "report-detail-context", children: [
1095
+ /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-pills", children: [
1096
+ /* @__PURE__ */ jsx2("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
1097
+ /* @__PURE__ */ jsx2("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
1098
+ /* @__PURE__ */ jsx2("span", { class: "pill pill-capture", children: captureLabel })
1099
+ ] }),
1100
+ /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", children: [
1101
+ /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
1102
+ /* @__PURE__ */ jsx2("span", { children: formatSubmittedAt(detail.created_at) })
1103
+ ] }),
1104
+ detail.page_url && /* @__PURE__ */ jsxs2("div", { class: "report-detail-context-line", title: detail.page_url, children: [
1105
+ /* @__PURE__ */ jsx2("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
1106
+ /* @__PURE__ */ jsx2(
1107
+ "a",
1108
+ {
1109
+ class: "report-detail-context-url",
1110
+ href: detail.page_url,
1111
+ target: "_blank",
1112
+ rel: "noopener noreferrer",
1113
+ children: truncateUrl(detail.page_url, 64)
1114
+ }
1115
+ )
1116
+ ] })
1117
+ ] });
1118
+ }
1119
+ function formatSubmittedAt(iso) {
1120
+ try {
1121
+ return new Date(iso).toLocaleString(void 0, {
1122
+ dateStyle: "medium",
1123
+ timeStyle: "short"
1124
+ });
1125
+ } catch {
1126
+ return iso;
1127
+ }
1128
+ }
1129
+ var TECH_CONSOLE_LIMIT = 20;
1130
+ var TECH_NETWORK_LIMIT = 15;
1131
+ function TechnicalContextDrawer({ ctx, strings }) {
1132
+ const errors = ctx.errors ?? [];
1133
+ const consoleLogs = (ctx.consoleLogs ?? []).slice(-TECH_CONSOLE_LIMIT);
1134
+ const network = (ctx.networkRequests ?? []).slice(-TECH_NETWORK_LIMIT);
1135
+ const device = ctx.device;
1136
+ const hasAny = !!device || errors.length > 0 || consoleLogs.length > 0 || network.length > 0;
1137
+ if (!hasAny) return null;
1138
+ const errorCount = errors.length;
1139
+ 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"];
1140
+ return /* @__PURE__ */ jsxs2("details", { class: "report-detail-tech", children: [
1141
+ /* @__PURE__ */ jsx2("summary", { children: summary }),
1142
+ /* @__PURE__ */ jsxs2("div", { class: "tech-body", children: [
1143
+ device && /* @__PURE__ */ jsx2(DeviceSection, { device, strings }),
1144
+ errors.length > 0 && /* @__PURE__ */ jsx2(ErrorsSection, { errors, strings }),
1145
+ consoleLogs.length > 0 && /* @__PURE__ */ jsx2(ConsoleSection, { logs: consoleLogs, strings }),
1146
+ network.length > 0 && /* @__PURE__ */ jsx2(NetworkSection, { rows: network, strings })
1147
+ ] })
1148
+ ] });
1149
+ }
1150
+ function DeviceSection({
1151
+ device,
1152
+ strings
1153
+ }) {
1154
+ const parts = [];
1155
+ if (device?.viewport) {
1156
+ const dpr = device.viewport.dpr ? ` @${device.viewport.dpr}x` : "";
1157
+ parts.push({
1158
+ label: strings["detail.tech.device.viewport"],
1159
+ value: `${device.viewport.w}\xD7${device.viewport.h}${dpr}`
1160
+ });
1161
+ }
1162
+ if (device?.platform) parts.push({ label: strings["detail.tech.device.platform"], value: device.platform });
1163
+ if (device?.language) parts.push({ label: strings["detail.tech.device.language"], value: device.language });
1164
+ if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
1165
+ if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
1166
+ if (parts.length === 0) return null;
1167
+ return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1168
+ /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.device"] }),
1169
+ /* @__PURE__ */ jsx2("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs2(Fragment, { children: [
1170
+ /* @__PURE__ */ jsx2("dt", { children: p.label }),
1171
+ /* @__PURE__ */ jsx2("dd", { children: p.value })
1172
+ ] })) })
1173
+ ] });
1174
+ }
1175
+ function ErrorsSection({
1176
+ errors,
1177
+ strings
1178
+ }) {
1179
+ return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1180
+ /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.errors"] }),
1181
+ /* @__PURE__ */ jsx2("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs2("li", { children: [
1182
+ /* @__PURE__ */ jsx2("div", { class: "tech-errors-msg", children: e.message }),
1183
+ e.stack && /* @__PURE__ */ jsx2("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
1184
+ ] })) })
1185
+ ] });
1186
+ }
1187
+ function ConsoleSection({
1188
+ logs,
1189
+ strings
1190
+ }) {
1191
+ return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1192
+ /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.console"] }),
1193
+ /* @__PURE__ */ jsx2("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs2("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
1194
+ /* @__PURE__ */ jsx2("span", { class: "tech-console-level", children: l.level }),
1195
+ /* @__PURE__ */ jsx2("span", { class: "tech-console-msg", children: l.message })
1196
+ ] })) })
1197
+ ] });
1198
+ }
1199
+ function NetworkSection({
1200
+ rows,
1201
+ strings
1202
+ }) {
1203
+ return /* @__PURE__ */ jsxs2("div", { class: "tech-section", children: [
1204
+ /* @__PURE__ */ jsx2("h4", { children: strings["detail.tech.network"] }),
1205
+ /* @__PURE__ */ jsx2("ul", { class: "tech-network", children: rows.map((n) => {
1206
+ const failed = n.status >= 400 || n.status === 0;
1207
+ return /* @__PURE__ */ jsxs2("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
1208
+ /* @__PURE__ */ jsx2("span", { class: "tech-network-status", children: n.status || "\u2014" }),
1209
+ /* @__PURE__ */ jsx2("span", { class: "tech-network-method", children: n.method }),
1210
+ /* @__PURE__ */ jsx2("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
1211
+ /* @__PURE__ */ jsxs2("span", { class: "tech-network-time", children: [
1212
+ Math.round(n.durationMs),
1213
+ "ms"
1214
+ ] })
1215
+ ] });
1216
+ }) })
1217
+ ] });
1218
+ }
1219
+ function truncateStack(stack) {
1220
+ const lines = stack.split("\n");
1221
+ if (lines.length <= 12) return stack;
1222
+ return lines.slice(0, 12).join("\n") + "\n \u2026";
1223
+ }
1224
+ function StatusHistorySection({ rows, strings }) {
1225
+ return /* @__PURE__ */ jsxs2("div", { class: "report-detail-history", children: [
1226
+ /* @__PURE__ */ jsx2("h3", { class: "report-detail-section", children: strings["detail.history"] }),
1227
+ /* @__PURE__ */ jsx2("ol", { class: "status-history", children: rows.map((r) => {
1228
+ const from = strings[`status.${r.from_status}`] ?? r.from_status;
1229
+ const to = strings[`status.${r.to_status}`] ?? r.to_status;
1230
+ const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
1231
+ return /* @__PURE__ */ jsxs2("li", { class: "status-history-row", children: [
1232
+ /* @__PURE__ */ jsx2("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
1233
+ /* @__PURE__ */ jsxs2("span", { class: "status-history-transition", children: [
1234
+ /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
1235
+ /* @__PURE__ */ jsx2("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
1236
+ /* @__PURE__ */ jsx2("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
1237
+ ] }),
1238
+ /* @__PURE__ */ jsx2("span", { class: `status-history-source status-history-source--${r.changed_by_source}`, children: strings[sourceKey] })
1239
+ ] });
1240
+ }) })
1241
+ ] });
1242
+ }
1243
+
1244
+ // src/widget/BoardView.tsx
1245
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1246
+ var POLL_MS2 = 3e4;
1247
+ var STATUSES = [
1248
+ "new",
1249
+ "in_progress",
1250
+ "awaiting_validation",
1251
+ "closed",
1252
+ "rejected"
1253
+ ];
1254
+ var TYPES = ["bug", "feature", "question", "praise", "typo"];
1255
+ var SEVERITIES = ["blocker", "high", "medium", "low"];
1256
+ function BoardView({ api, externalId, strings }) {
1257
+ const [filters, setFilters] = useState2({});
1258
+ const [page, setPage] = useState2(null);
1259
+ const [kpis, setKpis] = useState2(null);
1260
+ const [loading, setLoading] = useState2(true);
1261
+ const [error, setError] = useState2(null);
1262
+ const [selectedId, setSelectedId] = useState2(null);
1263
+ const [detailKey, setDetailKey] = useState2(0);
1264
+ const activeFilterCount = useMemo(() => {
1265
+ return (filters.status?.length ?? 0) + (filters.type?.length ?? 0) + (filters.severity?.length ?? 0) + (filters.q ? 1 : 0) + (filters.mine ? 1 : 0);
1266
+ }, [filters]);
1267
+ useEffect2(() => {
1268
+ let cancelled = false;
1269
+ let timer = null;
1270
+ const tick = async () => {
1271
+ try {
1272
+ const [list, k] = await Promise.all([
1273
+ api.listBoard(externalId, filters),
1274
+ api.fetchBoardKpis(externalId, filters)
1275
+ ]);
1276
+ if (cancelled) return;
1277
+ setPage(list);
1278
+ setKpis(k);
1279
+ setError(null);
1280
+ } catch (e) {
1281
+ if (cancelled) return;
1282
+ setError(e instanceof Error ? e.message : String(e));
1283
+ } finally {
1284
+ if (!cancelled) setLoading(false);
1285
+ if (!cancelled) timer = setTimeout(tick, POLL_MS2);
1286
+ }
1287
+ };
1288
+ setLoading(true);
1289
+ void tick();
1290
+ return () => {
1291
+ cancelled = true;
1292
+ if (timer) clearTimeout(timer);
1293
+ };
1294
+ }, [api, externalId, filtersHash(filters)]);
1295
+ const selectedRow = useMemo(() => {
1296
+ if (!selectedId || !page) return null;
1297
+ return page.results.find((r) => r.id === selectedId) ?? null;
1298
+ }, [selectedId, page]);
1299
+ const onPickRow = (row) => {
1300
+ setSelectedId(row.id);
1301
+ setDetailKey((k) => k + 1);
1302
+ };
1303
+ return /* @__PURE__ */ jsxs3("div", { class: "board-view", children: [
1304
+ /* @__PURE__ */ jsx3(BoardHeader, { strings, kpis }),
1305
+ /* @__PURE__ */ jsx3(
1306
+ BoardFilters,
1307
+ {
1308
+ filters,
1309
+ onChange: setFilters,
1310
+ activeCount: activeFilterCount,
1311
+ strings
1312
+ }
1313
+ ),
1314
+ /* @__PURE__ */ jsxs3("div", { class: "board-body", children: [
1315
+ /* @__PURE__ */ jsxs3("div", { class: "board-list-wrap", "aria-busy": loading && !page, children: [
1316
+ error && /* @__PURE__ */ jsx3("div", { class: "board-error", children: strings["board.list.error"] }),
1317
+ !error && loading && !page && /* @__PURE__ */ jsx3(BoardListSkeleton, {}),
1318
+ !error && page && page.results.length === 0 && !loading && /* @__PURE__ */ jsx3(BoardEmpty, { strings }),
1319
+ !error && page && page.results.length > 0 && /* @__PURE__ */ jsx3(
1320
+ BoardList,
1321
+ {
1322
+ rows: page.results,
1323
+ selectedId,
1324
+ onPick: onPickRow,
1325
+ strings,
1326
+ total: page.count
1327
+ }
1328
+ )
1329
+ ] }),
1330
+ /* @__PURE__ */ jsx3("div", { class: `board-detail-wrap ${selectedRow ? "has-selection" : ""}`, children: selectedRow ? /* @__PURE__ */ jsx3(
1331
+ ReportDetailView,
1332
+ {
1333
+ api,
1334
+ externalId,
1335
+ reportId: selectedRow.id,
1336
+ strings,
1337
+ onBack: () => setSelectedId(null),
1338
+ canModerate: selectedRow.is_mine,
1339
+ variant: "board"
1340
+ },
1341
+ detailKey
1342
+ ) : /* @__PURE__ */ jsxs3("div", { class: "board-detail-empty", children: [
1343
+ /* @__PURE__ */ jsx3(DetailEmptyIllustration, {}),
1344
+ /* @__PURE__ */ jsx3("p", { children: strings["board.detail.empty"] })
1345
+ ] }) })
1346
+ ] })
1347
+ ] });
1348
+ }
1349
+ function BoardHeader({
1350
+ strings,
1351
+ kpis
1352
+ }) {
1353
+ const scopeLabel = kpis?.scope === "project" ? strings["board.scope.project"] : strings["board.scope.mine"];
1354
+ return /* @__PURE__ */ jsxs3("header", { class: "board-header", children: [
1355
+ /* @__PURE__ */ jsxs3("div", { class: "board-header-title", children: [
1356
+ /* @__PURE__ */ jsx3("span", { class: "board-header-emoji", "aria-hidden": "true", children: "\u{1F4CB}" }),
1357
+ /* @__PURE__ */ jsxs3("div", { children: [
1358
+ /* @__PURE__ */ jsx3("h2", { class: "board-header-h", children: strings["tab.board"] }),
1359
+ /* @__PURE__ */ jsx3("p", { class: "board-header-sub", children: kpis ? `${kpis.total} \xB7 ${scopeLabel}` : scopeLabel })
1360
+ ] })
1361
+ ] }),
1362
+ /* @__PURE__ */ jsx3(BoardKpiStrip, { kpis, strings })
1363
+ ] });
1364
+ }
1365
+ function BoardKpiStrip({
1366
+ kpis,
1367
+ strings
1368
+ }) {
1369
+ const cells = [
1370
+ {
1371
+ key: "new",
1372
+ label: strings["board.kpi.new"],
1373
+ value: String(kpis?.by_status?.new ?? 0),
1374
+ tone: "tone-new"
1375
+ },
1376
+ {
1377
+ key: "in_progress",
1378
+ label: strings["board.kpi.in_progress"],
1379
+ value: String(kpis?.by_status?.in_progress ?? 0),
1380
+ tone: "tone-progress"
1381
+ },
1382
+ {
1383
+ key: "awaiting_validation",
1384
+ label: strings["board.kpi.awaiting_validation"],
1385
+ value: String(kpis?.by_status?.awaiting_validation ?? 0),
1386
+ tone: "tone-validation"
1387
+ },
1388
+ {
1389
+ key: "rate",
1390
+ label: strings["board.kpi.resolution_rate"],
1391
+ value: kpis ? `${Math.round((kpis.resolution_rate || 0) * 100)}%` : "\u2014",
1392
+ tone: "tone-rate"
1393
+ }
1394
+ ];
1395
+ 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: [
1396
+ /* @__PURE__ */ jsx3("div", { class: "board-kpi-value", children: c.value }),
1397
+ /* @__PURE__ */ jsx3("div", { class: "board-kpi-label", children: c.label })
1398
+ ] }, c.key)) });
1399
+ }
1400
+ function BoardFilters({
1401
+ filters,
1402
+ onChange,
1403
+ activeCount,
1404
+ strings
1405
+ }) {
1406
+ const setStatus = (value) => {
1407
+ if (value === "") {
1408
+ const { status: _drop, ...rest } = filters;
1409
+ void _drop;
1410
+ onChange(rest);
1411
+ } else {
1412
+ onChange({ ...filters, status: [value] });
1413
+ }
1414
+ };
1415
+ const setType = (value) => {
1416
+ if (value === "") {
1417
+ const { type: _drop, ...rest } = filters;
1418
+ void _drop;
1419
+ onChange(rest);
1420
+ } else {
1421
+ onChange({ ...filters, type: [value] });
1422
+ }
1423
+ };
1424
+ const setSeverity = (value) => {
1425
+ if (value === "") {
1426
+ const { severity: _drop, ...rest } = filters;
1427
+ void _drop;
1428
+ onChange(rest);
1429
+ } else {
1430
+ onChange({ ...filters, severity: [value] });
1431
+ }
1432
+ };
1433
+ const setSearch = (q) => onChange({ ...filters, q });
1434
+ const toggleMine = () => {
1435
+ if (filters.mine) {
1436
+ const { mine: _drop, ...rest } = filters;
1437
+ void _drop;
1438
+ onChange(rest);
1439
+ } else {
1440
+ onChange({ ...filters, mine: true });
1441
+ }
1442
+ };
1443
+ const clear = () => onChange({});
1444
+ return /* @__PURE__ */ jsxs3("div", { class: "board-filters", children: [
1445
+ /* @__PURE__ */ jsxs3(
1446
+ "select",
1447
+ {
1448
+ class: "board-filter-select",
1449
+ "aria-label": strings["board.filter.status"],
1450
+ value: filters.status?.[0] ?? "",
1451
+ onChange: (e) => setStatus(e.target.value),
1452
+ children: [
1453
+ /* @__PURE__ */ jsx3("option", { value: "", children: strings["board.filter.status"] }),
1454
+ STATUSES.map((s) => /* @__PURE__ */ jsx3("option", { value: s, children: strings[`board.kpi.${s}`] ?? s }, s))
1455
+ ]
1456
+ }
1457
+ ),
1458
+ /* @__PURE__ */ jsxs3(
1459
+ "select",
1460
+ {
1461
+ class: "board-filter-select",
1462
+ "aria-label": strings["board.filter.type"],
1463
+ value: filters.type?.[0] ?? "",
1464
+ onChange: (e) => setType(e.target.value),
1465
+ children: [
1466
+ /* @__PURE__ */ jsx3("option", { value: "", children: strings["board.filter.type"] }),
1467
+ TYPES.map((t) => /* @__PURE__ */ jsx3("option", { value: t, children: strings[`type.${t}`] ?? t }, t))
1468
+ ]
1469
+ }
1470
+ ),
1471
+ /* @__PURE__ */ jsxs3(
1472
+ "select",
1473
+ {
1474
+ class: "board-filter-select",
1475
+ "aria-label": strings["board.filter.severity"],
1476
+ value: filters.severity?.[0] ?? "",
1477
+ onChange: (e) => setSeverity(e.target.value),
1478
+ children: [
1479
+ /* @__PURE__ */ jsx3("option", { value: "", children: strings["board.filter.severity"] }),
1480
+ SEVERITIES.map((s) => /* @__PURE__ */ jsx3("option", { value: s, children: strings[`severity.${s}`] ?? s }, s))
1481
+ ]
1482
+ }
1483
+ ),
1484
+ /* @__PURE__ */ jsx3(
1485
+ "input",
1486
+ {
1487
+ type: "search",
1488
+ class: "board-filter-search",
1489
+ placeholder: strings["board.filter.search.placeholder"],
1490
+ value: filters.q ?? "",
1491
+ onInput: (e) => setSearch(e.target.value)
1492
+ }
1493
+ ),
1494
+ /* @__PURE__ */ jsxs3("label", { class: "board-filter-toggle", children: [
1495
+ /* @__PURE__ */ jsx3(
1496
+ "input",
1497
+ {
1498
+ type: "checkbox",
1499
+ checked: Boolean(filters.mine),
1500
+ onChange: toggleMine
1501
+ }
1502
+ ),
1503
+ /* @__PURE__ */ jsx3("span", { children: strings["board.filter.mine"] })
1504
+ ] }),
1505
+ activeCount > 0 && /* @__PURE__ */ jsxs3("button", { type: "button", class: "board-filter-clear", onClick: clear, children: [
1506
+ /* @__PURE__ */ jsx3(CloseIcon, {}),
1507
+ strings["board.filter.clear"]
1508
+ ] })
1509
+ ] });
1510
+ }
1511
+ function BoardList({
1512
+ rows,
1513
+ selectedId,
1514
+ onPick,
1515
+ strings,
1516
+ total
1517
+ }) {
1518
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
1519
+ /* @__PURE__ */ jsx3("div", { class: "board-list-count", children: strings["board.list.count"].replace("{n}", String(rows.length)).replace("{total}", String(total)) }),
1520
+ /* @__PURE__ */ jsx3("ul", { class: "board-list", role: "list", children: rows.map((row) => /* @__PURE__ */ jsx3(
1521
+ BoardRowCard,
1522
+ {
1523
+ row,
1524
+ selected: row.id === selectedId,
1525
+ onPick,
1526
+ strings
1527
+ },
1528
+ row.id
1529
+ )) })
1530
+ ] });
1531
+ }
1532
+ function BoardRowCard({
1533
+ row,
1534
+ selected,
1535
+ onPick,
1536
+ strings
1537
+ }) {
1538
+ const author = row.is_mine ? strings["board.list.you"] : row.author_label || "\u2014";
1539
+ return /* @__PURE__ */ jsx3("li", { children: /* @__PURE__ */ jsxs3(
1540
+ "button",
1541
+ {
1542
+ type: "button",
1543
+ class: `board-row ${selected ? "is-selected" : ""}`,
1544
+ onClick: () => onPick(row),
1545
+ "aria-pressed": selected,
1546
+ children: [
1547
+ /* @__PURE__ */ jsxs3("div", { class: "board-row-badges", children: [
1548
+ /* @__PURE__ */ jsx3("span", { class: `badge status-${row.status}`, children: strings[`status.${row.status}`] ?? row.status }),
1549
+ /* @__PURE__ */ jsx3("span", { class: `badge severity-${row.severity}`, children: strings[`severity.${row.severity}`] ?? row.severity })
1550
+ ] }),
1551
+ /* @__PURE__ */ jsx3("div", { class: "board-row-description", children: row.description }),
1552
+ /* @__PURE__ */ jsxs3("div", { class: "board-row-meta", children: [
1553
+ /* @__PURE__ */ jsx3("span", { class: "board-row-author", children: author }),
1554
+ row.comment_count > 0 && /* @__PURE__ */ jsxs3("span", { class: "board-row-comments", children: [
1555
+ /* @__PURE__ */ jsx3(CommentIcon, {}),
1556
+ " ",
1557
+ row.comment_count
1558
+ ] }),
1559
+ /* @__PURE__ */ jsx3("span", { class: "board-row-time", children: formatRelative(row.created_at) })
1560
+ ] })
1561
+ ]
1562
+ }
1563
+ ) });
1564
+ }
1565
+ function BoardEmpty({ strings }) {
1566
+ return /* @__PURE__ */ jsxs3("div", { class: "board-empty", children: [
1567
+ /* @__PURE__ */ jsx3(EmptyIllustration, {}),
1568
+ /* @__PURE__ */ jsx3("h3", { children: strings["board.list.empty.title"] }),
1569
+ /* @__PURE__ */ jsx3("p", { children: strings["board.list.empty.description"] })
1570
+ ] });
1571
+ }
1572
+ function BoardListSkeleton() {
1573
+ 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: [
1574
+ /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-badges" }),
1575
+ /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text" }),
1576
+ /* @__PURE__ */ jsx3("div", { class: "skeleton-line skeleton-text short" })
1577
+ ] }) }, i)) });
1578
+ }
1579
+ function CommentIcon() {
1580
+ 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" }) });
1581
+ }
1582
+ function CloseIcon() {
1583
+ 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: [
1584
+ /* @__PURE__ */ jsx3("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
1585
+ /* @__PURE__ */ jsx3("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
1586
+ ] });
1587
+ }
1588
+ function EmptyIllustration() {
1589
+ return /* @__PURE__ */ jsxs3("svg", { width: "64", height: "64", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1590
+ /* @__PURE__ */ jsx3("circle", { cx: "32", cy: "32", r: "28", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2" }),
1591
+ /* @__PURE__ */ jsx3("path", { d: "M22 32h20M32 22v20", stroke: "currentColor", "stroke-opacity": "0.35", "stroke-width": "2", "stroke-linecap": "round" })
1592
+ ] });
1593
+ }
1594
+ function DetailEmptyIllustration() {
1595
+ return /* @__PURE__ */ jsxs3("svg", { width: "80", height: "80", viewBox: "0 0 64 64", fill: "none", "aria-hidden": "true", children: [
1596
+ /* @__PURE__ */ jsx3("rect", { x: "10", y: "12", width: "44", height: "36", rx: "4", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2" }),
1597
+ /* @__PURE__ */ jsx3("line", { x1: "18", y1: "22", x2: "46", y2: "22", stroke: "currentColor", "stroke-opacity": "0.22", "stroke-width": "2", "stroke-linecap": "round" }),
1598
+ /* @__PURE__ */ jsx3("line", { x1: "18", y1: "30", x2: "38", y2: "30", stroke: "currentColor", "stroke-opacity": "0.18", "stroke-width": "2", "stroke-linecap": "round" }),
1599
+ /* @__PURE__ */ jsx3("line", { x1: "18", y1: "38", x2: "32", y2: "38", stroke: "currentColor", "stroke-opacity": "0.14", "stroke-width": "2", "stroke-linecap": "round" })
1600
+ ] });
1601
+ }
1602
+ function filtersHash(f) {
1603
+ return JSON.stringify({
1604
+ s: f.status?.slice().sort(),
1605
+ t: f.type?.slice().sort(),
1606
+ sv: f.severity?.slice().sort(),
1607
+ q: f.q ?? "",
1608
+ m: Boolean(f.mine)
1609
+ });
1610
+ }
1611
+ function formatRelative(iso) {
1612
+ const then = new Date(iso).getTime();
1613
+ if (Number.isNaN(then)) return "";
1614
+ const delta = Math.max(0, Date.now() - then);
1615
+ const m = Math.floor(delta / 6e4);
1616
+ if (m < 1) return "just now";
1617
+ if (m < 60) return `${m}m`;
1618
+ const h2 = Math.floor(m / 60);
1619
+ if (h2 < 24) return `${h2}h`;
1620
+ const d = Math.floor(h2 / 24);
1621
+ if (d < 7) return `${d}d`;
1622
+ const w = Math.floor(d / 7);
1623
+ return `${w}w`;
1624
+ }
1625
+
1626
+ // src/widget/ChangelogList.tsx
1627
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "preact/hooks";
1628
+
1629
+ // src/widget/ReportRow.tsx
1630
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
1631
+ function statusClassName(status) {
1632
+ return `pill pill-status pill-status--${status}`;
1633
+ }
1634
+ function severityClassName(severity) {
1635
+ return `pill pill-severity pill-severity--${severity}`;
1636
+ }
1637
+ function typeClassName() {
1638
+ return "pill pill-type";
1639
+ }
1640
+ function formatRelative2(iso) {
1641
+ const then = Date.parse(iso);
1642
+ if (!Number.isFinite(then)) return "";
1643
+ const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
1644
+ if (seconds < 60) return `${seconds}s`;
1645
+ const minutes = Math.round(seconds / 60);
1646
+ if (minutes < 60) return `${minutes}m`;
1647
+ const hours = Math.round(minutes / 60);
1648
+ if (hours < 48) return `${hours}h`;
1649
+ const days = Math.round(hours / 24);
1650
+ return `${days}d`;
1651
+ }
1652
+ function repliesLabel(count, strings) {
1653
+ if (count === 1) return strings["mine.replies_one"];
1654
+ return strings["mine.replies_many"].replace("{count}", String(count));
1655
+ }
1656
+ function ReportRow({ row, strings, onClick }) {
1657
+ const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
1658
+ return /* @__PURE__ */ jsxs4("button", { type: "button", class: "mine-row", onClick, children: [
1659
+ /* @__PURE__ */ jsxs4("div", { class: "mine-row-pills", children: [
1660
+ /* @__PURE__ */ jsx4("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
1661
+ /* @__PURE__ */ jsx4("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
1662
+ /* @__PURE__ */ jsx4("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
1663
+ ] }),
1664
+ /* @__PURE__ */ jsx4("div", { class: "mine-row-preview", children: preview }),
1665
+ /* @__PURE__ */ jsxs4("div", { class: "mine-row-meta", children: [
1666
+ /* @__PURE__ */ jsx4("span", { children: formatRelative2(row.updated_at || row.created_at) }),
1667
+ row.comment_count > 0 && /* @__PURE__ */ jsxs4("span", { children: [
1668
+ "\xB7 ",
1669
+ repliesLabel(row.comment_count, strings)
1670
+ ] })
1671
+ ] })
1672
+ ] });
1673
+ }
1674
+
1675
+ // src/widget/ChangelogList.tsx
1676
+ import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
1677
+ var POLL_MS3 = 3e4;
1678
+ function isoWeekKey(iso) {
1679
+ const d = new Date(iso);
1680
+ if (Number.isNaN(d.getTime())) return "";
1681
+ const day = (d.getUTCDay() + 6) % 7;
1682
+ const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - day));
1683
+ return monday.toISOString().slice(0, 10);
1684
+ }
1685
+ function groupRowsByWeek(rows) {
1686
+ const buckets = /* @__PURE__ */ new Map();
1687
+ for (const row of rows) {
1688
+ const key = isoWeekKey(row.resolved_at);
1689
+ if (!key) continue;
1690
+ const existing = buckets.get(key);
1691
+ if (existing) existing.push(row);
1692
+ else buckets.set(key, [row]);
1693
+ }
1694
+ return Array.from(buckets.entries()).sort(([a], [b]) => a < b ? 1 : -1).map(([weekKey, weekRows]) => ({
1695
+ weekKey,
1696
+ label: formatWeekLabel(weekKey),
1697
+ rows: weekRows
1698
+ }));
1699
+ }
1700
+ function formatWeekLabel(weekKey) {
1701
+ try {
1702
+ return new Date(weekKey).toLocaleDateString(void 0, {
1703
+ year: "numeric",
1704
+ month: "short",
1705
+ day: "numeric"
1706
+ });
1707
+ } catch {
1708
+ return weekKey;
1709
+ }
1710
+ }
1711
+ function ChangelogList({ api, externalId, strings, onSelect }) {
1712
+ const [rows, setRows] = useState3(null);
1713
+ const [error, setError] = useState3(null);
1714
+ const [refreshing, setRefreshing] = useState3(false);
1715
+ const mountedRef = useRef2(true);
1716
+ const fetchRows = async () => {
1717
+ setRefreshing(true);
1718
+ setError(null);
1719
+ try {
1720
+ const next = await api.listChangelog(externalId);
1721
+ if (!mountedRef.current) return;
1722
+ setRows(next);
1723
+ } catch (err) {
1724
+ if (!mountedRef.current) return;
1725
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
1726
+ } finally {
1727
+ if (mountedRef.current) setRefreshing(false);
1728
+ }
1729
+ };
1730
+ useEffect3(() => {
1731
+ mountedRef.current = true;
1732
+ void fetchRows();
1733
+ const timer = setInterval(() => {
1734
+ void fetchRows();
1735
+ }, POLL_MS3);
1736
+ return () => {
1737
+ mountedRef.current = false;
1738
+ clearInterval(timer);
1739
+ };
1740
+ }, [externalId]);
1741
+ const groups = useMemo2(() => rows ? groupRowsByWeek(rows) : [], [rows]);
1742
+ const isLoading = rows === null && !error;
1743
+ const isEmpty = rows !== null && rows.length === 0;
1744
+ return /* @__PURE__ */ jsxs5("div", { class: "mine-list", children: [
1745
+ /* @__PURE__ */ jsxs5("div", { class: "mine-list-header", children: [
1746
+ /* @__PURE__ */ jsx5("h2", { children: strings["tab.changelog"] }),
1747
+ /* @__PURE__ */ jsx5(
1748
+ "button",
1749
+ {
1750
+ type: "button",
1751
+ class: "btn",
1752
+ onClick: () => {
1753
+ void fetchRows();
1754
+ },
1755
+ disabled: refreshing,
1756
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1757
+ }
1758
+ )
1759
+ ] }),
1760
+ isLoading && /* @__PURE__ */ jsx5("div", { class: "mine-loading", children: strings["mine.loading"] }),
1761
+ error && /* @__PURE__ */ jsx5("div", { class: "error", children: error }),
1762
+ isEmpty && /* @__PURE__ */ jsxs5("div", { class: "mine-empty", children: [
1763
+ /* @__PURE__ */ jsx5("strong", { children: strings["changelog.empty.title"] }),
1764
+ /* @__PURE__ */ jsx5("p", { children: strings["changelog.empty.body"] })
1765
+ ] }),
1766
+ groups.length > 0 && /* @__PURE__ */ jsx5("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs5("section", { class: "changelog-group", children: [
1767
+ /* @__PURE__ */ jsxs5("header", { class: "changelog-group-header", children: [
1768
+ /* @__PURE__ */ jsx5("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
1769
+ /* @__PURE__ */ jsx5("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
1770
+ /* @__PURE__ */ jsx5("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
1771
+ /* @__PURE__ */ jsx5("span", { class: "changelog-group-count", children: strings[g.rows.length === 1 ? "changelog.resolved_one" : "changelog.resolved_many"].replace("{count}", String(g.rows.length)) })
1772
+ ] }),
1773
+ /* @__PURE__ */ jsx5("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
1774
+ ] }, g.weekKey)) })
1775
+ ] });
1776
+ }
1777
+
1778
+ // src/widget/Fab.tsx
1779
+ import { jsx as jsx6 } from "preact/jsx-runtime";
1780
+ function ChatBubbleIcon() {
1781
+ return /* @__PURE__ */ jsx6(
1782
+ "svg",
1783
+ {
1784
+ width: "24",
1785
+ height: "24",
1786
+ viewBox: "0 0 24 24",
836
1787
  fill: "none",
837
1788
  "aria-hidden": "true",
838
1789
  focusable: "false",
839
- children: /* @__PURE__ */ jsx3(
1790
+ children: /* @__PURE__ */ jsx6(
840
1791
  "path",
841
1792
  {
842
1793
  d: "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z",
@@ -847,15 +1798,15 @@ function ChatBubbleIcon() {
847
1798
  );
848
1799
  }
849
1800
  function Fab({ label, onClick }) {
850
- return /* @__PURE__ */ jsx3("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: /* @__PURE__ */ jsx3(ChatBubbleIcon, {}) });
1801
+ return /* @__PURE__ */ jsx6("button", { type: "button", class: "fab", "aria-label": label, title: label, onClick, children: /* @__PURE__ */ jsx6(ChatBubbleIcon, {}) });
851
1802
  }
852
1803
 
853
1804
  // src/widget/Form.tsx
854
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
1805
+ import { useEffect as useEffect5, useRef as useRef4, useState as useState5 } from "preact/hooks";
855
1806
 
856
1807
  // src/widget/Annotator.tsx
857
- import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "preact/hooks";
858
- import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
1808
+ import { useEffect as useEffect4, useLayoutEffect, useRef as useRef3, useState as useState4 } from "preact/hooks";
1809
+ import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
859
1810
  var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
860
1811
  var HIGHLIGHT_ALPHA = 0.35;
861
1812
  function drawShape(ctx, shape, sourceImage) {
@@ -977,44 +1928,44 @@ function drawBlur(ctx, shape, sourceImage) {
977
1928
  ctx.imageSmoothingEnabled = true;
978
1929
  }
979
1930
  var Icon = {
980
- rect: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("rect", { x: "2", y: "3", width: "12", height: "10", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }) }),
981
- arrow: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
982
- pencil: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
983
- text: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
984
- highlight: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
985
- /* @__PURE__ */ jsx4("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" }),
986
- /* @__PURE__ */ jsx4("path", { d: "M2 14h12", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" })
1931
+ rect: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("rect", { x: "2", y: "3", width: "12", height: "10", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }) }),
1932
+ 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" }) }),
1933
+ 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" }) }),
1934
+ 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" }) }),
1935
+ highlight: /* @__PURE__ */ jsxs6("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
1936
+ /* @__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" }),
1937
+ /* @__PURE__ */ jsx7("path", { d: "M2 14h12", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" })
987
1938
  ] }),
988
- blur: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
989
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
990
- /* @__PURE__ */ jsx4("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
991
- /* @__PURE__ */ jsx4("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
992
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.6" }),
993
- /* @__PURE__ */ jsx4("rect", { x: "6", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.85" }),
994
- /* @__PURE__ */ jsx4("rect", { x: "10", y: "7", width: "4", height: "3", fill: "currentColor", opacity: "0.5" }),
995
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "11", width: "4", height: "3", fill: "currentColor", opacity: "0.4" }),
996
- /* @__PURE__ */ jsx4("rect", { x: "7", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.65" }),
997
- /* @__PURE__ */ jsx4("rect", { x: "11", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.85" })
1939
+ blur: /* @__PURE__ */ jsxs6("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
1940
+ /* @__PURE__ */ jsx7("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
1941
+ /* @__PURE__ */ jsx7("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
1942
+ /* @__PURE__ */ jsx7("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
1943
+ /* @__PURE__ */ jsx7("rect", { x: "2", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.6" }),
1944
+ /* @__PURE__ */ jsx7("rect", { x: "6", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.85" }),
1945
+ /* @__PURE__ */ jsx7("rect", { x: "10", y: "7", width: "4", height: "3", fill: "currentColor", opacity: "0.5" }),
1946
+ /* @__PURE__ */ jsx7("rect", { x: "2", y: "11", width: "4", height: "3", fill: "currentColor", opacity: "0.4" }),
1947
+ /* @__PURE__ */ jsx7("rect", { x: "7", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.65" }),
1948
+ /* @__PURE__ */ jsx7("rect", { x: "11", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.85" })
998
1949
  ] }),
999
- undo: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 7l3-3M4 7l3 3M4 7h6a3 3 0 0 1 0 6H7", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1000
- redo: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M12 7l-3-3M12 7l-3 3M12 7H6a3 3 0 0 0 0 6h2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1001
- trash: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 4h10M6 4V2.5h4V4M5 4l.5 9h5L11 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1002
- close: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 4l8 8M12 4l-8 8", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) })
1950
+ undo: /* @__PURE__ */ jsx7("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M4 7l3-3M4 7l3 3M4 7h6a3 3 0 0 1 0 6H7", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1951
+ redo: /* @__PURE__ */ jsx7("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M12 7l-3-3M12 7l-3 3M12 7H6a3 3 0 0 0 0 6h2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1952
+ trash: /* @__PURE__ */ jsx7("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M3 4h10M6 4V2.5h4V4M5 4l.5 9h5L11 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
1953
+ close: /* @__PURE__ */ jsx7("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx7("path", { d: "M4 4l8 8M12 4l-8 8", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) })
1003
1954
  };
1004
1955
  function Annotator({ imageBlob, strings, onSave, onCancel }) {
1005
- const canvasRef = useRef2(null);
1006
- const containerRef = useRef2(null);
1007
- const imageRef = useRef2(null);
1008
- const [tool, setTool] = useState2("rectangle");
1009
- const [color, setColor] = useState2(COLORS[0]);
1010
- const [shapes, setShapes] = useState2([]);
1011
- const [past, setPast] = useState2([]);
1012
- const [future, setFuture] = useState2([]);
1013
- const isDrawingRef = useRef2(false);
1014
- const [draftShape, setDraftShape] = useState2(null);
1015
- const [imageLoaded, setImageLoaded] = useState2(false);
1016
- const [saving, setSaving] = useState2(false);
1017
- const scaleRef = useRef2(1);
1956
+ const canvasRef = useRef3(null);
1957
+ const containerRef = useRef3(null);
1958
+ const imageRef = useRef3(null);
1959
+ const [tool, setTool] = useState4("rectangle");
1960
+ const [color, setColor] = useState4(COLORS[0]);
1961
+ const [shapes, setShapes] = useState4([]);
1962
+ const [past, setPast] = useState4([]);
1963
+ const [future, setFuture] = useState4([]);
1964
+ const isDrawingRef = useRef3(false);
1965
+ const [draftShape, setDraftShape] = useState4(null);
1966
+ const [imageLoaded, setImageLoaded] = useState4(false);
1967
+ const [saving, setSaving] = useState4(false);
1968
+ const scaleRef = useRef3(1);
1018
1969
  function addShape(shape) {
1019
1970
  setShapes((s) => {
1020
1971
  setPast((p) => [...p, s]);
@@ -1061,7 +2012,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1061
2012
  window.addEventListener("keydown", onKey);
1062
2013
  return () => window.removeEventListener("keydown", onKey);
1063
2014
  }, [onCancel]);
1064
- useEffect2(() => {
2015
+ useEffect4(() => {
1065
2016
  const onKey = (e) => {
1066
2017
  const mod = e.metaKey || e.ctrlKey;
1067
2018
  if (!mod) return;
@@ -1080,7 +2031,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1080
2031
  window.addEventListener("keydown", onKey);
1081
2032
  return () => window.removeEventListener("keydown", onKey);
1082
2033
  }, [past, future, shapes]);
1083
- useEffect2(() => {
2034
+ useEffect4(() => {
1084
2035
  const url = URL.createObjectURL(imageBlob);
1085
2036
  const img = new Image();
1086
2037
  img.onload = () => {
@@ -1090,7 +2041,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1090
2041
  img.src = url;
1091
2042
  return () => URL.revokeObjectURL(url);
1092
2043
  }, [imageBlob]);
1093
- useEffect2(() => {
2044
+ useEffect4(() => {
1094
2045
  if (!imageLoaded || !canvasRef.current || !imageRef.current || !containerRef.current) {
1095
2046
  return;
1096
2047
  }
@@ -1107,7 +2058,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1107
2058
  canvas.style.height = `${img.height * fitScale}px`;
1108
2059
  redraw();
1109
2060
  }, [imageLoaded]);
1110
- useEffect2(() => {
2061
+ useEffect4(() => {
1111
2062
  redraw();
1112
2063
  }, [shapes, draftShape]);
1113
2064
  function redraw() {
@@ -1181,28 +2132,36 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1181
2132
  }
1182
2133
  };
1183
2134
  const handleMouseMove = (e) => {
1184
- if (!isDrawingRef.current || !draftShape) return;
2135
+ if (!isDrawingRef.current) return;
1185
2136
  const { x, y } = getCanvasCoords(e);
1186
- if (draftShape.kind === "rectangle" || draftShape.kind === "highlight" || draftShape.kind === "blur") {
1187
- setDraftShape({ ...draftShape, w: x - draftShape.x, h: y - draftShape.y });
1188
- } else if (draftShape.kind === "arrow") {
1189
- setDraftShape({ ...draftShape, x2: x, y2: y });
1190
- } else if (draftShape.kind === "freehand") {
1191
- setDraftShape({ ...draftShape, points: [...draftShape.points, { x, y }] });
1192
- }
2137
+ setDraftShape((current) => {
2138
+ if (!current) return current;
2139
+ if (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") {
2140
+ return { ...current, w: x - current.x, h: y - current.y };
2141
+ }
2142
+ if (current.kind === "arrow") {
2143
+ return { ...current, x2: x, y2: y };
2144
+ }
2145
+ if (current.kind === "freehand") {
2146
+ return { ...current, points: [...current.points, { x, y }] };
2147
+ }
2148
+ return current;
2149
+ });
1193
2150
  };
1194
2151
  const handleMouseUp = () => {
1195
- if (isDrawingRef.current && draftShape) {
1196
- const isTiny = (draftShape.kind === "rectangle" || draftShape.kind === "highlight" || draftShape.kind === "blur") && Math.abs(draftShape.w) < 4 && Math.abs(draftShape.h) < 4 || draftShape.kind === "arrow" && Math.hypot(
1197
- draftShape.x2 - draftShape.x1,
1198
- draftShape.y2 - draftShape.y1
1199
- ) < 4 || draftShape.kind === "freehand" && draftShape.points.length < 3;
1200
- if (!isTiny) {
1201
- addShape(draftShape);
2152
+ setDraftShape((current) => {
2153
+ if (isDrawingRef.current && current) {
2154
+ const isTiny = (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") && Math.abs(current.w) < 4 && Math.abs(current.h) < 4 || current.kind === "arrow" && Math.hypot(
2155
+ current.x2 - current.x1,
2156
+ current.y2 - current.y1
2157
+ ) < 4 || current.kind === "freehand" && current.points.length < 3;
2158
+ if (!isTiny) {
2159
+ addShape(current);
2160
+ }
1202
2161
  }
1203
- }
2162
+ return null;
2163
+ });
1204
2164
  isDrawingRef.current = false;
1205
- setDraftShape(null);
1206
2165
  };
1207
2166
  const handleSave = async () => {
1208
2167
  const canvas = canvasRef.current;
@@ -1225,7 +2184,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1225
2184
  { id: "highlight", icon: Icon.highlight, label: strings["annotator.tool.highlight"] },
1226
2185
  { id: "blur", icon: Icon.blur, label: strings["annotator.tool.blur"] }
1227
2186
  ];
1228
- return /* @__PURE__ */ jsx4(
2187
+ return /* @__PURE__ */ jsx7(
1229
2188
  "div",
1230
2189
  {
1231
2190
  class: "annotator-backdrop",
@@ -1233,10 +2192,10 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1233
2192
  onClick: (e) => {
1234
2193
  if (e.target === e.currentTarget) onCancel();
1235
2194
  },
1236
- children: /* @__PURE__ */ jsxs3("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
1237
- /* @__PURE__ */ jsxs3("div", { class: "annotator-header", children: [
1238
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.title"] }),
1239
- /* @__PURE__ */ jsx4(
2195
+ children: /* @__PURE__ */ jsxs6("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
2196
+ /* @__PURE__ */ jsxs6("div", { class: "annotator-header", children: [
2197
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.title"] }),
2198
+ /* @__PURE__ */ jsx7(
1240
2199
  "button",
1241
2200
  {
1242
2201
  type: "button",
@@ -1247,8 +2206,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1247
2206
  }
1248
2207
  )
1249
2208
  ] }),
1250
- /* @__PURE__ */ jsxs3("div", { class: "annotator-toolbar", children: [
1251
- /* @__PURE__ */ jsx4("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx4(
2209
+ /* @__PURE__ */ jsxs6("div", { class: "annotator-toolbar", children: [
2210
+ /* @__PURE__ */ jsx7("div", { class: "annotator-tools", children: tools.map((t) => /* @__PURE__ */ jsx7(
1252
2211
  "button",
1253
2212
  {
1254
2213
  type: "button",
@@ -1261,9 +2220,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1261
2220
  },
1262
2221
  t.id
1263
2222
  )) }),
1264
- /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1265
- /* @__PURE__ */ jsxs3("div", { class: "annotator-colors", children: [
1266
- COLORS.map((c) => /* @__PURE__ */ jsx4(
2223
+ /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2224
+ /* @__PURE__ */ jsxs6("div", { class: "annotator-colors", children: [
2225
+ COLORS.map((c) => /* @__PURE__ */ jsx7(
1267
2226
  "button",
1268
2227
  {
1269
2228
  type: "button",
@@ -1275,7 +2234,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1275
2234
  },
1276
2235
  c
1277
2236
  )),
1278
- /* @__PURE__ */ jsx4("label", { class: "annotator-color annotator-color-picker", title: strings["annotator.color_picker"], children: /* @__PURE__ */ jsx4(
2237
+ /* @__PURE__ */ jsx7("label", { class: "annotator-color annotator-color-picker", title: strings["annotator.color_picker"], children: /* @__PURE__ */ jsx7(
1279
2238
  "input",
1280
2239
  {
1281
2240
  type: "color",
@@ -1285,8 +2244,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1285
2244
  }
1286
2245
  ) })
1287
2246
  ] }),
1288
- /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1289
- /* @__PURE__ */ jsxs3(
2247
+ /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2248
+ /* @__PURE__ */ jsxs6(
1290
2249
  "button",
1291
2250
  {
1292
2251
  type: "button",
@@ -1296,11 +2255,11 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1296
2255
  title: strings["annotator.undo"],
1297
2256
  children: [
1298
2257
  Icon.undo,
1299
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.undo"] })
2258
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.undo"] })
1300
2259
  ]
1301
2260
  }
1302
2261
  ),
1303
- /* @__PURE__ */ jsxs3(
2262
+ /* @__PURE__ */ jsxs6(
1304
2263
  "button",
1305
2264
  {
1306
2265
  type: "button",
@@ -1310,11 +2269,11 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1310
2269
  title: strings["annotator.redo"],
1311
2270
  children: [
1312
2271
  Icon.redo,
1313
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.redo"] })
2272
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.redo"] })
1314
2273
  ]
1315
2274
  }
1316
2275
  ),
1317
- /* @__PURE__ */ jsxs3(
2276
+ /* @__PURE__ */ jsxs6(
1318
2277
  "button",
1319
2278
  {
1320
2279
  type: "button",
@@ -1323,18 +2282,18 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1323
2282
  disabled: shapes.length === 0,
1324
2283
  children: [
1325
2284
  Icon.trash,
1326
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.clear"] })
2285
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.clear"] })
1327
2286
  ]
1328
2287
  }
1329
2288
  ),
1330
- /* @__PURE__ */ jsx4("span", { class: "annotator-spacer" }),
1331
- /* @__PURE__ */ jsxs3("span", { class: "annotator-count", children: [
2289
+ /* @__PURE__ */ jsx7("span", { class: "annotator-spacer" }),
2290
+ /* @__PURE__ */ jsxs6("span", { class: "annotator-count", children: [
1332
2291
  shapes.length,
1333
2292
  " ",
1334
2293
  strings["annotator.count_suffix"]
1335
2294
  ] })
1336
2295
  ] }),
1337
- /* @__PURE__ */ jsx4("div", { ref: containerRef, class: "annotator-canvas-wrap", children: !imageLoaded ? /* @__PURE__ */ jsx4("span", { class: "annotator-loading", children: strings["annotator.loading"] }) : /* @__PURE__ */ jsx4(
2296
+ /* @__PURE__ */ jsx7("div", { ref: containerRef, class: "annotator-canvas-wrap", children: !imageLoaded ? /* @__PURE__ */ jsx7("span", { class: "annotator-loading", children: strings["annotator.loading"] }) : /* @__PURE__ */ jsx7(
1338
2297
  "canvas",
1339
2298
  {
1340
2299
  ref: canvasRef,
@@ -1345,9 +2304,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1345
2304
  class: "annotator-canvas"
1346
2305
  }
1347
2306
  ) }),
1348
- /* @__PURE__ */ jsxs3("div", { class: "annotator-footer", children: [
1349
- /* @__PURE__ */ jsx4("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
1350
- /* @__PURE__ */ jsx4(
2307
+ /* @__PURE__ */ jsxs6("div", { class: "annotator-footer", children: [
2308
+ /* @__PURE__ */ jsx7("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
2309
+ /* @__PURE__ */ jsx7(
1351
2310
  "button",
1352
2311
  {
1353
2312
  type: "button",
@@ -1363,55 +2322,32 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1363
2322
  );
1364
2323
  }
1365
2324
 
1366
- // src/widget/screenshot-utils.ts
1367
- var ALLOWED_IMAGE_TYPES = [
1368
- "image/png",
1369
- "image/jpeg",
1370
- "image/webp"
1371
- ];
1372
- var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
1373
- function validateScreenshotFile(file) {
1374
- if (!ALLOWED_IMAGE_TYPES.includes(
1375
- file.type
1376
- )) {
1377
- return { kind: "type" };
1378
- }
1379
- if (file.size > MAX_SCREENSHOT_BYTES) {
1380
- return { kind: "size", maxMb: MAX_SCREENSHOT_BYTES / (1024 * 1024) };
1381
- }
1382
- return null;
1383
- }
1384
- function truncateUrl(url, maxLength = 80) {
1385
- if (url.length <= maxLength) return url;
1386
- const keepStart = Math.floor((maxLength - 1) / 2);
1387
- const keepEnd = maxLength - 1 - keepStart;
1388
- return `${url.slice(0, keepStart)}\u2026${url.slice(url.length - keepEnd)}`;
1389
- }
1390
-
1391
2325
  // src/widget/Form.tsx
1392
- import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1393
- var TYPES = ["bug", "feature", "question", "praise", "typo"];
1394
- var SEVERITIES = ["blocker", "high", "medium", "low"];
2326
+ import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
2327
+ var TYPES2 = ["bug", "feature", "question", "praise", "typo"];
2328
+ var SEVERITIES2 = ["blocker", "high", "medium", "low"];
1395
2329
  function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1396
- const [description, setDescription] = useState3("");
1397
- const [feedbackType, setFeedbackType] = useState3("bug");
1398
- const [severity, setSeverity] = useState3("medium");
1399
- const [localError, setLocalError] = useState3("");
1400
- const [screenshotBlob, setScreenshotBlob] = useState3(null);
1401
- const [screenshotPreview, setScreenshotPreview] = useState3(null);
1402
- const [isDragOver, setIsDragOver] = useState3(false);
1403
- const [annotatorOpen, setAnnotatorOpen] = useState3(false);
1404
- const fileInputRef = useRef3(null);
1405
- const dropZoneRef = useRef3(null);
2330
+ const [description, setDescription] = useState5("");
2331
+ const [feedbackType, setFeedbackType] = useState5("bug");
2332
+ const [severity, setSeverity] = useState5("medium");
2333
+ const [localError, setLocalError] = useState5("");
2334
+ const [screenshotBlob, setScreenshotBlob] = useState5(null);
2335
+ const [screenshotPreview, setScreenshotPreview] = useState5(null);
2336
+ const [screenshotMethod, setScreenshotMethod] = useState5("manual");
2337
+ const [isDragOver, setIsDragOver] = useState5(false);
2338
+ const [annotatorOpen, setAnnotatorOpen] = useState5(false);
2339
+ const [capturing, setCapturing] = useState5(false);
2340
+ const fileInputRef = useRef4(null);
2341
+ const dropZoneRef = useRef4(null);
1406
2342
  const submitting = status === "submitting";
1407
2343
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
1408
2344
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
1409
- useEffect3(() => {
2345
+ useEffect5(() => {
1410
2346
  return () => {
1411
2347
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1412
2348
  };
1413
2349
  }, [screenshotPreview]);
1414
- const acceptFile = (file) => {
2350
+ const acceptFile = (file, method = "manual") => {
1415
2351
  setLocalError("");
1416
2352
  if (file instanceof File) {
1417
2353
  const err = validateScreenshotFile(file);
@@ -1425,14 +2361,41 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1425
2361
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1426
2362
  setScreenshotBlob(file);
1427
2363
  setScreenshotPreview(URL.createObjectURL(file));
2364
+ setScreenshotMethod(method);
1428
2365
  };
1429
2366
  const clearScreenshot = () => {
1430
2367
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1431
2368
  setScreenshotPreview(null);
1432
2369
  setScreenshotBlob(null);
2370
+ setScreenshotMethod("manual");
1433
2371
  setLocalError("");
1434
2372
  if (fileInputRef.current) fileInputRef.current.value = "";
1435
2373
  };
2374
+ const handleCapturePage = async () => {
2375
+ if (capturing) return;
2376
+ setLocalError("");
2377
+ setCapturing(true);
2378
+ try {
2379
+ const blob = await captureWithDisplayMedia();
2380
+ if (!blob) {
2381
+ if (typeof navigator === "undefined" || !navigator.mediaDevices?.getDisplayMedia) {
2382
+ setLocalError(strings["form.screenshot.capture_error"]);
2383
+ }
2384
+ return;
2385
+ }
2386
+ if (blob.size > 10 * 1024 * 1024) {
2387
+ setLocalError(
2388
+ strings["form.screenshot.error_size"].replace("{max}", "10")
2389
+ );
2390
+ return;
2391
+ }
2392
+ acceptFile(blob, "display_media");
2393
+ } catch {
2394
+ setLocalError(strings["form.screenshot.capture_error"]);
2395
+ } finally {
2396
+ setCapturing(false);
2397
+ }
2398
+ };
1436
2399
  const handleFileInputChange = (e) => {
1437
2400
  const file = e.target.files?.[0];
1438
2401
  if (file) acceptFile(file);
@@ -1454,7 +2417,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1454
2417
  const file = e.dataTransfer?.files?.[0];
1455
2418
  if (file) acceptFile(file);
1456
2419
  };
1457
- useEffect3(() => {
2420
+ useEffect5(() => {
1458
2421
  const zone = dropZoneRef.current;
1459
2422
  if (!zone) return;
1460
2423
  const onPaste = (e) => {
@@ -1492,14 +2455,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1492
2455
  feedback_type: feedbackType,
1493
2456
  severity
1494
2457
  };
1495
- if (screenshotBlob) values.screenshot = screenshotBlob;
2458
+ if (screenshotBlob) {
2459
+ values.screenshot = screenshotBlob;
2460
+ values.capture_method = screenshotMethod;
2461
+ }
1496
2462
  onSubmit(values);
1497
2463
  };
1498
- return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, children: [
1499
- /* @__PURE__ */ jsx5("h2", { children: strings["form.title"] }),
1500
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1501
- /* @__PURE__ */ jsx5("label", { for: "mfb-desc", children: strings["form.description.label"] }),
1502
- /* @__PURE__ */ jsx5(
2464
+ return /* @__PURE__ */ jsxs7("form", { onSubmit: handleSubmit, children: [
2465
+ /* @__PURE__ */ jsx8("h2", { children: strings["form.title"] }),
2466
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2467
+ /* @__PURE__ */ jsx8("label", { for: "mfb-desc", children: strings["form.description.label"] }),
2468
+ /* @__PURE__ */ jsx8(
1503
2469
  "textarea",
1504
2470
  {
1505
2471
  id: "mfb-desc",
@@ -1509,35 +2475,35 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1509
2475
  }
1510
2476
  )
1511
2477
  ] }),
1512
- /* @__PURE__ */ jsxs4("div", { class: "row", children: [
1513
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1514
- /* @__PURE__ */ jsx5("label", { for: "mfb-type", children: strings["form.type.label"] }),
1515
- /* @__PURE__ */ jsx5(
2478
+ /* @__PURE__ */ jsxs7("div", { class: "row", children: [
2479
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2480
+ /* @__PURE__ */ jsx8("label", { for: "mfb-type", children: strings["form.type.label"] }),
2481
+ /* @__PURE__ */ jsx8(
1516
2482
  "select",
1517
2483
  {
1518
2484
  id: "mfb-type",
1519
2485
  value: feedbackType,
1520
2486
  onChange: (e) => setFeedbackType(e.target.value),
1521
- children: TYPES.map((t) => /* @__PURE__ */ jsx5("option", { value: t, children: strings[`type.${t}`] }))
2487
+ children: TYPES2.map((t) => /* @__PURE__ */ jsx8("option", { value: t, children: strings[`type.${t}`] }))
1522
2488
  }
1523
2489
  )
1524
2490
  ] }),
1525
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1526
- /* @__PURE__ */ jsx5("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
1527
- /* @__PURE__ */ jsx5(
2491
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2492
+ /* @__PURE__ */ jsx8("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
2493
+ /* @__PURE__ */ jsx8(
1528
2494
  "select",
1529
2495
  {
1530
2496
  id: "mfb-sev",
1531
2497
  value: severity,
1532
2498
  onChange: (e) => setSeverity(e.target.value),
1533
- children: SEVERITIES.map((s) => /* @__PURE__ */ jsx5("option", { value: s, children: strings[`severity.${s}`] }))
2499
+ children: SEVERITIES2.map((s) => /* @__PURE__ */ jsx8("option", { value: s, children: strings[`severity.${s}`] }))
1534
2500
  }
1535
2501
  )
1536
2502
  ] })
1537
2503
  ] }),
1538
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1539
- /* @__PURE__ */ jsx5("label", { children: strings["form.screenshot.label"] }),
1540
- /* @__PURE__ */ jsx5(
2504
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2505
+ /* @__PURE__ */ jsx8("label", { children: strings["form.screenshot.label"] }),
2506
+ /* @__PURE__ */ jsx8(
1541
2507
  "input",
1542
2508
  {
1543
2509
  ref: fileInputRef,
@@ -1549,30 +2515,30 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1549
2515
  tabIndex: -1
1550
2516
  }
1551
2517
  ),
1552
- screenshotPreview ? /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview", children: [
1553
- /* @__PURE__ */ jsx5("img", { src: screenshotPreview, alt: "" }),
1554
- /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview-actions", children: [
1555
- /* @__PURE__ */ jsxs4(
2518
+ screenshotPreview ? /* @__PURE__ */ jsxs7("div", { class: "screenshot-preview", children: [
2519
+ /* @__PURE__ */ jsx8("img", { src: screenshotPreview, alt: "" }),
2520
+ /* @__PURE__ */ jsxs7("div", { class: "screenshot-preview-actions", children: [
2521
+ /* @__PURE__ */ jsxs7(
1556
2522
  "button",
1557
2523
  {
1558
2524
  type: "button",
1559
2525
  class: "btn btn--primary screenshot-annotate",
1560
2526
  onClick: () => setAnnotatorOpen(true),
1561
2527
  children: [
1562
- /* @__PURE__ */ jsxs4("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: [
1563
- /* @__PURE__ */ jsx5("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
1564
- /* @__PURE__ */ jsx5("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
2528
+ /* @__PURE__ */ jsxs7("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: [
2529
+ /* @__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" }),
2530
+ /* @__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" })
1565
2531
  ] }),
1566
2532
  strings["form.screenshot.annotate"]
1567
2533
  ]
1568
2534
  }
1569
2535
  ),
1570
- /* @__PURE__ */ jsxs4("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
1571
- /* @__PURE__ */ jsx5("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__ */ jsx5("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" }) }),
2536
+ /* @__PURE__ */ jsxs7("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
2537
+ /* @__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" }) }),
1572
2538
  strings["form.screenshot.remove"]
1573
2539
  ] })
1574
2540
  ] })
1575
- ] }) : /* @__PURE__ */ jsxs4(
2541
+ ] }) : /* @__PURE__ */ jsxs7(
1576
2542
  "div",
1577
2543
  {
1578
2544
  ref: dropZoneRef,
@@ -1591,33 +2557,69 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1591
2557
  onDragLeave: handleDragLeave,
1592
2558
  onDrop: handleDrop,
1593
2559
  children: [
1594
- /* @__PURE__ */ jsxs4("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: [
1595
- /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1596
- /* @__PURE__ */ jsx5("polyline", { points: "17 8 12 3 7 8" }),
1597
- /* @__PURE__ */ jsx5("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
2560
+ /* @__PURE__ */ jsxs7("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: [
2561
+ /* @__PURE__ */ jsx8("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
2562
+ /* @__PURE__ */ jsx8("polyline", { points: "17 8 12 3 7 8" }),
2563
+ /* @__PURE__ */ jsx8("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
1598
2564
  ] }),
1599
- /* @__PURE__ */ jsxs4("div", { class: "screenshot-cta", children: [
1600
- /* @__PURE__ */ jsx5("strong", { children: strings["form.screenshot.cta_click"] }),
2565
+ /* @__PURE__ */ jsxs7("div", { class: "screenshot-cta", children: [
2566
+ /* @__PURE__ */ jsx8("strong", { children: strings["form.screenshot.cta_click"] }),
1601
2567
  ", ",
1602
2568
  strings["form.screenshot.cta_rest"]
1603
2569
  ] }),
1604
- /* @__PURE__ */ jsx5("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
2570
+ /* @__PURE__ */ jsx8("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
1605
2571
  ]
1606
2572
  }
1607
- )
2573
+ ),
2574
+ !screenshotPreview && /* @__PURE__ */ jsxs7("div", { class: "screenshot-alt", children: [
2575
+ /* @__PURE__ */ jsx8("span", { class: "screenshot-or", children: strings["form.screenshot.or"] }),
2576
+ /* @__PURE__ */ jsxs7(
2577
+ "button",
2578
+ {
2579
+ type: "button",
2580
+ class: "btn btn--ghost screenshot-capture-page",
2581
+ onClick: handleCapturePage,
2582
+ disabled: capturing || submitting,
2583
+ "aria-label": strings["form.screenshot.capture_page"],
2584
+ children: [
2585
+ /* @__PURE__ */ jsxs7(
2586
+ "svg",
2587
+ {
2588
+ width: "14",
2589
+ height: "14",
2590
+ viewBox: "0 0 24 24",
2591
+ fill: "none",
2592
+ stroke: "currentColor",
2593
+ "stroke-width": "2",
2594
+ "stroke-linecap": "round",
2595
+ "stroke-linejoin": "round",
2596
+ "aria-hidden": "true",
2597
+ children: [
2598
+ /* @__PURE__ */ jsx8("rect", { x: "2", y: "3", width: "20", height: "14", rx: "2", ry: "2" }),
2599
+ /* @__PURE__ */ jsx8("line", { x1: "8", y1: "21", x2: "16", y2: "21" }),
2600
+ /* @__PURE__ */ jsx8("line", { x1: "12", y1: "17", x2: "12", y2: "21" })
2601
+ ]
2602
+ }
2603
+ ),
2604
+ capturing ? strings["form.submitting"] : strings["form.screenshot.capture_page"]
2605
+ ]
2606
+ }
2607
+ ),
2608
+ /* @__PURE__ */ jsx8("span", { class: "screenshot-capture-hint", children: strings["form.screenshot.capture_page_hint"] })
2609
+ ] })
1608
2610
  ] }),
1609
- pageUrl && /* @__PURE__ */ jsxs4("div", { class: "page-context", title: pageUrl, children: [
1610
- /* @__PURE__ */ jsx5("span", { class: "page-context-label", children: strings["form.context.label"] }),
1611
- /* @__PURE__ */ jsx5("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
2611
+ pageUrl && /* @__PURE__ */ jsxs7("div", { class: "page-context", title: pageUrl, children: [
2612
+ /* @__PURE__ */ jsx8("span", { class: "page-context-label", children: strings["form.context.label"] }),
2613
+ /* @__PURE__ */ jsx8("span", { class: "page-context-url", children: truncateUrl(pageUrl, 90) })
1612
2614
  ] }),
1613
- localError && /* @__PURE__ */ jsx5("div", { class: "error", children: localError }),
1614
- status === "error" && errorMessage && /* @__PURE__ */ jsx5("div", { class: "error", children: errorMessage }),
1615
- status === "success" && /* @__PURE__ */ jsx5("div", { class: "success", children: strings["form.success"] }),
1616
- /* @__PURE__ */ jsxs4("div", { class: "actions", children: [
1617
- /* @__PURE__ */ jsx5("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
1618
- /* @__PURE__ */ jsx5("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
2615
+ localError && /* @__PURE__ */ jsx8("div", { class: "error", children: localError }),
2616
+ status === "error" && errorMessage && /* @__PURE__ */ jsx8("div", { class: "error", children: errorMessage }),
2617
+ status === "success" && /* @__PURE__ */ jsx8("div", { class: "success", children: strings["form.success"] }),
2618
+ /* @__PURE__ */ jsxs7("div", { class: "actions", children: [
2619
+ /* @__PURE__ */ jsx8("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
2620
+ /* @__PURE__ */ jsx8("button", { type: "submit", class: "btn btn--primary", disabled: submitting, children: submitLabel })
1619
2621
  ] }),
1620
- annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx5(
2622
+ annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx8(
1621
2623
  Annotator,
1622
2624
  {
1623
2625
  imageBlob: screenshotBlob,
@@ -1630,10 +2632,10 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1630
2632
  }
1631
2633
 
1632
2634
  // src/widget/MineList.tsx
1633
- import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "preact/hooks";
2635
+ import { useEffect as useEffect6, useRef as useRef5, useState as useState6 } from "preact/hooks";
1634
2636
 
1635
2637
  // src/widget/KpiStrip.tsx
1636
- import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
2638
+ import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1637
2639
  function computeKpiCounts(rows) {
1638
2640
  const counts = {
1639
2641
  new: 0,
@@ -1700,468 +2702,151 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
1700
2702
  value: resolutionRate === null ? "\u2014" : `${resolutionRate}%`
1701
2703
  }
1702
2704
  ];
1703
- return /* @__PURE__ */ jsx6("div", { class: "kpi-strip", role: "toolbar", children: cells.map((c) => {
2705
+ return /* @__PURE__ */ jsx9("div", { class: "kpi-strip", role: "toolbar", children: cells.map((c) => {
1704
2706
  const active = filter === c.key;
1705
2707
  const toggleTo = active ? "all" : c.key;
1706
- return /* @__PURE__ */ jsxs5(
2708
+ return /* @__PURE__ */ jsxs8(
1707
2709
  "button",
1708
2710
  {
1709
2711
  type: "button",
1710
2712
  class: `kpi-cell kpi-cell--${c.key}${active ? " is-active" : ""}`,
1711
2713
  onClick: () => onFilter(toggleTo),
1712
2714
  "aria-pressed": active,
1713
- children: [
1714
- /* @__PURE__ */ jsx6("span", { class: "kpi-value", children: c.value }),
1715
- /* @__PURE__ */ jsx6("span", { class: "kpi-label", children: c.label })
1716
- ]
1717
- }
1718
- );
1719
- }) });
1720
- }
1721
-
1722
- // src/widget/MineList.tsx
1723
- import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1724
- var POLL_MS2 = 3e4;
1725
- function MineList({ api, externalId, strings, onSelect }) {
1726
- const [rows, setRows] = useState4(null);
1727
- const [error, setError] = useState4(null);
1728
- const [refreshing, setRefreshing] = useState4(false);
1729
- const [filter, setFilter] = useState4("all");
1730
- const mountedRef = useRef4(true);
1731
- const fetchRows = async () => {
1732
- setRefreshing(true);
1733
- setError(null);
1734
- try {
1735
- const next = await api.listMine(externalId);
1736
- if (!mountedRef.current) return;
1737
- setRows(next);
1738
- } catch (err) {
1739
- if (!mountedRef.current) return;
1740
- setError(err instanceof Error ? err.message : strings["mine.error"]);
1741
- } finally {
1742
- if (mountedRef.current) setRefreshing(false);
1743
- }
1744
- };
1745
- useEffect4(() => {
1746
- mountedRef.current = true;
1747
- void fetchRows();
1748
- const timer = setInterval(() => {
1749
- void fetchRows();
1750
- }, POLL_MS2);
1751
- return () => {
1752
- mountedRef.current = false;
1753
- clearInterval(timer);
1754
- };
1755
- }, [externalId]);
1756
- const isEmpty = rows !== null && rows.length === 0;
1757
- const isLoading = rows === null && !error;
1758
- const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
1759
- const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
1760
- return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
1761
- /* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
1762
- /* @__PURE__ */ jsx7("h2", { children: strings["tab.mine"] }),
1763
- /* @__PURE__ */ jsx7(
1764
- "button",
1765
- {
1766
- type: "button",
1767
- class: "btn",
1768
- onClick: () => {
1769
- void fetchRows();
1770
- },
1771
- disabled: refreshing,
1772
- children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1773
- }
1774
- )
1775
- ] }),
1776
- rows && rows.length > 0 && /* @__PURE__ */ jsx7(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
1777
- isLoading && /* @__PURE__ */ jsx7("div", { class: "mine-loading", children: strings["mine.loading"] }),
1778
- error && /* @__PURE__ */ jsx7("div", { class: "error", children: error }),
1779
- isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
1780
- /* @__PURE__ */ jsx7("strong", { children: strings["mine.empty.title"] }),
1781
- /* @__PURE__ */ jsx7("p", { children: strings["mine.empty.body"] })
1782
- ] }),
1783
- visibleEmpty && /* @__PURE__ */ jsx7("div", { class: "mine-empty", children: /* @__PURE__ */ jsx7("p", { children: strings["mine.filter.empty"] }) }),
1784
- visibleRows && visibleRows.length > 0 && /* @__PURE__ */ jsx7("ul", { class: "mine-rows", children: visibleRows.map((row) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
1785
- ] });
1786
- }
1787
-
1788
- // src/widget/Modal.tsx
1789
- import { useEffect as useEffect5, useRef as useRef5 } from "preact/hooks";
1790
- import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1791
- function Modal({ onDismiss, children, closeLabel = "Close" }) {
1792
- const modalRef = useRef5(null);
1793
- const previouslyFocused = useRef5(null);
1794
- useEffect5(() => {
1795
- previouslyFocused.current = document.activeElement;
1796
- const onKey = (e) => {
1797
- if (e.key !== "Escape") return;
1798
- const root = modalRef.current?.getRootNode();
1799
- if (root instanceof ShadowRoot && root.querySelector(".annotator-backdrop")) {
1800
- return;
1801
- }
1802
- e.stopPropagation();
1803
- onDismiss();
1804
- };
1805
- window.addEventListener("keydown", onKey);
1806
- const first = modalRef.current?.querySelector(
1807
- "textarea, input, select, button"
1808
- );
1809
- first?.focus();
1810
- return () => {
1811
- window.removeEventListener("keydown", onKey);
1812
- const prev = previouslyFocused.current;
1813
- if (prev && typeof prev.focus === "function") prev.focus();
1814
- };
1815
- }, [onDismiss]);
1816
- return /* @__PURE__ */ jsx8(
1817
- "div",
1818
- {
1819
- class: "backdrop",
1820
- role: "presentation",
1821
- onClick: (e) => {
1822
- if (e.target === e.currentTarget) onDismiss();
1823
- },
1824
- children: /* @__PURE__ */ jsxs7("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1825
- /* @__PURE__ */ jsx8(
1826
- "button",
1827
- {
1828
- type: "button",
1829
- class: "modal-close",
1830
- "aria-label": closeLabel,
1831
- onClick: onDismiss,
1832
- children: "\xD7"
1833
- }
1834
- ),
1835
- children
1836
- ] })
1837
- }
1838
- );
1839
- }
1840
-
1841
- // src/widget/ReportDetailView.tsx
1842
- import { useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "preact/hooks";
1843
-
1844
- // src/widget/CommentBubble.tsx
1845
- import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1846
- function CommentBubble({ comment, strings }) {
1847
- const isMine = comment.is_mine;
1848
- const isAgent = !isMine && comment.author_source === "mcp";
1849
- const isSystem = !isMine && comment.author_source === "system";
1850
- const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1851
- const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1852
- const label = comment.author_label || strings[labelKey];
1853
- return /* @__PURE__ */ jsxs8("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1854
- !isMine && label && /* @__PURE__ */ jsx9("div", { class: `comment-author comment-author--${variant}`, children: label }),
1855
- /* @__PURE__ */ jsx9("div", { class: "comment-body", children: comment.body }),
1856
- /* @__PURE__ */ jsx9("div", { class: "comment-time", children: formatTime(comment.created_at) })
1857
- ] });
1858
- }
1859
- function formatTime(iso) {
1860
- try {
1861
- return new Date(iso).toLocaleString(void 0, {
1862
- dateStyle: "short",
1863
- timeStyle: "short"
1864
- });
1865
- } catch {
1866
- return iso;
1867
- }
1868
- }
1869
-
1870
- // src/widget/ReportDetailView.tsx
1871
- import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
1872
- var POLL_MS3 = 3e4;
1873
- function ReportDetailView({
1874
- api,
1875
- externalId,
1876
- reportId,
1877
- strings,
1878
- onBack
1879
- }) {
1880
- const [detail, setDetail] = useState5(null);
1881
- const [error, setError] = useState5(null);
1882
- const [composeBody, setComposeBody] = useState5("");
1883
- const [sending, setSending] = useState5(false);
1884
- const [closing, setClosing] = useState5(false);
1885
- const mountedRef = useRef6(true);
1886
- const fetchDetail = async () => {
1887
- try {
1888
- const next = await api.getReport(reportId, externalId);
1889
- if (!mountedRef.current) return;
1890
- setDetail(next);
1891
- setError(null);
1892
- } catch (err) {
1893
- if (!mountedRef.current) return;
1894
- setError(err instanceof Error ? err.message : "load_failed");
1895
- }
1896
- };
1897
- useEffect6(() => {
1898
- mountedRef.current = true;
1899
- void fetchDetail();
1900
- const timer = setInterval(() => {
1901
- void fetchDetail();
1902
- }, POLL_MS3);
1903
- return () => {
1904
- mountedRef.current = false;
1905
- clearInterval(timer);
1906
- };
1907
- }, [reportId, externalId]);
1908
- const handleSend = async () => {
1909
- if (!composeBody.trim() || sending) return;
1910
- setSending(true);
1911
- try {
1912
- const nonce = `${reportId}:${Date.now()}`;
1913
- const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
1914
- if (!mountedRef.current) return;
1915
- setComposeBody("");
1916
- setDetail(
1917
- (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
1918
- );
1919
- void fetchDetail();
1920
- } catch (err) {
1921
- if (!mountedRef.current) return;
1922
- setError(err instanceof Error ? err.message : "comment_failed");
1923
- } finally {
1924
- if (mountedRef.current) setSending(false);
1925
- }
1926
- };
1927
- const handleClose = async () => {
1928
- if (closing) return;
1929
- setClosing(true);
2715
+ children: [
2716
+ /* @__PURE__ */ jsx9("span", { class: "kpi-value", children: c.value }),
2717
+ /* @__PURE__ */ jsx9("span", { class: "kpi-label", children: c.label })
2718
+ ]
2719
+ }
2720
+ );
2721
+ }) });
2722
+ }
2723
+
2724
+ // src/widget/MineList.tsx
2725
+ import { jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
2726
+ var POLL_MS4 = 3e4;
2727
+ function MineList({ api, externalId, strings, onSelect }) {
2728
+ const [rows, setRows] = useState6(null);
2729
+ const [error, setError] = useState6(null);
2730
+ const [refreshing, setRefreshing] = useState6(false);
2731
+ const [filter, setFilter] = useState6("all");
2732
+ const mountedRef = useRef5(true);
2733
+ const fetchRows = async () => {
2734
+ setRefreshing(true);
2735
+ setError(null);
1930
2736
  try {
1931
- const next = await api.closeAsResolved(reportId, externalId);
2737
+ const next = await api.listMine(externalId);
1932
2738
  if (!mountedRef.current) return;
1933
- setDetail(next);
2739
+ setRows(next);
1934
2740
  } catch (err) {
1935
2741
  if (!mountedRef.current) return;
1936
- setError(err instanceof Error ? err.message : "close_failed");
2742
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
1937
2743
  } finally {
1938
- if (mountedRef.current) setClosing(false);
2744
+ if (mountedRef.current) setRefreshing(false);
1939
2745
  }
1940
2746
  };
1941
- if (!detail && !error) {
1942
- return /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] });
1943
- }
1944
- if (!detail) {
1945
- return /* @__PURE__ */ jsx10("div", { class: "error", role: "alert", children: error });
1946
- }
1947
- const showCloseCta = detail.status === "awaiting_validation";
1948
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail", children: [
1949
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-header", children: [
1950
- /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: onBack, children: [
1951
- "\u2190 ",
1952
- strings["detail.back"]
1953
- ] }),
1954
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
1955
- ] }),
1956
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-body", children: [
1957
- /* @__PURE__ */ jsx10(ContextBlock, { detail, strings }),
1958
- /* @__PURE__ */ jsx10("p", { class: "report-detail-description", children: detail.description }),
1959
- detail.screenshot_url && /* @__PURE__ */ jsx10(
1960
- "a",
2747
+ useEffect6(() => {
2748
+ mountedRef.current = true;
2749
+ void fetchRows();
2750
+ const timer = setInterval(() => {
2751
+ void fetchRows();
2752
+ }, POLL_MS4);
2753
+ return () => {
2754
+ mountedRef.current = false;
2755
+ clearInterval(timer);
2756
+ };
2757
+ }, [externalId]);
2758
+ const isEmpty = rows !== null && rows.length === 0;
2759
+ const isLoading = rows === null && !error;
2760
+ const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
2761
+ const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
2762
+ return /* @__PURE__ */ jsxs9("div", { class: "mine-list", children: [
2763
+ /* @__PURE__ */ jsxs9("div", { class: "mine-list-header", children: [
2764
+ /* @__PURE__ */ jsx10("h2", { children: strings["tab.mine"] }),
2765
+ /* @__PURE__ */ jsx10(
2766
+ "button",
1961
2767
  {
1962
- class: "report-detail-screenshot",
1963
- href: detail.screenshot_url,
1964
- target: "_blank",
1965
- rel: "noopener noreferrer",
1966
- children: /* @__PURE__ */ jsx10("img", { src: detail.screenshot_url, alt: "", loading: "lazy" })
2768
+ type: "button",
2769
+ class: "btn",
2770
+ onClick: () => {
2771
+ void fetchRows();
2772
+ },
2773
+ disabled: refreshing,
2774
+ children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1967
2775
  }
1968
- ),
1969
- /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
1970
- detail.comments.length === 0 ? /* @__PURE__ */ jsx10("p", { class: "report-detail-empty", children: strings["detail.no_replies"] }) : /* @__PURE__ */ jsx10("ul", { class: "report-comments", children: detail.comments.map((c) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(CommentBubble, { comment: c, strings }) })) }),
1971
- detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx10(StatusHistorySection, { rows: detail.status_history, strings }),
1972
- detail.technical_context && /* @__PURE__ */ jsx10(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
1973
- /* @__PURE__ */ jsxs9("div", { class: "report-compose", children: [
1974
- /* @__PURE__ */ jsx10(
1975
- "textarea",
1976
- {
1977
- value: composeBody,
1978
- placeholder: strings["detail.compose_placeholder"],
1979
- onInput: (e) => setComposeBody(e.target.value),
1980
- disabled: sending
1981
- }
1982
- ),
1983
- /* @__PURE__ */ jsxs9("div", { class: "report-compose-actions", children: [
1984
- showCloseCta && /* @__PURE__ */ jsx10(
1985
- "button",
1986
- {
1987
- type: "button",
1988
- class: "btn",
1989
- onClick: handleClose,
1990
- disabled: closing,
1991
- children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
1992
- }
1993
- ),
1994
- /* @__PURE__ */ jsx10(
1995
- "button",
1996
- {
1997
- type: "button",
1998
- class: "btn btn--primary",
1999
- onClick: handleSend,
2000
- disabled: !composeBody.trim() || sending,
2001
- children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
2002
- }
2003
- )
2004
- ] })
2005
- ] }),
2006
- error && /* @__PURE__ */ jsx10("div", { class: "error", children: error })
2007
- ] })
2008
- ] });
2009
- }
2010
- function appendComment(current, next) {
2011
- if (current.some((c) => c.id === next.id)) return current;
2012
- return [...current, next];
2013
- }
2014
- function ContextBlock({ detail, strings }) {
2015
- const captureKey = `detail.context.capture.${detail.capture_method}`;
2016
- const captureLabel = strings[captureKey] ?? detail.capture_method;
2017
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail-context", children: [
2018
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-pills", children: [
2019
- /* @__PURE__ */ jsx10("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
2020
- /* @__PURE__ */ jsx10("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
2021
- /* @__PURE__ */ jsx10("span", { class: "pill pill-capture", children: captureLabel })
2776
+ )
2022
2777
  ] }),
2023
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", children: [
2024
- /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
2025
- /* @__PURE__ */ jsx10("span", { children: formatSubmittedAt(detail.created_at) })
2778
+ rows && rows.length > 0 && /* @__PURE__ */ jsx10(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
2779
+ isLoading && /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] }),
2780
+ error && /* @__PURE__ */ jsx10("div", { class: "error", children: error }),
2781
+ isEmpty && /* @__PURE__ */ jsxs9("div", { class: "mine-empty", children: [
2782
+ /* @__PURE__ */ jsx10("strong", { children: strings["mine.empty.title"] }),
2783
+ /* @__PURE__ */ jsx10("p", { children: strings["mine.empty.body"] })
2026
2784
  ] }),
2027
- detail.page_url && /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", title: detail.page_url, children: [
2028
- /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
2029
- /* @__PURE__ */ jsx10(
2030
- "a",
2785
+ visibleEmpty && /* @__PURE__ */ jsx10("div", { class: "mine-empty", children: /* @__PURE__ */ jsx10("p", { children: strings["mine.filter.empty"] }) }),
2786
+ visibleRows && visibleRows.length > 0 && /* @__PURE__ */ jsx10("ul", { class: "mine-rows", children: visibleRows.map((row) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
2787
+ ] });
2788
+ }
2789
+
2790
+ // src/widget/Modal.tsx
2791
+ import { useEffect as useEffect7, useRef as useRef6 } from "preact/hooks";
2792
+ import { jsx as jsx11, jsxs as jsxs10 } from "preact/jsx-runtime";
2793
+ function Modal({ onDismiss, children, closeLabel = "Close", expanded = false }) {
2794
+ const modalRef = useRef6(null);
2795
+ const previouslyFocused = useRef6(null);
2796
+ useEffect7(() => {
2797
+ previouslyFocused.current = document.activeElement;
2798
+ const onKey = (e) => {
2799
+ if (e.key !== "Escape") return;
2800
+ const root = modalRef.current?.getRootNode();
2801
+ if (root instanceof ShadowRoot && root.querySelector(".annotator-backdrop")) {
2802
+ return;
2803
+ }
2804
+ e.stopPropagation();
2805
+ onDismiss();
2806
+ };
2807
+ window.addEventListener("keydown", onKey);
2808
+ const first = modalRef.current?.querySelector(
2809
+ "textarea, input, select, button"
2810
+ );
2811
+ first?.focus();
2812
+ return () => {
2813
+ window.removeEventListener("keydown", onKey);
2814
+ const prev = previouslyFocused.current;
2815
+ if (prev && typeof prev.focus === "function") prev.focus();
2816
+ };
2817
+ }, [onDismiss]);
2818
+ return /* @__PURE__ */ jsx11(
2819
+ "div",
2820
+ {
2821
+ class: `backdrop ${expanded ? "is-expanded" : ""}`,
2822
+ role: "presentation",
2823
+ onClick: (e) => {
2824
+ if (e.target === e.currentTarget) onDismiss();
2825
+ },
2826
+ children: /* @__PURE__ */ jsxs10(
2827
+ "div",
2031
2828
  {
2032
- class: "report-detail-context-url",
2033
- href: detail.page_url,
2034
- target: "_blank",
2035
- rel: "noopener noreferrer",
2036
- children: truncateUrl(detail.page_url, 64)
2829
+ ref: modalRef,
2830
+ class: `modal ${expanded ? "is-expanded" : ""}`,
2831
+ role: "dialog",
2832
+ "aria-modal": "true",
2833
+ children: [
2834
+ /* @__PURE__ */ jsx11(
2835
+ "button",
2836
+ {
2837
+ type: "button",
2838
+ class: "modal-close",
2839
+ "aria-label": closeLabel,
2840
+ onClick: onDismiss,
2841
+ children: "\xD7"
2842
+ }
2843
+ ),
2844
+ children
2845
+ ]
2037
2846
  }
2038
2847
  )
2039
- ] })
2040
- ] });
2041
- }
2042
- function formatSubmittedAt(iso) {
2043
- try {
2044
- return new Date(iso).toLocaleString(void 0, {
2045
- dateStyle: "medium",
2046
- timeStyle: "short"
2047
- });
2048
- } catch {
2049
- return iso;
2050
- }
2051
- }
2052
- var TECH_CONSOLE_LIMIT = 20;
2053
- var TECH_NETWORK_LIMIT = 15;
2054
- function TechnicalContextDrawer({ ctx, strings }) {
2055
- const errors = ctx.errors ?? [];
2056
- const consoleLogs = (ctx.consoleLogs ?? []).slice(-TECH_CONSOLE_LIMIT);
2057
- const network = (ctx.networkRequests ?? []).slice(-TECH_NETWORK_LIMIT);
2058
- const device = ctx.device;
2059
- const hasAny = !!device || errors.length > 0 || consoleLogs.length > 0 || network.length > 0;
2060
- if (!hasAny) return null;
2061
- const errorCount = errors.length;
2062
- 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"];
2063
- return /* @__PURE__ */ jsxs9("details", { class: "report-detail-tech", children: [
2064
- /* @__PURE__ */ jsx10("summary", { children: summary }),
2065
- /* @__PURE__ */ jsxs9("div", { class: "tech-body", children: [
2066
- device && /* @__PURE__ */ jsx10(DeviceSection, { device, strings }),
2067
- errors.length > 0 && /* @__PURE__ */ jsx10(ErrorsSection, { errors, strings }),
2068
- consoleLogs.length > 0 && /* @__PURE__ */ jsx10(ConsoleSection, { logs: consoleLogs, strings }),
2069
- network.length > 0 && /* @__PURE__ */ jsx10(NetworkSection, { rows: network, strings })
2070
- ] })
2071
- ] });
2072
- }
2073
- function DeviceSection({
2074
- device,
2075
- strings
2076
- }) {
2077
- const parts = [];
2078
- if (device?.viewport) {
2079
- const dpr = device.viewport.dpr ? ` @${device.viewport.dpr}x` : "";
2080
- parts.push({
2081
- label: strings["detail.tech.device.viewport"],
2082
- value: `${device.viewport.w}\xD7${device.viewport.h}${dpr}`
2083
- });
2084
- }
2085
- if (device?.platform) parts.push({ label: strings["detail.tech.device.platform"], value: device.platform });
2086
- if (device?.language) parts.push({ label: strings["detail.tech.device.language"], value: device.language });
2087
- if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
2088
- if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
2089
- if (parts.length === 0) return null;
2090
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2091
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.device"] }),
2092
- /* @__PURE__ */ jsx10("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs9(Fragment, { children: [
2093
- /* @__PURE__ */ jsx10("dt", { children: p.label }),
2094
- /* @__PURE__ */ jsx10("dd", { children: p.value })
2095
- ] })) })
2096
- ] });
2097
- }
2098
- function ErrorsSection({
2099
- errors,
2100
- strings
2101
- }) {
2102
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2103
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.errors"] }),
2104
- /* @__PURE__ */ jsx10("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs9("li", { children: [
2105
- /* @__PURE__ */ jsx10("div", { class: "tech-errors-msg", children: e.message }),
2106
- e.stack && /* @__PURE__ */ jsx10("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
2107
- ] })) })
2108
- ] });
2109
- }
2110
- function ConsoleSection({
2111
- logs,
2112
- strings
2113
- }) {
2114
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2115
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.console"] }),
2116
- /* @__PURE__ */ jsx10("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs9("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
2117
- /* @__PURE__ */ jsx10("span", { class: "tech-console-level", children: l.level }),
2118
- /* @__PURE__ */ jsx10("span", { class: "tech-console-msg", children: l.message })
2119
- ] })) })
2120
- ] });
2121
- }
2122
- function NetworkSection({
2123
- rows,
2124
- strings
2125
- }) {
2126
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2127
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.network"] }),
2128
- /* @__PURE__ */ jsx10("ul", { class: "tech-network", children: rows.map((n) => {
2129
- const failed = n.status >= 400 || n.status === 0;
2130
- return /* @__PURE__ */ jsxs9("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
2131
- /* @__PURE__ */ jsx10("span", { class: "tech-network-status", children: n.status || "\u2014" }),
2132
- /* @__PURE__ */ jsx10("span", { class: "tech-network-method", children: n.method }),
2133
- /* @__PURE__ */ jsx10("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
2134
- /* @__PURE__ */ jsxs9("span", { class: "tech-network-time", children: [
2135
- Math.round(n.durationMs),
2136
- "ms"
2137
- ] })
2138
- ] });
2139
- }) })
2140
- ] });
2141
- }
2142
- function truncateStack(stack) {
2143
- const lines = stack.split("\n");
2144
- if (lines.length <= 12) return stack;
2145
- return lines.slice(0, 12).join("\n") + "\n \u2026";
2146
- }
2147
- function StatusHistorySection({ rows, strings }) {
2148
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail-history", children: [
2149
- /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.history"] }),
2150
- /* @__PURE__ */ jsx10("ol", { class: "status-history", children: rows.map((r) => {
2151
- const from = strings[`status.${r.from_status}`] ?? r.from_status;
2152
- const to = strings[`status.${r.to_status}`] ?? r.to_status;
2153
- const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
2154
- return /* @__PURE__ */ jsxs9("li", { class: "status-history-row", children: [
2155
- /* @__PURE__ */ jsx10("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
2156
- /* @__PURE__ */ jsxs9("span", { class: "status-history-transition", children: [
2157
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
2158
- /* @__PURE__ */ jsx10("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
2159
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
2160
- ] }),
2161
- /* @__PURE__ */ jsx10("span", { class: `status-history-source status-history-source--${r.changed_by_source}`, children: strings[sourceKey] })
2162
- ] });
2163
- }) })
2164
- ] });
2848
+ }
2849
+ );
2165
2850
  }
2166
2851
 
2167
2852
  // src/widget/styles.ts
@@ -2467,6 +3152,19 @@ var WIDGET_STYLES = `
2467
3152
  border-color: color-mix(in srgb, var(--mfb-accent) 88%, black);
2468
3153
  }
2469
3154
 
3155
+ /* Subdued button \u2014 borrows accent text but transparent background. Used
3156
+ * for the "Capture this page" alt-action that sits below the dropzone,
3157
+ * where a full --primary would compete with the form's main submit. */
3158
+ .btn--ghost {
3159
+ background: transparent;
3160
+ color: var(--mfb-accent);
3161
+ border-color: color-mix(in srgb, var(--mfb-accent) 40%, transparent);
3162
+ }
3163
+ .btn--ghost:hover {
3164
+ background: color-mix(in srgb, var(--mfb-accent) 8%, transparent);
3165
+ border-color: var(--mfb-accent);
3166
+ }
3167
+
2470
3168
  .btn[disabled] { opacity: 0.6; cursor: not-allowed; }
2471
3169
 
2472
3170
  .error { color: #dc2626; font-size: 13px; }
@@ -2530,6 +3228,40 @@ var WIDGET_STYLES = `
2530
3228
  .screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
2531
3229
  .screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
2532
3230
 
3231
+ /* "or \u2014 Capture this page" row sits below the dropzone. The thin "or"
3232
+ * separator borrows the pattern from auth forms so the second option
3233
+ * reads as an alternative, not a follow-up step. v0.12. */
3234
+ .screenshot-alt {
3235
+ display: flex;
3236
+ align-items: center;
3237
+ gap: 12px;
3238
+ margin-top: 8px;
3239
+ flex-wrap: wrap;
3240
+ }
3241
+ .screenshot-or {
3242
+ position: relative;
3243
+ font-size: var(--mfb-text-xs);
3244
+ color: var(--mfb-text-muted);
3245
+ text-transform: uppercase;
3246
+ letter-spacing: 0.08em;
3247
+ padding: 0 4px;
3248
+ }
3249
+ .screenshot-capture-page {
3250
+ display: inline-flex;
3251
+ align-items: center;
3252
+ gap: 6px;
3253
+ }
3254
+ .screenshot-capture-page:disabled {
3255
+ opacity: 0.55;
3256
+ cursor: progress;
3257
+ }
3258
+ .screenshot-capture-hint {
3259
+ font-size: var(--mfb-text-xs);
3260
+ color: var(--mfb-text-muted);
3261
+ flex: 1 1 100%;
3262
+ margin-top: 4px;
3263
+ }
3264
+
2533
3265
  .screenshot-preview {
2534
3266
  position: relative;
2535
3267
  border: 1px solid var(--mfb-border);
@@ -2796,7 +3528,17 @@ var WIDGET_STYLES = `
2796
3528
  }
2797
3529
  .tab-button[aria-selected="true"] { font-weight: 600; }
2798
3530
 
2799
- .mine-list { display: flex; flex-direction: column; gap: var(--mfb-space-3); }
3531
+ .mine-list {
3532
+ display: flex;
3533
+ flex-direction: column;
3534
+ gap: var(--mfb-space-3);
3535
+ /* Matches .board-view's entrance so switching tabs feels like a real
3536
+ * page transition rather than a flicker. */
3537
+ animation: mfb-board-in 280ms ease-out 40ms both;
3538
+ }
3539
+ @media (prefers-reduced-motion: reduce) {
3540
+ .mine-list { animation: none; }
3541
+ }
2800
3542
  .mine-list-header {
2801
3543
  display: flex;
2802
3544
  align-items: center;
@@ -2836,9 +3578,19 @@ var WIDGET_STYLES = `
2836
3578
  flex-direction: column;
2837
3579
  gap: 4px;
2838
3580
  width: 100%;
2839
- transition: border-color 120ms ease, background 120ms ease;
3581
+ transition:
3582
+ border-color 140ms ease,
3583
+ background 140ms ease,
3584
+ transform 100ms ease,
3585
+ box-shadow 140ms ease;
3586
+ }
3587
+ .mine-row:hover {
3588
+ border-color: var(--mfb-border-strong);
3589
+ background: var(--mfb-bg);
3590
+ transform: translateY(-1px);
3591
+ box-shadow: var(--mfb-shadow);
2840
3592
  }
2841
- .mine-row:hover { border-color: var(--mfb-text-muted); }
3593
+ .mine-row:active { transform: scale(0.997); }
2842
3594
  .mine-row:focus-visible {
2843
3595
  outline: 2px solid var(--mfb-accent);
2844
3596
  outline-offset: 2px;
@@ -3206,7 +3958,7 @@ var WIDGET_STYLES = `
3206
3958
  background: var(--mfb-bg);
3207
3959
  border: 1px solid var(--mfb-border);
3208
3960
  border-radius: var(--mfb-radius);
3209
- padding: 8px 6px;
3961
+ padding: 10px 10px 10px 14px;
3210
3962
  cursor: pointer;
3211
3963
  display: flex;
3212
3964
  flex-direction: column;
@@ -3214,9 +3966,36 @@ var WIDGET_STYLES = `
3214
3966
  gap: 2px;
3215
3967
  font-family: inherit;
3216
3968
  color: inherit;
3217
- transition: background 120ms, border-color 120ms;
3969
+ position: relative;
3970
+ overflow: hidden;
3971
+ transition:
3972
+ background 140ms ease,
3973
+ border-color 140ms ease,
3974
+ transform 100ms ease,
3975
+ box-shadow 140ms ease;
3976
+ }
3977
+ .kpi-cell::before {
3978
+ /* Vertical tone accent \u2014 matches the Board KPI cards' visual rhythm so
3979
+ * the two surfaces feel like they belong to the same product. v0.12
3980
+ * polish pass. */
3981
+ content: '';
3982
+ position: absolute;
3983
+ top: 0;
3984
+ left: 0;
3985
+ width: 3px;
3986
+ height: 100%;
3987
+ background: var(--mfb-border);
3988
+ transition: background 140ms ease;
3989
+ }
3990
+ .kpi-cell--new::before { background: #3b82f6; }
3991
+ .kpi-cell--in_progress::before { background: #f59e0b; }
3992
+ .kpi-cell--awaiting_validation::before { background: #a855f7; }
3993
+ .kpi-cell--resolved::before { background: #10b981; }
3994
+ .kpi-cell:hover {
3995
+ background: var(--mfb-surface);
3996
+ transform: translateY(-1px);
3997
+ box-shadow: var(--mfb-shadow);
3218
3998
  }
3219
- .kpi-cell:hover { background: var(--mfb-surface); }
3220
3999
  .kpi-cell.is-active {
3221
4000
  border-color: var(--mfb-accent);
3222
4001
  background: rgba(59, 130, 246, 0.08);
@@ -3244,6 +4023,10 @@ var WIDGET_STYLES = `
3244
4023
  display: flex;
3245
4024
  flex-direction: column;
3246
4025
  gap: 12px;
4026
+ animation: mfb-board-in 280ms ease-out 40ms both;
4027
+ }
4028
+ @media (prefers-reduced-motion: reduce) {
4029
+ .changelog-groups { animation: none; }
3247
4030
  }
3248
4031
  .changelog-group {
3249
4032
  display: flex;
@@ -3275,10 +4058,437 @@ var WIDGET_STYLES = `
3275
4058
  font-variant-numeric: tabular-nums;
3276
4059
  font-weight: 600;
3277
4060
  }
4061
+
4062
+ /* ---- v0.12: Board tab + expanded modal ("transport" feel) ------------ */
4063
+
4064
+ /* When the Board tab is active, the modal grows toward near-fullscreen
4065
+ * on desktop and full-screen on mobile. The CSS transition gives the
4066
+ * sense of clicking through to a real page rather than switching tabs.
4067
+ * Width/height/max-* are animated together; padding too, since the
4068
+ * board's denser layout needs less of the modal's default padding. */
4069
+ .modal.is-expanded {
4070
+ width: min(1280px, calc(100vw - var(--mfb-space-5)));
4071
+ max-height: calc(100vh - var(--mfb-space-5));
4072
+ height: calc(100vh - var(--mfb-space-5));
4073
+ padding: var(--mfb-space-5);
4074
+ transition:
4075
+ width 320ms cubic-bezier(0.22, 1, 0.36, 1),
4076
+ height 320ms cubic-bezier(0.22, 1, 0.36, 1),
4077
+ max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
4078
+ padding 320ms cubic-bezier(0.22, 1, 0.36, 1);
4079
+ }
4080
+ @media (prefers-reduced-motion: reduce) {
4081
+ .modal.is-expanded { transition: none; }
4082
+ }
4083
+ .backdrop.is-expanded { /* hook for any backdrop-level overrides */ }
4084
+
4085
+ @media (max-width: 640px) {
4086
+ .modal.is-expanded {
4087
+ width: 100vw;
4088
+ height: calc(100vh - var(--mfb-space-5));
4089
+ border-radius: var(--mfb-radius-lg) var(--mfb-radius-lg) 0 0;
4090
+ }
4091
+ }
4092
+
4093
+ /* Decorative purple accent on the Board tab button so users discover
4094
+ * it's a different surface \u2014 subtle, not loud. */
4095
+ .tab-button--board.is-active {
4096
+ color: #6b21a8;
4097
+ box-shadow: inset 0 -2px 0 0 #a855f7;
4098
+ }
4099
+ .tab-button--board:not(.is-active):hover {
4100
+ color: #6b21a8;
4101
+ }
4102
+
4103
+ /* ---- Board layout ---- */
4104
+
4105
+ .board-view {
4106
+ display: flex;
4107
+ flex-direction: column;
4108
+ gap: var(--mfb-space-4);
4109
+ flex: 1 1 auto;
4110
+ min-height: 0; /* allow inner panes to scroll */
4111
+ animation: mfb-board-in 280ms ease-out 40ms both;
4112
+ }
4113
+ @keyframes mfb-board-in {
4114
+ from { opacity: 0; transform: translateY(8px); }
4115
+ to { opacity: 1; transform: translateY(0); }
4116
+ }
4117
+ @media (prefers-reduced-motion: reduce) {
4118
+ .board-view { animation: none; }
4119
+ }
4120
+
4121
+ .board-header {
4122
+ display: flex;
4123
+ flex-direction: column;
4124
+ gap: var(--mfb-space-4);
4125
+ }
4126
+ .board-header-title {
4127
+ display: flex;
4128
+ align-items: center;
4129
+ gap: 12px;
4130
+ }
4131
+ .board-header-emoji {
4132
+ font-size: 26px;
4133
+ line-height: 1;
4134
+ }
4135
+ .board-header-h {
4136
+ margin: 0;
4137
+ font-size: var(--mfb-text-2xl);
4138
+ font-weight: 600;
4139
+ letter-spacing: -0.01em;
4140
+ }
4141
+ .board-header-sub {
4142
+ margin: 2px 0 0 0;
4143
+ color: var(--mfb-text-muted);
4144
+ font-size: var(--mfb-text-sm);
4145
+ }
4146
+
4147
+ /* KPI strip \u2014 4 cards, equal width, subtle tone-tints by bucket. */
4148
+ .board-kpi-strip {
4149
+ display: grid;
4150
+ grid-template-columns: repeat(4, 1fr);
4151
+ gap: 12px;
4152
+ }
4153
+ .board-kpi-card {
4154
+ border: 1px solid var(--mfb-border);
4155
+ border-radius: var(--mfb-radius);
4156
+ padding: 14px 16px;
4157
+ background: var(--mfb-bg);
4158
+ display: flex;
4159
+ flex-direction: column;
4160
+ gap: 4px;
4161
+ position: relative;
4162
+ overflow: hidden;
4163
+ transition: transform 160ms ease, box-shadow 160ms ease;
4164
+ }
4165
+ .board-kpi-card::after {
4166
+ content: '';
4167
+ position: absolute;
4168
+ top: 0;
4169
+ left: 0;
4170
+ width: 3px;
4171
+ height: 100%;
4172
+ background: var(--mfb-border);
4173
+ transition: background 160ms ease;
4174
+ }
4175
+ .board-kpi-card:hover {
4176
+ transform: translateY(-1px);
4177
+ box-shadow: var(--mfb-shadow);
4178
+ }
4179
+ .board-kpi-card.tone-new::after { background: #3b82f6; }
4180
+ .board-kpi-card.tone-progress::after { background: #f59e0b; }
4181
+ .board-kpi-card.tone-validation::after { background: #a855f7; }
4182
+ .board-kpi-card.tone-rate::after { background: #10b981; }
4183
+ .board-kpi-value {
4184
+ font-size: 28px;
4185
+ font-weight: 700;
4186
+ line-height: 1;
4187
+ letter-spacing: -0.02em;
4188
+ color: var(--mfb-text);
4189
+ font-variant-numeric: tabular-nums;
4190
+ }
4191
+ .board-kpi-label {
4192
+ font-size: var(--mfb-text-xs);
4193
+ color: var(--mfb-text-muted);
4194
+ text-transform: uppercase;
4195
+ letter-spacing: 0.04em;
4196
+ font-weight: 500;
4197
+ }
4198
+ .board-kpi-strip.is-loading .board-kpi-value {
4199
+ opacity: 0.35;
4200
+ }
4201
+ @media (max-width: 720px) {
4202
+ .board-kpi-strip { grid-template-columns: repeat(2, 1fr); }
4203
+ }
4204
+
4205
+ /* Filter row */
4206
+ .board-filters {
4207
+ display: flex;
4208
+ align-items: center;
4209
+ gap: 8px;
4210
+ flex-wrap: wrap;
4211
+ }
4212
+ .board-filter-select,
4213
+ .board-filter-search {
4214
+ padding: 6px 10px;
4215
+ border: 1px solid var(--mfb-border);
4216
+ border-radius: var(--mfb-radius);
4217
+ background: var(--mfb-bg);
4218
+ color: var(--mfb-text);
4219
+ font: inherit;
4220
+ font-size: var(--mfb-text-sm);
4221
+ height: 32px;
4222
+ transition: border-color 120ms ease, box-shadow 120ms ease;
4223
+ }
4224
+ .board-filter-select:focus-visible,
4225
+ .board-filter-search:focus-visible {
4226
+ outline: none;
4227
+ border-color: var(--mfb-accent);
4228
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
4229
+ }
4230
+ .board-filter-search { min-width: 180px; flex: 1 1 220px; }
4231
+ .board-filter-toggle {
4232
+ display: inline-flex;
4233
+ align-items: center;
4234
+ gap: 6px;
4235
+ font-size: var(--mfb-text-sm);
4236
+ color: var(--mfb-text);
4237
+ cursor: pointer;
4238
+ user-select: none;
4239
+ }
4240
+ .board-filter-toggle input { margin: 0; }
4241
+ .board-filter-clear {
4242
+ background: transparent;
4243
+ border: 0;
4244
+ color: var(--mfb-text-muted);
4245
+ font: inherit;
4246
+ font-size: var(--mfb-text-sm);
4247
+ display: inline-flex;
4248
+ align-items: center;
4249
+ gap: 4px;
4250
+ cursor: pointer;
4251
+ padding: 4px 6px;
4252
+ border-radius: var(--mfb-radius);
4253
+ transition: background 120ms ease, color 120ms ease;
4254
+ }
4255
+ .board-filter-clear:hover {
4256
+ color: var(--mfb-text);
4257
+ background: var(--mfb-surface);
4258
+ }
4259
+
4260
+ /* Master/detail layout \u2014 two-column at >=900px, stacked below. */
4261
+ .board-body {
4262
+ display: grid;
4263
+ grid-template-columns: minmax(280px, 360px) 1fr;
4264
+ gap: var(--mfb-space-4);
4265
+ flex: 1 1 auto;
4266
+ min-height: 0;
4267
+ }
4268
+ @media (max-width: 900px) {
4269
+ .board-body { grid-template-columns: 1fr; }
4270
+ .board-detail-wrap:not(.has-selection) { display: none; }
4271
+ }
4272
+
4273
+ .board-list-wrap {
4274
+ display: flex;
4275
+ flex-direction: column;
4276
+ gap: 6px;
4277
+ min-height: 0;
4278
+ overflow: hidden;
4279
+ }
4280
+ .board-list-count {
4281
+ font-size: var(--mfb-text-xs);
4282
+ color: var(--mfb-text-muted);
4283
+ padding: 0 4px;
4284
+ }
4285
+ .board-list {
4286
+ list-style: none;
4287
+ margin: 0;
4288
+ padding: 0;
4289
+ display: flex;
4290
+ flex-direction: column;
4291
+ gap: 8px;
4292
+ overflow-y: auto;
4293
+ flex: 1 1 auto;
4294
+ scrollbar-width: thin;
4295
+ scrollbar-gutter: stable;
4296
+ }
4297
+
4298
+ .board-row {
4299
+ display: flex;
4300
+ flex-direction: column;
4301
+ gap: 6px;
4302
+ width: 100%;
4303
+ text-align: left;
4304
+ padding: 12px 14px;
4305
+ border-radius: var(--mfb-radius);
4306
+ background: var(--mfb-bg);
4307
+ border: 1px solid var(--mfb-border);
4308
+ cursor: pointer;
4309
+ font: inherit;
4310
+ color: inherit;
4311
+ /* Stagger reveal on first paint. */
4312
+ animation: mfb-row-in 280ms ease-out both;
4313
+ transition: border-color 140ms ease, background 140ms ease, transform 80ms ease;
4314
+ }
4315
+ .board-row:hover {
4316
+ border-color: var(--mfb-border-strong);
4317
+ background: var(--mfb-surface);
4318
+ }
4319
+ .board-row:active { transform: scale(0.997); }
4320
+ .board-row.is-selected {
4321
+ border-color: var(--mfb-accent);
4322
+ background: color-mix(in srgb, var(--mfb-accent) 6%, var(--mfb-bg));
4323
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
4324
+ }
4325
+ @keyframes mfb-row-in {
4326
+ from { opacity: 0; transform: translateY(4px); }
4327
+ to { opacity: 1; transform: translateY(0); }
4328
+ }
4329
+ @media (prefers-reduced-motion: reduce) {
4330
+ .board-row { animation: none; }
4331
+ }
4332
+ /* Stagger first 6 rows so the list reveals top-down. */
4333
+ .board-row:nth-child(1) { animation-delay: 0ms; }
4334
+ .board-row:nth-child(2) { animation-delay: 40ms; }
4335
+ .board-row:nth-child(3) { animation-delay: 80ms; }
4336
+ .board-row:nth-child(4) { animation-delay: 120ms; }
4337
+ .board-row:nth-child(5) { animation-delay: 160ms; }
4338
+ .board-row:nth-child(6) { animation-delay: 200ms; }
4339
+
4340
+ .board-row-badges {
4341
+ display: flex;
4342
+ gap: 6px;
4343
+ align-items: center;
4344
+ flex-wrap: wrap;
4345
+ }
4346
+ .board-row-description {
4347
+ font-size: var(--mfb-text-sm);
4348
+ color: var(--mfb-text);
4349
+ line-height: 1.4;
4350
+ display: -webkit-box;
4351
+ -webkit-line-clamp: 2;
4352
+ -webkit-box-orient: vertical;
4353
+ overflow: hidden;
4354
+ }
4355
+ .board-row-meta {
4356
+ display: flex;
4357
+ align-items: center;
4358
+ gap: 10px;
4359
+ font-size: var(--mfb-text-xs);
4360
+ color: var(--mfb-text-muted);
4361
+ flex-wrap: wrap;
4362
+ }
4363
+ .board-row-author { font-weight: 500; color: var(--mfb-text); }
4364
+ .board-row-comments {
4365
+ display: inline-flex;
4366
+ align-items: center;
4367
+ gap: 3px;
4368
+ font-variant-numeric: tabular-nums;
4369
+ }
4370
+ .board-row-time { margin-left: auto; font-variant-numeric: tabular-nums; }
4371
+
4372
+ /* Status / severity badges shared with the detail panel. The tones
4373
+ * borrow PNR's badge palette \u2014 calm pastels that don't fight content. */
4374
+ .board-row-badges .badge {
4375
+ font-size: 11px;
4376
+ font-weight: 500;
4377
+ letter-spacing: 0.02em;
4378
+ padding: 2px 8px;
4379
+ border-radius: 999px;
4380
+ background: #f3f4f6;
4381
+ color: #374151;
4382
+ text-transform: lowercase;
4383
+ }
4384
+ .badge.status-new { background: #dbeafe; color: #1e40af; }
4385
+ .badge.status-in_progress { background: #fef3c7; color: #92400e; }
4386
+ .badge.status-awaiting_validation { background: #ede9fe; color: #6b21a8; }
4387
+ .badge.status-closed { background: #d1fae5; color: #065f46; }
4388
+ .badge.status-rejected { background: #fee2e2; color: #991b1b; }
4389
+ .badge.status-duplicate { background: #f3f4f6; color: #4b5563; }
4390
+ .badge.status-wontfix { background: #f3f4f6; color: #4b5563; }
4391
+ .badge.severity-blocker { background: #fee2e2; color: #991b1b; }
4392
+ .badge.severity-high { background: #fee2e2; color: #b91c1c; }
4393
+ .badge.severity-medium { background: #fef3c7; color: #92400e; }
4394
+ .badge.severity-low { background: #e0f2fe; color: #075985; }
4395
+
4396
+ /* Detail pane (within Board). Inherits from .report-detail.variant-board */
4397
+ .board-detail-wrap {
4398
+ border: 1px solid var(--mfb-border);
4399
+ border-radius: var(--mfb-radius);
4400
+ background: var(--mfb-bg);
4401
+ overflow-y: auto;
4402
+ min-height: 0;
4403
+ display: flex;
4404
+ flex-direction: column;
4405
+ }
4406
+ .board-detail-empty {
4407
+ flex: 1 1 auto;
4408
+ display: flex;
4409
+ flex-direction: column;
4410
+ align-items: center;
4411
+ justify-content: center;
4412
+ gap: 12px;
4413
+ color: var(--mfb-text-muted);
4414
+ padding: var(--mfb-space-5);
4415
+ text-align: center;
4416
+ font-size: var(--mfb-text-sm);
4417
+ }
4418
+ .board-detail-empty svg { opacity: 0.6; }
4419
+
4420
+ .report-detail.variant-board { padding: 16px 18px; }
4421
+ .report-detail-header--board {
4422
+ align-items: center;
4423
+ }
4424
+ .report-detail-back {
4425
+ display: inline-flex;
4426
+ align-items: center;
4427
+ gap: 4px;
4428
+ height: 28px;
4429
+ padding: 0 10px;
4430
+ }
4431
+
4432
+ /* Empty + loading + error states */
4433
+ .board-empty {
4434
+ display: flex;
4435
+ flex-direction: column;
4436
+ align-items: center;
4437
+ justify-content: center;
4438
+ gap: 8px;
4439
+ text-align: center;
4440
+ padding: var(--mfb-space-5);
4441
+ color: var(--mfb-text-muted);
4442
+ flex: 1 1 auto;
4443
+ }
4444
+ .board-empty h3 {
4445
+ margin: 0;
4446
+ font-size: var(--mfb-text-base);
4447
+ color: var(--mfb-text);
4448
+ }
4449
+ .board-empty p {
4450
+ margin: 0;
4451
+ font-size: var(--mfb-text-sm);
4452
+ }
4453
+ .board-error {
4454
+ padding: 12px 14px;
4455
+ border-radius: var(--mfb-radius);
4456
+ background: #fef2f2;
4457
+ color: #991b1b;
4458
+ font-size: var(--mfb-text-sm);
4459
+ }
4460
+
4461
+ /* Skeleton rows pulse subtly so the user knows something's coming. */
4462
+ .board-list-skeleton .skeleton-row {
4463
+ cursor: default;
4464
+ pointer-events: none;
4465
+ }
4466
+ .skeleton-line {
4467
+ height: 12px;
4468
+ background: linear-gradient(
4469
+ 90deg,
4470
+ var(--mfb-surface) 0%,
4471
+ color-mix(in srgb, var(--mfb-surface) 60%, var(--mfb-border)) 50%,
4472
+ var(--mfb-surface) 100%
4473
+ );
4474
+ background-size: 200% 100%;
4475
+ border-radius: 4px;
4476
+ animation: mfb-skeleton 1400ms ease-in-out infinite;
4477
+ }
4478
+ .skeleton-badges { width: 60%; height: 14px; }
4479
+ .skeleton-text { width: 90%; }
4480
+ .skeleton-text.short { width: 60%; }
4481
+ @keyframes mfb-skeleton {
4482
+ from { background-position: 100% 0; }
4483
+ to { background-position: -100% 0; }
4484
+ }
4485
+ @media (prefers-reduced-motion: reduce) {
4486
+ .skeleton-line { animation: none; }
4487
+ }
3278
4488
  `;
3279
4489
 
3280
4490
  // src/widget/mount.tsx
3281
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "preact/jsx-runtime";
4491
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "preact/jsx-runtime";
3282
4492
  function mountWidget(options) {
3283
4493
  const shadow = options.host.attachShadow({ mode: "open" });
3284
4494
  const style = document.createElement("style");
@@ -3325,22 +4535,23 @@ function mountWidget(options) {
3325
4535
  const externalId = options.getExternalId?.();
3326
4536
  const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
3327
4537
  const showMineTab = Boolean(options.api && externalId);
3328
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
3329
- fabVisible && /* @__PURE__ */ jsx11(
4538
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
4539
+ fabVisible && /* @__PURE__ */ jsx12(
3330
4540
  Fab,
3331
4541
  {
3332
4542
  label: options.strings["fab.label"],
3333
4543
  onClick: () => rerender({ ...currentState, open: true })
3334
4544
  }
3335
4545
  ),
3336
- state.open && /* @__PURE__ */ jsxs10(
4546
+ state.open && /* @__PURE__ */ jsxs11(
3337
4547
  Modal,
3338
4548
  {
3339
4549
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
3340
4550
  closeLabel: options.strings["form.close"],
4551
+ expanded: state.tab === "board",
3341
4552
  children: [
3342
- showMineTab && /* @__PURE__ */ jsxs10("div", { class: "tab-strip", role: "tablist", children: [
3343
- /* @__PURE__ */ jsx11(
4553
+ showMineTab && /* @__PURE__ */ jsxs11("div", { class: "tab-strip", role: "tablist", children: [
4554
+ /* @__PURE__ */ jsx12(
3344
4555
  "button",
3345
4556
  {
3346
4557
  type: "button",
@@ -3351,7 +4562,7 @@ function mountWidget(options) {
3351
4562
  children: options.strings["tab.send"]
3352
4563
  }
3353
4564
  ),
3354
- /* @__PURE__ */ jsx11(
4565
+ /* @__PURE__ */ jsx12(
3355
4566
  "button",
3356
4567
  {
3357
4568
  type: "button",
@@ -3362,7 +4573,7 @@ function mountWidget(options) {
3362
4573
  children: options.strings["tab.mine"]
3363
4574
  }
3364
4575
  ),
3365
- /* @__PURE__ */ jsx11(
4576
+ /* @__PURE__ */ jsx12(
3366
4577
  "button",
3367
4578
  {
3368
4579
  type: "button",
@@ -3372,9 +4583,20 @@ function mountWidget(options) {
3372
4583
  onClick: () => rerender(clearSelected({ ...currentState, tab: "changelog" })),
3373
4584
  children: options.strings["tab.changelog"]
3374
4585
  }
4586
+ ),
4587
+ /* @__PURE__ */ jsx12(
4588
+ "button",
4589
+ {
4590
+ type: "button",
4591
+ role: "tab",
4592
+ "aria-selected": state.tab === "board",
4593
+ class: `tab-button tab-button--board ${state.tab === "board" ? "is-active" : ""}`,
4594
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "board" })),
4595
+ children: options.strings["tab.board"]
4596
+ }
3375
4597
  )
3376
4598
  ] }),
3377
- state.tab === "send" && /* @__PURE__ */ jsx11(
4599
+ state.tab === "send" && /* @__PURE__ */ jsx12(
3378
4600
  Form,
3379
4601
  {
3380
4602
  strings: options.strings,
@@ -3384,7 +4606,7 @@ function mountWidget(options) {
3384
4606
  ...state.error !== void 0 && { errorMessage: state.error }
3385
4607
  }
3386
4608
  ),
3387
- state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4609
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3388
4610
  MineList,
3389
4611
  {
3390
4612
  api: options.api,
@@ -3393,7 +4615,7 @@ function mountWidget(options) {
3393
4615
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3394
4616
  }
3395
4617
  ),
3396
- (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx11(
4618
+ (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx12(
3397
4619
  ReportDetailView,
3398
4620
  {
3399
4621
  api: options.api,
@@ -3403,7 +4625,7 @@ function mountWidget(options) {
3403
4625
  onBack: () => rerender(clearSelected({ ...currentState }))
3404
4626
  }
3405
4627
  ),
3406
- state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4628
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3407
4629
  ChangelogList,
3408
4630
  {
3409
4631
  api: options.api,
@@ -3411,6 +4633,16 @@ function mountWidget(options) {
3411
4633
  strings: options.strings,
3412
4634
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3413
4635
  }
4636
+ ),
4637
+ state.tab === "board" && options.api && externalId && // BoardView owns its own master/detail navigation — no
4638
+ // selectedReportId routing through the modal-level state.
4639
+ /* @__PURE__ */ jsx12(
4640
+ BoardView,
4641
+ {
4642
+ api: options.api,
4643
+ externalId,
4644
+ strings: options.strings
4645
+ }
3414
4646
  )
3415
4647
  ]
3416
4648
  }
@@ -3464,15 +4696,13 @@ function createFeedback(config) {
3464
4696
  document.body.appendChild(host);
3465
4697
  }
3466
4698
  async function buildAndSubmit(values) {
3467
- const manualScreenshot = values.screenshot;
3468
- const screenshot = values.synthetic ? void 0 : manualScreenshot ?? await takeScreenshot(document.body, {
3469
- mask: [".mhosaic-feedback", "[data-mfb-mask]"]
3470
- });
4699
+ const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
3471
4700
  const technical_context = capture.snapshot();
3472
4701
  if (user) technical_context.user = user;
3473
4702
  if (metadata && Object.keys(metadata).length > 0) {
3474
4703
  technical_context.metadata = { ...metadata };
3475
4704
  }
4705
+ const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
3476
4706
  const payload = {
3477
4707
  description: values.description,
3478
4708
  feedback_type: values.feedback_type ?? "bug",
@@ -3480,13 +4710,13 @@ function createFeedback(config) {
3480
4710
  env,
3481
4711
  page_url: window.location.href,
3482
4712
  user_agent: navigator.userAgent,
3483
- capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
4713
+ capture_method: captureMethod,
3484
4714
  technical_context
3485
4715
  };
3486
- if ("0.10.0") {
3487
- payload.widget_version = "0.10.0";
4716
+ if ("0.12.0") {
4717
+ payload.widget_version = "0.12.0";
3488
4718
  }
3489
- if (screenshot) payload.screenshot = screenshot;
4719
+ if (manualScreenshot) payload.screenshot = manualScreenshot;
3490
4720
  if (values.synthetic) payload.synthetic = true;
3491
4721
  if (user?.id !== void 0 && user.id !== null && user.id !== "") {
3492
4722
  payload.user = {
@@ -3570,4 +4800,4 @@ function createFeedback(config) {
3570
4800
  export {
3571
4801
  createFeedback
3572
4802
  };
3573
- //# sourceMappingURL=chunk-XSAUJEJN.mjs.map
4803
+ //# sourceMappingURL=chunk-XSYC5TDL.mjs.map