@mhosaic/feedback 0.11.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.
@@ -134,7 +134,56 @@ function createApiClient(options) {
134
134
  }
135
135
  return response.json();
136
136
  }
137
- 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
+ };
138
187
  }
139
188
 
140
189
  // src/capture/urlSanitizer.ts
@@ -465,65 +514,6 @@ function installCapture(options = {}) {
465
514
  };
466
515
  }
467
516
 
468
- // src/screenshot/index.ts
469
- var DEFAULT_REDACTED_INPUT_SELECTORS = [
470
- 'input[type="password"]',
471
- 'input[type="tel"]',
472
- 'input[autocomplete*="cc-"]',
473
- 'input[autocomplete*="current-password"]',
474
- 'input[autocomplete*="new-password"]',
475
- 'input[autocomplete*="one-time-code"]',
476
- 'input[name*="cvv" i]',
477
- 'input[name*="ccv" i]',
478
- 'input[name*="cardnum" i]',
479
- 'input[name*="card-num" i]'
480
- ];
481
- var REDACTION_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
482
- function isMaskedElement(el) {
483
- return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
484
- }
485
- function prepareMaskMatcher(selectors) {
486
- const joined = selectors.join(",").trim();
487
- if (!joined) return isMaskedElement;
488
- return (el) => isMaskedElement(el) || el.matches(joined);
489
- }
490
- function redactSensitiveInputs(clonedDoc, extraSelectors) {
491
- const selectors = [
492
- ...DEFAULT_REDACTED_INPUT_SELECTORS,
493
- ...extraSelectors,
494
- "[data-mfb-mask]",
495
- ".mfb-mask"
496
- ].join(",");
497
- const elements = clonedDoc.querySelectorAll(selectors);
498
- elements.forEach((el) => {
499
- if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
500
- const len = Math.min(Math.max(el.value.length || 8, 4), 16);
501
- el.value = REDACTION_MASK.slice(0, len);
502
- el.setAttribute("value", el.value);
503
- } else if (el instanceof HTMLElement) {
504
- el.textContent = REDACTION_MASK;
505
- }
506
- });
507
- }
508
- async function takeScreenshot(target, options = {}) {
509
- if (typeof document === "undefined") return null;
510
- try {
511
- const html2canvas = (await import("html2canvas-pro")).default;
512
- const extraSelectors = options.mask ?? [];
513
- const matcher = prepareMaskMatcher(extraSelectors);
514
- const canvas = await html2canvas(target, {
515
- ignoreElements: (el) => matcher(el),
516
- onclone: (clonedDoc) => redactSensitiveInputs(clonedDoc, extraSelectors),
517
- useCORS: true,
518
- logging: false,
519
- backgroundColor: null
520
- });
521
- return await new Promise((resolve) => canvas.toBlob(resolve, "image/png"));
522
- } catch {
523
- return null;
524
- }
525
- }
526
-
527
517
  // src/widget/i18n.ts
528
518
  var DEFAULT_STRINGS = {
529
519
  "fab.label": "Send feedback",
@@ -547,6 +537,10 @@ var DEFAULT_STRINGS = {
547
537
  "form.screenshot.annotate": "Annotate",
548
538
  "form.screenshot.error_type": "Unsupported file type. Use PNG, JPEG or WebP.",
549
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.",
550
544
  "form.context.label": "Page",
551
545
  "type.bug": "Bug",
552
546
  "type.feature": "Feature request",
@@ -576,6 +570,33 @@ var DEFAULT_STRINGS = {
576
570
  "tab.send": "Send",
577
571
  "tab.mine": "My reports",
578
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",
579
600
  "changelog.empty.title": "Nothing resolved yet",
580
601
  "changelog.empty.body": "Once a report you sent is fixed it will appear here, grouped by week.",
581
602
  "changelog.week_of": "Week of {date}",
@@ -653,6 +674,10 @@ var FRENCH_STRINGS = {
653
674
  "form.screenshot.annotate": "Annoter",
654
675
  "form.screenshot.error_type": "Format non support\xE9. Utilisez PNG, JPEG ou WebP.",
655
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.",
656
681
  "form.context.label": "Page",
657
682
  "type.bug": "Bogue",
658
683
  "type.feature": "Suggestion",
@@ -682,6 +707,33 @@ var FRENCH_STRINGS = {
682
707
  "tab.send": "Envoyer",
683
708
  "tab.mine": "Mes rapports",
684
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",
685
737
  "changelog.empty.title": "Rien de r\xE9solu pour l\u2019instant",
686
738
  "changelog.empty.body": "Quand un rapport que vous avez envoy\xE9 est corrig\xE9, il appara\xEEtra ici, regroup\xE9 par semaine.",
687
739
  "changelog.week_of": "Semaine du {date}",
@@ -758,190 +810,1003 @@ function resolveStrings(overrides, options = {}) {
758
810
  import { h, render } from "preact";
759
811
  import { useCallback } from "preact/hooks";
760
812
 
761
- // src/widget/ChangelogList.tsx
762
- 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";
763
815
 
764
- // src/widget/ReportRow.tsx
816
+ // src/widget/ReportDetailView.tsx
817
+ import { useEffect, useRef, useState } from "preact/hooks";
818
+
819
+ // src/widget/CommentBubble.tsx
765
820
  import { jsx, jsxs } from "preact/jsx-runtime";
766
- function statusClassName(status) {
767
- return `pill pill-status pill-status--${status}`;
768
- }
769
- function severityClassName(severity) {
770
- return `pill pill-severity pill-severity--${severity}`;
771
- }
772
- function typeClassName() {
773
- return "pill pill-type";
774
- }
775
- function formatRelative(iso) {
776
- const then = Date.parse(iso);
777
- if (!Number.isFinite(then)) return "";
778
- const seconds = Math.max(1, Math.round((Date.now() - then) / 1e3));
779
- if (seconds < 60) return `${seconds}s`;
780
- const minutes = Math.round(seconds / 60);
781
- if (minutes < 60) return `${minutes}m`;
782
- const hours = Math.round(minutes / 60);
783
- if (hours < 48) return `${hours}h`;
784
- const days = Math.round(hours / 24);
785
- return `${days}d`;
786
- }
787
- function repliesLabel(count, strings) {
788
- if (count === 1) return strings["mine.replies_one"];
789
- return strings["mine.replies_many"].replace("{count}", String(count));
790
- }
791
- function ReportRow({ row, strings, onClick }) {
792
- const preview = row.description.length > 120 ? row.description.slice(0, 117) + "\u2026" : row.description;
793
- return /* @__PURE__ */ jsxs("button", { type: "button", class: "mine-row", onClick, children: [
794
- /* @__PURE__ */ jsxs("div", { class: "mine-row-pills", children: [
795
- /* @__PURE__ */ jsx("span", { class: statusClassName(row.status), children: strings[`status.${row.status}`] ?? row.status }),
796
- /* @__PURE__ */ jsx("span", { class: typeClassName(), children: strings[`type.${row.feedback_type}`] }),
797
- /* @__PURE__ */ jsx("span", { class: severityClassName(row.severity), children: strings[`severity.${row.severity}`] })
798
- ] }),
799
- /* @__PURE__ */ jsx("div", { class: "mine-row-preview", children: preview }),
800
- /* @__PURE__ */ jsxs("div", { class: "mine-row-meta", children: [
801
- /* @__PURE__ */ jsx("span", { children: formatRelative(row.updated_at || row.created_at) }),
802
- row.comment_count > 0 && /* @__PURE__ */ jsxs("span", { children: [
803
- "\xB7 ",
804
- repliesLabel(row.comment_count, strings)
805
- ] })
806
- ] })
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) })
807
832
  ] });
808
833
  }
809
-
810
- // src/widget/ChangelogList.tsx
811
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
812
- var POLL_MS = 3e4;
813
- function isoWeekKey(iso) {
814
- const d = new Date(iso);
815
- if (Number.isNaN(d.getTime())) return "";
816
- const day = (d.getUTCDay() + 6) % 7;
817
- const monday = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - day));
818
- 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
+ }
819
843
  }
820
- function groupRowsByWeek(rows) {
821
- const buckets = /* @__PURE__ */ new Map();
822
- for (const row of rows) {
823
- const key = isoWeekKey(row.resolved_at);
824
- if (!key) continue;
825
- const existing = buckets.get(key);
826
- if (existing) existing.push(row);
827
- 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" };
828
857
  }
829
- return Array.from(buckets.entries()).sort(([a], [b]) => a < b ? 1 : -1).map(([weekKey, weekRows]) => ({
830
- weekKey,
831
- label: formatWeekLabel(weekKey),
832
- rows: weekRows
833
- }));
858
+ if (file.size > MAX_SCREENSHOT_BYTES) {
859
+ return { kind: "size", maxMb: MAX_SCREENSHOT_BYTES / (1024 * 1024) };
860
+ }
861
+ return null;
834
862
  }
835
- 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;
836
874
  try {
837
- return new Date(weekKey).toLocaleDateString(void 0, {
838
- year: "numeric",
839
- month: "short",
840
- 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.
841
880
  });
842
881
  } catch {
843
- 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());
844
912
  }
845
913
  }
846
- function ChangelogList({ api, externalId, strings, onSelect }) {
847
- 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);
848
942
  const [error, setError] = useState(null);
849
- const [refreshing, setRefreshing] = useState(false);
943
+ const [composeBody, setComposeBody] = useState("");
944
+ const [sending, setSending] = useState(false);
945
+ const [closing, setClosing] = useState(false);
850
946
  const mountedRef = useRef(true);
851
- const fetchRows = async () => {
852
- setRefreshing(true);
853
- setError(null);
947
+ const fetchDetail = async () => {
854
948
  try {
855
- const next = await api.listChangelog(externalId);
949
+ const next = await api.getReport(reportId, externalId);
856
950
  if (!mountedRef.current) return;
857
- setRows(next);
951
+ setDetail(next);
952
+ setError(null);
858
953
  } catch (err) {
859
954
  if (!mountedRef.current) return;
860
- setError(err instanceof Error ? err.message : strings["mine.error"]);
861
- } finally {
862
- if (mountedRef.current) setRefreshing(false);
955
+ setError(err instanceof Error ? err.message : "load_failed");
863
956
  }
864
957
  };
865
958
  useEffect(() => {
866
959
  mountedRef.current = true;
867
- void fetchRows();
960
+ void fetchDetail();
868
961
  const timer = setInterval(() => {
869
- void fetchRows();
962
+ void fetchDetail();
870
963
  }, POLL_MS);
871
964
  return () => {
872
965
  mountedRef.current = false;
873
966
  clearInterval(timer);
874
967
  };
875
- }, [externalId]);
876
- const groups = useMemo(() => rows ? groupRowsByWeek(rows) : [], [rows]);
877
- const isLoading = rows === null && !error;
878
- const isEmpty = rows !== null && rows.length === 0;
879
- return /* @__PURE__ */ jsxs2("div", { class: "mine-list", children: [
880
- /* @__PURE__ */ jsxs2("div", { class: "mine-list-header", children: [
881
- /* @__PURE__ */ jsx2("h2", { children: strings["tab.changelog"] }),
882
- /* @__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(
883
1019
  "button",
884
1020
  {
885
1021
  type: "button",
886
- class: "btn",
887
- onClick: () => {
888
- void fetchRows();
889
- },
890
- disabled: refreshing,
891
- 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
+ ]
892
1029
  }
893
- )
894
- ] }),
895
- isLoading && /* @__PURE__ */ jsx2("div", { class: "mine-loading", children: strings["mine.loading"] }),
896
- error && /* @__PURE__ */ jsx2("div", { class: "error", children: error }),
897
- isEmpty && /* @__PURE__ */ jsxs2("div", { class: "mine-empty", children: [
898
- /* @__PURE__ */ jsx2("strong", { children: strings["changelog.empty.title"] }),
899
- /* @__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 })
900
1032
  ] }),
901
- groups.length > 0 && /* @__PURE__ */ jsx2("div", { class: "changelog-groups", children: groups.map((g) => /* @__PURE__ */ jsxs2("section", { class: "changelog-group", children: [
902
- /* @__PURE__ */ jsxs2("header", { class: "changelog-group-header", children: [
903
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-marker", "aria-hidden": "true", children: "\u25CF" }),
904
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-label", children: strings["changelog.week_of"].replace("{date}", g.label) }),
905
- /* @__PURE__ */ jsx2("span", { class: "changelog-group-rule", "aria-hidden": "true" }),
906
- /* @__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)) })
907
- ] }),
908
- /* @__PURE__ */ jsx2("ul", { class: "mine-rows", children: g.rows.map((row) => /* @__PURE__ */ jsx2("li", { children: /* @__PURE__ */ jsx2(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
909
- ] }, g.weekKey)) })
910
- ] });
911
- }
912
-
913
- // src/widget/Fab.tsx
914
- import { jsx as jsx3 } from "preact/jsx-runtime";
915
- function ChatBubbleIcon() {
916
- return /* @__PURE__ */ jsx3(
917
- "svg",
918
- {
919
- width: "24",
920
- height: "24",
921
- viewBox: "0 0 24 24",
922
- fill: "none",
923
- "aria-hidden": "true",
924
- focusable: "false",
925
- children: /* @__PURE__ */ jsx3(
926
- "path",
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",
927
1038
  {
928
- 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",
929
- fill: "currentColor"
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",
1787
+ fill: "none",
1788
+ "aria-hidden": "true",
1789
+ focusable: "false",
1790
+ children: /* @__PURE__ */ jsx6(
1791
+ "path",
1792
+ {
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",
1794
+ fill: "currentColor"
930
1795
  }
931
1796
  )
932
1797
  }
933
1798
  );
934
1799
  }
935
1800
  function Fab({ label, onClick }) {
936
- 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, {}) });
937
1802
  }
938
1803
 
939
1804
  // src/widget/Form.tsx
940
- 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";
941
1806
 
942
1807
  // src/widget/Annotator.tsx
943
- import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "preact/hooks";
944
- 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";
945
1810
  var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
946
1811
  var HIGHLIGHT_ALPHA = 0.35;
947
1812
  function drawShape(ctx, shape, sourceImage) {
@@ -1063,44 +1928,44 @@ function drawBlur(ctx, shape, sourceImage) {
1063
1928
  ctx.imageSmoothingEnabled = true;
1064
1929
  }
1065
1930
  var Icon = {
1066
- 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" }) }),
1067
- 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" }) }),
1068
- 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" }) }),
1069
- 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" }) }),
1070
- highlight: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
1071
- /* @__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" }),
1072
- /* @__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" })
1073
1938
  ] }),
1074
- blur: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
1075
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
1076
- /* @__PURE__ */ jsx4("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
1077
- /* @__PURE__ */ jsx4("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
1078
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.6" }),
1079
- /* @__PURE__ */ jsx4("rect", { x: "6", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.85" }),
1080
- /* @__PURE__ */ jsx4("rect", { x: "10", y: "7", width: "4", height: "3", fill: "currentColor", opacity: "0.5" }),
1081
- /* @__PURE__ */ jsx4("rect", { x: "2", y: "11", width: "4", height: "3", fill: "currentColor", opacity: "0.4" }),
1082
- /* @__PURE__ */ jsx4("rect", { x: "7", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.65" }),
1083
- /* @__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" })
1084
1949
  ] }),
1085
- 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" }) }),
1086
- 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" }) }),
1087
- 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" }) }),
1088
- 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" }) })
1089
1954
  };
1090
1955
  function Annotator({ imageBlob, strings, onSave, onCancel }) {
1091
- const canvasRef = useRef2(null);
1092
- const containerRef = useRef2(null);
1093
- const imageRef = useRef2(null);
1094
- const [tool, setTool] = useState2("rectangle");
1095
- const [color, setColor] = useState2(COLORS[0]);
1096
- const [shapes, setShapes] = useState2([]);
1097
- const [past, setPast] = useState2([]);
1098
- const [future, setFuture] = useState2([]);
1099
- const isDrawingRef = useRef2(false);
1100
- const [draftShape, setDraftShape] = useState2(null);
1101
- const [imageLoaded, setImageLoaded] = useState2(false);
1102
- const [saving, setSaving] = useState2(false);
1103
- 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);
1104
1969
  function addShape(shape) {
1105
1970
  setShapes((s) => {
1106
1971
  setPast((p) => [...p, s]);
@@ -1147,7 +2012,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1147
2012
  window.addEventListener("keydown", onKey);
1148
2013
  return () => window.removeEventListener("keydown", onKey);
1149
2014
  }, [onCancel]);
1150
- useEffect2(() => {
2015
+ useEffect4(() => {
1151
2016
  const onKey = (e) => {
1152
2017
  const mod = e.metaKey || e.ctrlKey;
1153
2018
  if (!mod) return;
@@ -1166,7 +2031,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1166
2031
  window.addEventListener("keydown", onKey);
1167
2032
  return () => window.removeEventListener("keydown", onKey);
1168
2033
  }, [past, future, shapes]);
1169
- useEffect2(() => {
2034
+ useEffect4(() => {
1170
2035
  const url = URL.createObjectURL(imageBlob);
1171
2036
  const img = new Image();
1172
2037
  img.onload = () => {
@@ -1176,7 +2041,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1176
2041
  img.src = url;
1177
2042
  return () => URL.revokeObjectURL(url);
1178
2043
  }, [imageBlob]);
1179
- useEffect2(() => {
2044
+ useEffect4(() => {
1180
2045
  if (!imageLoaded || !canvasRef.current || !imageRef.current || !containerRef.current) {
1181
2046
  return;
1182
2047
  }
@@ -1193,7 +2058,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1193
2058
  canvas.style.height = `${img.height * fitScale}px`;
1194
2059
  redraw();
1195
2060
  }, [imageLoaded]);
1196
- useEffect2(() => {
2061
+ useEffect4(() => {
1197
2062
  redraw();
1198
2063
  }, [shapes, draftShape]);
1199
2064
  function redraw() {
@@ -1319,7 +2184,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1319
2184
  { id: "highlight", icon: Icon.highlight, label: strings["annotator.tool.highlight"] },
1320
2185
  { id: "blur", icon: Icon.blur, label: strings["annotator.tool.blur"] }
1321
2186
  ];
1322
- return /* @__PURE__ */ jsx4(
2187
+ return /* @__PURE__ */ jsx7(
1323
2188
  "div",
1324
2189
  {
1325
2190
  class: "annotator-backdrop",
@@ -1327,10 +2192,10 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1327
2192
  onClick: (e) => {
1328
2193
  if (e.target === e.currentTarget) onCancel();
1329
2194
  },
1330
- children: /* @__PURE__ */ jsxs3("div", { class: "annotator", role: "dialog", "aria-modal": "true", "aria-label": strings["annotator.title"], children: [
1331
- /* @__PURE__ */ jsxs3("div", { class: "annotator-header", children: [
1332
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.title"] }),
1333
- /* @__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(
1334
2199
  "button",
1335
2200
  {
1336
2201
  type: "button",
@@ -1341,8 +2206,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1341
2206
  }
1342
2207
  )
1343
2208
  ] }),
1344
- /* @__PURE__ */ jsxs3("div", { class: "annotator-toolbar", children: [
1345
- /* @__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(
1346
2211
  "button",
1347
2212
  {
1348
2213
  type: "button",
@@ -1355,9 +2220,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1355
2220
  },
1356
2221
  t.id
1357
2222
  )) }),
1358
- /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1359
- /* @__PURE__ */ jsxs3("div", { class: "annotator-colors", children: [
1360
- 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(
1361
2226
  "button",
1362
2227
  {
1363
2228
  type: "button",
@@ -1369,7 +2234,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1369
2234
  },
1370
2235
  c
1371
2236
  )),
1372
- /* @__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(
1373
2238
  "input",
1374
2239
  {
1375
2240
  type: "color",
@@ -1379,8 +2244,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1379
2244
  }
1380
2245
  ) })
1381
2246
  ] }),
1382
- /* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
1383
- /* @__PURE__ */ jsxs3(
2247
+ /* @__PURE__ */ jsx7("span", { class: "annotator-sep" }),
2248
+ /* @__PURE__ */ jsxs6(
1384
2249
  "button",
1385
2250
  {
1386
2251
  type: "button",
@@ -1390,11 +2255,11 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1390
2255
  title: strings["annotator.undo"],
1391
2256
  children: [
1392
2257
  Icon.undo,
1393
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.undo"] })
2258
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.undo"] })
1394
2259
  ]
1395
2260
  }
1396
2261
  ),
1397
- /* @__PURE__ */ jsxs3(
2262
+ /* @__PURE__ */ jsxs6(
1398
2263
  "button",
1399
2264
  {
1400
2265
  type: "button",
@@ -1404,11 +2269,11 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1404
2269
  title: strings["annotator.redo"],
1405
2270
  children: [
1406
2271
  Icon.redo,
1407
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.redo"] })
2272
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.redo"] })
1408
2273
  ]
1409
2274
  }
1410
2275
  ),
1411
- /* @__PURE__ */ jsxs3(
2276
+ /* @__PURE__ */ jsxs6(
1412
2277
  "button",
1413
2278
  {
1414
2279
  type: "button",
@@ -1417,18 +2282,18 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1417
2282
  disabled: shapes.length === 0,
1418
2283
  children: [
1419
2284
  Icon.trash,
1420
- /* @__PURE__ */ jsx4("span", { children: strings["annotator.clear"] })
2285
+ /* @__PURE__ */ jsx7("span", { children: strings["annotator.clear"] })
1421
2286
  ]
1422
2287
  }
1423
2288
  ),
1424
- /* @__PURE__ */ jsx4("span", { class: "annotator-spacer" }),
1425
- /* @__PURE__ */ jsxs3("span", { class: "annotator-count", children: [
2289
+ /* @__PURE__ */ jsx7("span", { class: "annotator-spacer" }),
2290
+ /* @__PURE__ */ jsxs6("span", { class: "annotator-count", children: [
1426
2291
  shapes.length,
1427
2292
  " ",
1428
2293
  strings["annotator.count_suffix"]
1429
2294
  ] })
1430
2295
  ] }),
1431
- /* @__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(
1432
2297
  "canvas",
1433
2298
  {
1434
2299
  ref: canvasRef,
@@ -1439,9 +2304,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1439
2304
  class: "annotator-canvas"
1440
2305
  }
1441
2306
  ) }),
1442
- /* @__PURE__ */ jsxs3("div", { class: "annotator-footer", children: [
1443
- /* @__PURE__ */ jsx4("button", { type: "button", class: "btn", onClick: onCancel, children: strings["form.cancel"] }),
1444
- /* @__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(
1445
2310
  "button",
1446
2311
  {
1447
2312
  type: "button",
@@ -1457,55 +2322,32 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
1457
2322
  );
1458
2323
  }
1459
2324
 
1460
- // src/widget/screenshot-utils.ts
1461
- var ALLOWED_IMAGE_TYPES = [
1462
- "image/png",
1463
- "image/jpeg",
1464
- "image/webp"
1465
- ];
1466
- var MAX_SCREENSHOT_BYTES = 10 * 1024 * 1024;
1467
- function validateScreenshotFile(file) {
1468
- if (!ALLOWED_IMAGE_TYPES.includes(
1469
- file.type
1470
- )) {
1471
- return { kind: "type" };
1472
- }
1473
- if (file.size > MAX_SCREENSHOT_BYTES) {
1474
- return { kind: "size", maxMb: MAX_SCREENSHOT_BYTES / (1024 * 1024) };
1475
- }
1476
- return null;
1477
- }
1478
- function truncateUrl(url, maxLength = 80) {
1479
- if (url.length <= maxLength) return url;
1480
- const keepStart = Math.floor((maxLength - 1) / 2);
1481
- const keepEnd = maxLength - 1 - keepStart;
1482
- return `${url.slice(0, keepStart)}\u2026${url.slice(url.length - keepEnd)}`;
1483
- }
1484
-
1485
2325
  // src/widget/Form.tsx
1486
- import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1487
- var TYPES = ["bug", "feature", "question", "praise", "typo"];
1488
- 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"];
1489
2329
  function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1490
- const [description, setDescription] = useState3("");
1491
- const [feedbackType, setFeedbackType] = useState3("bug");
1492
- const [severity, setSeverity] = useState3("medium");
1493
- const [localError, setLocalError] = useState3("");
1494
- const [screenshotBlob, setScreenshotBlob] = useState3(null);
1495
- const [screenshotPreview, setScreenshotPreview] = useState3(null);
1496
- const [isDragOver, setIsDragOver] = useState3(false);
1497
- const [annotatorOpen, setAnnotatorOpen] = useState3(false);
1498
- const fileInputRef = useRef3(null);
1499
- 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);
1500
2342
  const submitting = status === "submitting";
1501
2343
  const submitLabel = submitting ? strings["form.submitting"] : strings["form.submit"];
1502
2344
  const pageUrl = typeof window !== "undefined" ? window.location.href : "";
1503
- useEffect3(() => {
2345
+ useEffect5(() => {
1504
2346
  return () => {
1505
2347
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1506
2348
  };
1507
2349
  }, [screenshotPreview]);
1508
- const acceptFile = (file) => {
2350
+ const acceptFile = (file, method = "manual") => {
1509
2351
  setLocalError("");
1510
2352
  if (file instanceof File) {
1511
2353
  const err = validateScreenshotFile(file);
@@ -1519,14 +2361,41 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1519
2361
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1520
2362
  setScreenshotBlob(file);
1521
2363
  setScreenshotPreview(URL.createObjectURL(file));
2364
+ setScreenshotMethod(method);
1522
2365
  };
1523
2366
  const clearScreenshot = () => {
1524
2367
  if (screenshotPreview) URL.revokeObjectURL(screenshotPreview);
1525
2368
  setScreenshotPreview(null);
1526
2369
  setScreenshotBlob(null);
2370
+ setScreenshotMethod("manual");
1527
2371
  setLocalError("");
1528
2372
  if (fileInputRef.current) fileInputRef.current.value = "";
1529
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
+ };
1530
2399
  const handleFileInputChange = (e) => {
1531
2400
  const file = e.target.files?.[0];
1532
2401
  if (file) acceptFile(file);
@@ -1548,7 +2417,7 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1548
2417
  const file = e.dataTransfer?.files?.[0];
1549
2418
  if (file) acceptFile(file);
1550
2419
  };
1551
- useEffect3(() => {
2420
+ useEffect5(() => {
1552
2421
  const zone = dropZoneRef.current;
1553
2422
  if (!zone) return;
1554
2423
  const onPaste = (e) => {
@@ -1586,14 +2455,17 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1586
2455
  feedback_type: feedbackType,
1587
2456
  severity
1588
2457
  };
1589
- if (screenshotBlob) values.screenshot = screenshotBlob;
2458
+ if (screenshotBlob) {
2459
+ values.screenshot = screenshotBlob;
2460
+ values.capture_method = screenshotMethod;
2461
+ }
1590
2462
  onSubmit(values);
1591
2463
  };
1592
- return /* @__PURE__ */ jsxs4("form", { onSubmit: handleSubmit, children: [
1593
- /* @__PURE__ */ jsx5("h2", { children: strings["form.title"] }),
1594
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1595
- /* @__PURE__ */ jsx5("label", { for: "mfb-desc", children: strings["form.description.label"] }),
1596
- /* @__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(
1597
2469
  "textarea",
1598
2470
  {
1599
2471
  id: "mfb-desc",
@@ -1603,35 +2475,35 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1603
2475
  }
1604
2476
  )
1605
2477
  ] }),
1606
- /* @__PURE__ */ jsxs4("div", { class: "row", children: [
1607
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1608
- /* @__PURE__ */ jsx5("label", { for: "mfb-type", children: strings["form.type.label"] }),
1609
- /* @__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(
1610
2482
  "select",
1611
2483
  {
1612
2484
  id: "mfb-type",
1613
2485
  value: feedbackType,
1614
2486
  onChange: (e) => setFeedbackType(e.target.value),
1615
- 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}`] }))
1616
2488
  }
1617
2489
  )
1618
2490
  ] }),
1619
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1620
- /* @__PURE__ */ jsx5("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
1621
- /* @__PURE__ */ jsx5(
2491
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2492
+ /* @__PURE__ */ jsx8("label", { for: "mfb-sev", children: strings["form.severity.label"] }),
2493
+ /* @__PURE__ */ jsx8(
1622
2494
  "select",
1623
2495
  {
1624
2496
  id: "mfb-sev",
1625
2497
  value: severity,
1626
2498
  onChange: (e) => setSeverity(e.target.value),
1627
- 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}`] }))
1628
2500
  }
1629
2501
  )
1630
2502
  ] })
1631
2503
  ] }),
1632
- /* @__PURE__ */ jsxs4("div", { class: "field", children: [
1633
- /* @__PURE__ */ jsx5("label", { children: strings["form.screenshot.label"] }),
1634
- /* @__PURE__ */ jsx5(
2504
+ /* @__PURE__ */ jsxs7("div", { class: "field", children: [
2505
+ /* @__PURE__ */ jsx8("label", { children: strings["form.screenshot.label"] }),
2506
+ /* @__PURE__ */ jsx8(
1635
2507
  "input",
1636
2508
  {
1637
2509
  ref: fileInputRef,
@@ -1643,30 +2515,30 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1643
2515
  tabIndex: -1
1644
2516
  }
1645
2517
  ),
1646
- screenshotPreview ? /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview", children: [
1647
- /* @__PURE__ */ jsx5("img", { src: screenshotPreview, alt: "" }),
1648
- /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview-actions", children: [
1649
- /* @__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(
1650
2522
  "button",
1651
2523
  {
1652
2524
  type: "button",
1653
2525
  class: "btn btn--primary screenshot-annotate",
1654
2526
  onClick: () => setAnnotatorOpen(true),
1655
2527
  children: [
1656
- /* @__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: [
1657
- /* @__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" }),
1658
- /* @__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" })
1659
2531
  ] }),
1660
2532
  strings["form.screenshot.annotate"]
1661
2533
  ]
1662
2534
  }
1663
2535
  ),
1664
- /* @__PURE__ */ jsxs4("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
1665
- /* @__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" }) }),
1666
2538
  strings["form.screenshot.remove"]
1667
2539
  ] })
1668
2540
  ] })
1669
- ] }) : /* @__PURE__ */ jsxs4(
2541
+ ] }) : /* @__PURE__ */ jsxs7(
1670
2542
  "div",
1671
2543
  {
1672
2544
  ref: dropZoneRef,
@@ -1685,33 +2557,69 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1685
2557
  onDragLeave: handleDragLeave,
1686
2558
  onDrop: handleDrop,
1687
2559
  children: [
1688
- /* @__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: [
1689
- /* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
1690
- /* @__PURE__ */ jsx5("polyline", { points: "17 8 12 3 7 8" }),
1691
- /* @__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" })
1692
2564
  ] }),
1693
- /* @__PURE__ */ jsxs4("div", { class: "screenshot-cta", children: [
1694
- /* @__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"] }),
1695
2567
  ", ",
1696
2568
  strings["form.screenshot.cta_rest"]
1697
2569
  ] }),
1698
- /* @__PURE__ */ jsx5("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
2570
+ /* @__PURE__ */ jsx8("div", { class: "screenshot-formats", children: strings["form.screenshot.formats"] })
1699
2571
  ]
1700
2572
  }
1701
- )
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
+ ] })
1702
2610
  ] }),
1703
- pageUrl && /* @__PURE__ */ jsxs4("div", { class: "page-context", title: pageUrl, children: [
1704
- /* @__PURE__ */ jsx5("span", { class: "page-context-label", children: strings["form.context.label"] }),
1705
- /* @__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) })
1706
2614
  ] }),
1707
- localError && /* @__PURE__ */ jsx5("div", { class: "error", children: localError }),
1708
- status === "error" && errorMessage && /* @__PURE__ */ jsx5("div", { class: "error", children: errorMessage }),
1709
- status === "success" && /* @__PURE__ */ jsx5("div", { class: "success", children: strings["form.success"] }),
1710
- /* @__PURE__ */ jsxs4("div", { class: "actions", children: [
1711
- /* @__PURE__ */ jsx5("button", { type: "button", class: "btn", onClick: onCancel, disabled: submitting, children: strings["form.cancel"] }),
1712
- /* @__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 })
1713
2621
  ] }),
1714
- annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx5(
2622
+ annotatorOpen && screenshotBlob && /* @__PURE__ */ jsx8(
1715
2623
  Annotator,
1716
2624
  {
1717
2625
  imageBlob: screenshotBlob,
@@ -1724,10 +2632,10 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
1724
2632
  }
1725
2633
 
1726
2634
  // src/widget/MineList.tsx
1727
- 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";
1728
2636
 
1729
2637
  // src/widget/KpiStrip.tsx
1730
- import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
2638
+ import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1731
2639
  function computeKpiCounts(rows) {
1732
2640
  const counts = {
1733
2641
  new: 0,
@@ -1794,468 +2702,151 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
1794
2702
  value: resolutionRate === null ? "\u2014" : `${resolutionRate}%`
1795
2703
  }
1796
2704
  ];
1797
- 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) => {
1798
2706
  const active = filter === c.key;
1799
2707
  const toggleTo = active ? "all" : c.key;
1800
- return /* @__PURE__ */ jsxs5(
2708
+ return /* @__PURE__ */ jsxs8(
1801
2709
  "button",
1802
2710
  {
1803
2711
  type: "button",
1804
2712
  class: `kpi-cell kpi-cell--${c.key}${active ? " is-active" : ""}`,
1805
2713
  onClick: () => onFilter(toggleTo),
1806
2714
  "aria-pressed": active,
1807
- children: [
1808
- /* @__PURE__ */ jsx6("span", { class: "kpi-value", children: c.value }),
1809
- /* @__PURE__ */ jsx6("span", { class: "kpi-label", children: c.label })
1810
- ]
1811
- }
1812
- );
1813
- }) });
1814
- }
1815
-
1816
- // src/widget/MineList.tsx
1817
- import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1818
- var POLL_MS2 = 3e4;
1819
- function MineList({ api, externalId, strings, onSelect }) {
1820
- const [rows, setRows] = useState4(null);
1821
- const [error, setError] = useState4(null);
1822
- const [refreshing, setRefreshing] = useState4(false);
1823
- const [filter, setFilter] = useState4("all");
1824
- const mountedRef = useRef4(true);
1825
- const fetchRows = async () => {
1826
- setRefreshing(true);
1827
- setError(null);
1828
- try {
1829
- const next = await api.listMine(externalId);
1830
- if (!mountedRef.current) return;
1831
- setRows(next);
1832
- } catch (err) {
1833
- if (!mountedRef.current) return;
1834
- setError(err instanceof Error ? err.message : strings["mine.error"]);
1835
- } finally {
1836
- if (mountedRef.current) setRefreshing(false);
1837
- }
1838
- };
1839
- useEffect4(() => {
1840
- mountedRef.current = true;
1841
- void fetchRows();
1842
- const timer = setInterval(() => {
1843
- void fetchRows();
1844
- }, POLL_MS2);
1845
- return () => {
1846
- mountedRef.current = false;
1847
- clearInterval(timer);
1848
- };
1849
- }, [externalId]);
1850
- const isEmpty = rows !== null && rows.length === 0;
1851
- const isLoading = rows === null && !error;
1852
- const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
1853
- const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
1854
- return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
1855
- /* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
1856
- /* @__PURE__ */ jsx7("h2", { children: strings["tab.mine"] }),
1857
- /* @__PURE__ */ jsx7(
1858
- "button",
1859
- {
1860
- type: "button",
1861
- class: "btn",
1862
- onClick: () => {
1863
- void fetchRows();
1864
- },
1865
- disabled: refreshing,
1866
- children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
1867
- }
1868
- )
1869
- ] }),
1870
- rows && rows.length > 0 && /* @__PURE__ */ jsx7(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
1871
- isLoading && /* @__PURE__ */ jsx7("div", { class: "mine-loading", children: strings["mine.loading"] }),
1872
- error && /* @__PURE__ */ jsx7("div", { class: "error", children: error }),
1873
- isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
1874
- /* @__PURE__ */ jsx7("strong", { children: strings["mine.empty.title"] }),
1875
- /* @__PURE__ */ jsx7("p", { children: strings["mine.empty.body"] })
1876
- ] }),
1877
- visibleEmpty && /* @__PURE__ */ jsx7("div", { class: "mine-empty", children: /* @__PURE__ */ jsx7("p", { children: strings["mine.filter.empty"] }) }),
1878
- 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) }) })) })
1879
- ] });
1880
- }
1881
-
1882
- // src/widget/Modal.tsx
1883
- import { useEffect as useEffect5, useRef as useRef5 } from "preact/hooks";
1884
- import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
1885
- function Modal({ onDismiss, children, closeLabel = "Close" }) {
1886
- const modalRef = useRef5(null);
1887
- const previouslyFocused = useRef5(null);
1888
- useEffect5(() => {
1889
- previouslyFocused.current = document.activeElement;
1890
- const onKey = (e) => {
1891
- if (e.key !== "Escape") return;
1892
- const root = modalRef.current?.getRootNode();
1893
- if (root instanceof ShadowRoot && root.querySelector(".annotator-backdrop")) {
1894
- return;
1895
- }
1896
- e.stopPropagation();
1897
- onDismiss();
1898
- };
1899
- window.addEventListener("keydown", onKey);
1900
- const first = modalRef.current?.querySelector(
1901
- "textarea, input, select, button"
1902
- );
1903
- first?.focus();
1904
- return () => {
1905
- window.removeEventListener("keydown", onKey);
1906
- const prev = previouslyFocused.current;
1907
- if (prev && typeof prev.focus === "function") prev.focus();
1908
- };
1909
- }, [onDismiss]);
1910
- return /* @__PURE__ */ jsx8(
1911
- "div",
1912
- {
1913
- class: "backdrop",
1914
- role: "presentation",
1915
- onClick: (e) => {
1916
- if (e.target === e.currentTarget) onDismiss();
1917
- },
1918
- children: /* @__PURE__ */ jsxs7("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
1919
- /* @__PURE__ */ jsx8(
1920
- "button",
1921
- {
1922
- type: "button",
1923
- class: "modal-close",
1924
- "aria-label": closeLabel,
1925
- onClick: onDismiss,
1926
- children: "\xD7"
1927
- }
1928
- ),
1929
- children
1930
- ] })
1931
- }
1932
- );
1933
- }
1934
-
1935
- // src/widget/ReportDetailView.tsx
1936
- import { useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "preact/hooks";
1937
-
1938
- // src/widget/CommentBubble.tsx
1939
- import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
1940
- function CommentBubble({ comment, strings }) {
1941
- const isMine = comment.is_mine;
1942
- const isAgent = !isMine && comment.author_source === "mcp";
1943
- const isSystem = !isMine && comment.author_source === "system";
1944
- const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
1945
- const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
1946
- const label = comment.author_label || strings[labelKey];
1947
- return /* @__PURE__ */ jsxs8("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
1948
- !isMine && label && /* @__PURE__ */ jsx9("div", { class: `comment-author comment-author--${variant}`, children: label }),
1949
- /* @__PURE__ */ jsx9("div", { class: "comment-body", children: comment.body }),
1950
- /* @__PURE__ */ jsx9("div", { class: "comment-time", children: formatTime(comment.created_at) })
1951
- ] });
1952
- }
1953
- function formatTime(iso) {
1954
- try {
1955
- return new Date(iso).toLocaleString(void 0, {
1956
- dateStyle: "short",
1957
- timeStyle: "short"
1958
- });
1959
- } catch {
1960
- return iso;
1961
- }
1962
- }
1963
-
1964
- // src/widget/ReportDetailView.tsx
1965
- import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
1966
- var POLL_MS3 = 3e4;
1967
- function ReportDetailView({
1968
- api,
1969
- externalId,
1970
- reportId,
1971
- strings,
1972
- onBack
1973
- }) {
1974
- const [detail, setDetail] = useState5(null);
1975
- const [error, setError] = useState5(null);
1976
- const [composeBody, setComposeBody] = useState5("");
1977
- const [sending, setSending] = useState5(false);
1978
- const [closing, setClosing] = useState5(false);
1979
- const mountedRef = useRef6(true);
1980
- const fetchDetail = async () => {
1981
- try {
1982
- const next = await api.getReport(reportId, externalId);
1983
- if (!mountedRef.current) return;
1984
- setDetail(next);
1985
- setError(null);
1986
- } catch (err) {
1987
- if (!mountedRef.current) return;
1988
- setError(err instanceof Error ? err.message : "load_failed");
1989
- }
1990
- };
1991
- useEffect6(() => {
1992
- mountedRef.current = true;
1993
- void fetchDetail();
1994
- const timer = setInterval(() => {
1995
- void fetchDetail();
1996
- }, POLL_MS3);
1997
- return () => {
1998
- mountedRef.current = false;
1999
- clearInterval(timer);
2000
- };
2001
- }, [reportId, externalId]);
2002
- const handleSend = async () => {
2003
- if (!composeBody.trim() || sending) return;
2004
- setSending(true);
2005
- try {
2006
- const nonce = `${reportId}:${Date.now()}`;
2007
- const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
2008
- if (!mountedRef.current) return;
2009
- setComposeBody("");
2010
- setDetail(
2011
- (prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
2012
- );
2013
- void fetchDetail();
2014
- } catch (err) {
2015
- if (!mountedRef.current) return;
2016
- setError(err instanceof Error ? err.message : "comment_failed");
2017
- } finally {
2018
- if (mountedRef.current) setSending(false);
2019
- }
2020
- };
2021
- const handleClose = async () => {
2022
- if (closing) return;
2023
- 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);
2024
2736
  try {
2025
- const next = await api.closeAsResolved(reportId, externalId);
2737
+ const next = await api.listMine(externalId);
2026
2738
  if (!mountedRef.current) return;
2027
- setDetail(next);
2739
+ setRows(next);
2028
2740
  } catch (err) {
2029
2741
  if (!mountedRef.current) return;
2030
- setError(err instanceof Error ? err.message : "close_failed");
2742
+ setError(err instanceof Error ? err.message : strings["mine.error"]);
2031
2743
  } finally {
2032
- if (mountedRef.current) setClosing(false);
2744
+ if (mountedRef.current) setRefreshing(false);
2033
2745
  }
2034
2746
  };
2035
- if (!detail && !error) {
2036
- return /* @__PURE__ */ jsx10("div", { class: "mine-loading", children: strings["mine.loading"] });
2037
- }
2038
- if (!detail) {
2039
- return /* @__PURE__ */ jsx10("div", { class: "error", role: "alert", children: error });
2040
- }
2041
- const showCloseCta = detail.status === "awaiting_validation";
2042
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail", children: [
2043
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-header", children: [
2044
- /* @__PURE__ */ jsxs9("button", { type: "button", class: "btn", onClick: onBack, children: [
2045
- "\u2190 ",
2046
- strings["detail.back"]
2047
- ] }),
2048
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${detail.status}`, children: strings[`status.${detail.status}`] ?? detail.status })
2049
- ] }),
2050
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-body", children: [
2051
- /* @__PURE__ */ jsx10(ContextBlock, { detail, strings }),
2052
- /* @__PURE__ */ jsx10("p", { class: "report-detail-description", children: detail.description }),
2053
- detail.screenshot_url && /* @__PURE__ */ jsx10(
2054
- "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",
2055
2767
  {
2056
- class: "report-detail-screenshot",
2057
- href: detail.screenshot_url,
2058
- target: "_blank",
2059
- rel: "noopener noreferrer",
2060
- 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"]
2061
2775
  }
2062
- ),
2063
- /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.thread"] }),
2064
- 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 }) })) }),
2065
- detail.status_history && detail.status_history.length > 0 && /* @__PURE__ */ jsx10(StatusHistorySection, { rows: detail.status_history, strings }),
2066
- detail.technical_context && /* @__PURE__ */ jsx10(TechnicalContextDrawer, { ctx: detail.technical_context, strings }),
2067
- /* @__PURE__ */ jsxs9("div", { class: "report-compose", children: [
2068
- /* @__PURE__ */ jsx10(
2069
- "textarea",
2070
- {
2071
- value: composeBody,
2072
- placeholder: strings["detail.compose_placeholder"],
2073
- onInput: (e) => setComposeBody(e.target.value),
2074
- disabled: sending
2075
- }
2076
- ),
2077
- /* @__PURE__ */ jsxs9("div", { class: "report-compose-actions", children: [
2078
- showCloseCta && /* @__PURE__ */ jsx10(
2079
- "button",
2080
- {
2081
- type: "button",
2082
- class: "btn",
2083
- onClick: handleClose,
2084
- disabled: closing,
2085
- children: closing ? strings["detail.close_busy"] : strings["detail.close_cta"]
2086
- }
2087
- ),
2088
- /* @__PURE__ */ jsx10(
2089
- "button",
2090
- {
2091
- type: "button",
2092
- class: "btn btn--primary",
2093
- onClick: handleSend,
2094
- disabled: !composeBody.trim() || sending,
2095
- children: sending ? strings["detail.compose_sending"] : strings["detail.compose_send"]
2096
- }
2097
- )
2098
- ] })
2099
- ] }),
2100
- error && /* @__PURE__ */ jsx10("div", { class: "error", children: error })
2101
- ] })
2102
- ] });
2103
- }
2104
- function appendComment(current, next) {
2105
- if (current.some((c) => c.id === next.id)) return current;
2106
- return [...current, next];
2107
- }
2108
- function ContextBlock({ detail, strings }) {
2109
- const captureKey = `detail.context.capture.${detail.capture_method}`;
2110
- const captureLabel = strings[captureKey] ?? detail.capture_method;
2111
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail-context", children: [
2112
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-pills", children: [
2113
- /* @__PURE__ */ jsx10("span", { class: "pill pill-type", children: strings[`type.${detail.feedback_type}`] }),
2114
- /* @__PURE__ */ jsx10("span", { class: `pill pill-severity pill-severity--${detail.severity}`, children: strings[`severity.${detail.severity}`] }),
2115
- /* @__PURE__ */ jsx10("span", { class: "pill pill-capture", children: captureLabel })
2776
+ )
2116
2777
  ] }),
2117
- /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", children: [
2118
- /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.submitted_at"] }),
2119
- /* @__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"] })
2120
2784
  ] }),
2121
- detail.page_url && /* @__PURE__ */ jsxs9("div", { class: "report-detail-context-line", title: detail.page_url, children: [
2122
- /* @__PURE__ */ jsx10("span", { class: "report-detail-context-label", children: strings["detail.context.page"] }),
2123
- /* @__PURE__ */ jsx10(
2124
- "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",
2125
2828
  {
2126
- class: "report-detail-context-url",
2127
- href: detail.page_url,
2128
- target: "_blank",
2129
- rel: "noopener noreferrer",
2130
- 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
+ ]
2131
2846
  }
2132
2847
  )
2133
- ] })
2134
- ] });
2135
- }
2136
- function formatSubmittedAt(iso) {
2137
- try {
2138
- return new Date(iso).toLocaleString(void 0, {
2139
- dateStyle: "medium",
2140
- timeStyle: "short"
2141
- });
2142
- } catch {
2143
- return iso;
2144
- }
2145
- }
2146
- var TECH_CONSOLE_LIMIT = 20;
2147
- var TECH_NETWORK_LIMIT = 15;
2148
- function TechnicalContextDrawer({ ctx, strings }) {
2149
- const errors = ctx.errors ?? [];
2150
- const consoleLogs = (ctx.consoleLogs ?? []).slice(-TECH_CONSOLE_LIMIT);
2151
- const network = (ctx.networkRequests ?? []).slice(-TECH_NETWORK_LIMIT);
2152
- const device = ctx.device;
2153
- const hasAny = !!device || errors.length > 0 || consoleLogs.length > 0 || network.length > 0;
2154
- if (!hasAny) return null;
2155
- const errorCount = errors.length;
2156
- 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"];
2157
- return /* @__PURE__ */ jsxs9("details", { class: "report-detail-tech", children: [
2158
- /* @__PURE__ */ jsx10("summary", { children: summary }),
2159
- /* @__PURE__ */ jsxs9("div", { class: "tech-body", children: [
2160
- device && /* @__PURE__ */ jsx10(DeviceSection, { device, strings }),
2161
- errors.length > 0 && /* @__PURE__ */ jsx10(ErrorsSection, { errors, strings }),
2162
- consoleLogs.length > 0 && /* @__PURE__ */ jsx10(ConsoleSection, { logs: consoleLogs, strings }),
2163
- network.length > 0 && /* @__PURE__ */ jsx10(NetworkSection, { rows: network, strings })
2164
- ] })
2165
- ] });
2166
- }
2167
- function DeviceSection({
2168
- device,
2169
- strings
2170
- }) {
2171
- const parts = [];
2172
- if (device?.viewport) {
2173
- const dpr = device.viewport.dpr ? ` @${device.viewport.dpr}x` : "";
2174
- parts.push({
2175
- label: strings["detail.tech.device.viewport"],
2176
- value: `${device.viewport.w}\xD7${device.viewport.h}${dpr}`
2177
- });
2178
- }
2179
- if (device?.platform) parts.push({ label: strings["detail.tech.device.platform"], value: device.platform });
2180
- if (device?.language) parts.push({ label: strings["detail.tech.device.language"], value: device.language });
2181
- if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
2182
- if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
2183
- if (parts.length === 0) return null;
2184
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2185
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.device"] }),
2186
- /* @__PURE__ */ jsx10("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs9(Fragment, { children: [
2187
- /* @__PURE__ */ jsx10("dt", { children: p.label }),
2188
- /* @__PURE__ */ jsx10("dd", { children: p.value })
2189
- ] })) })
2190
- ] });
2191
- }
2192
- function ErrorsSection({
2193
- errors,
2194
- strings
2195
- }) {
2196
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2197
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.errors"] }),
2198
- /* @__PURE__ */ jsx10("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs9("li", { children: [
2199
- /* @__PURE__ */ jsx10("div", { class: "tech-errors-msg", children: e.message }),
2200
- e.stack && /* @__PURE__ */ jsx10("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
2201
- ] })) })
2202
- ] });
2203
- }
2204
- function ConsoleSection({
2205
- logs,
2206
- strings
2207
- }) {
2208
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2209
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.console"] }),
2210
- /* @__PURE__ */ jsx10("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs9("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
2211
- /* @__PURE__ */ jsx10("span", { class: "tech-console-level", children: l.level }),
2212
- /* @__PURE__ */ jsx10("span", { class: "tech-console-msg", children: l.message })
2213
- ] })) })
2214
- ] });
2215
- }
2216
- function NetworkSection({
2217
- rows,
2218
- strings
2219
- }) {
2220
- return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
2221
- /* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.network"] }),
2222
- /* @__PURE__ */ jsx10("ul", { class: "tech-network", children: rows.map((n) => {
2223
- const failed = n.status >= 400 || n.status === 0;
2224
- return /* @__PURE__ */ jsxs9("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
2225
- /* @__PURE__ */ jsx10("span", { class: "tech-network-status", children: n.status || "\u2014" }),
2226
- /* @__PURE__ */ jsx10("span", { class: "tech-network-method", children: n.method }),
2227
- /* @__PURE__ */ jsx10("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
2228
- /* @__PURE__ */ jsxs9("span", { class: "tech-network-time", children: [
2229
- Math.round(n.durationMs),
2230
- "ms"
2231
- ] })
2232
- ] });
2233
- }) })
2234
- ] });
2235
- }
2236
- function truncateStack(stack) {
2237
- const lines = stack.split("\n");
2238
- if (lines.length <= 12) return stack;
2239
- return lines.slice(0, 12).join("\n") + "\n \u2026";
2240
- }
2241
- function StatusHistorySection({ rows, strings }) {
2242
- return /* @__PURE__ */ jsxs9("div", { class: "report-detail-history", children: [
2243
- /* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.history"] }),
2244
- /* @__PURE__ */ jsx10("ol", { class: "status-history", children: rows.map((r) => {
2245
- const from = strings[`status.${r.from_status}`] ?? r.from_status;
2246
- const to = strings[`status.${r.to_status}`] ?? r.to_status;
2247
- const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
2248
- return /* @__PURE__ */ jsxs9("li", { class: "status-history-row", children: [
2249
- /* @__PURE__ */ jsx10("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
2250
- /* @__PURE__ */ jsxs9("span", { class: "status-history-transition", children: [
2251
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
2252
- /* @__PURE__ */ jsx10("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
2253
- /* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
2254
- ] }),
2255
- /* @__PURE__ */ jsx10("span", { class: `status-history-source status-history-source--${r.changed_by_source}`, children: strings[sourceKey] })
2256
- ] });
2257
- }) })
2258
- ] });
2848
+ }
2849
+ );
2259
2850
  }
2260
2851
 
2261
2852
  // src/widget/styles.ts
@@ -2561,6 +3152,19 @@ var WIDGET_STYLES = `
2561
3152
  border-color: color-mix(in srgb, var(--mfb-accent) 88%, black);
2562
3153
  }
2563
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
+
2564
3168
  .btn[disabled] { opacity: 0.6; cursor: not-allowed; }
2565
3169
 
2566
3170
  .error { color: #dc2626; font-size: 13px; }
@@ -2624,6 +3228,40 @@ var WIDGET_STYLES = `
2624
3228
  .screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
2625
3229
  .screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
2626
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
+
2627
3265
  .screenshot-preview {
2628
3266
  position: relative;
2629
3267
  border: 1px solid var(--mfb-border);
@@ -2890,7 +3528,17 @@ var WIDGET_STYLES = `
2890
3528
  }
2891
3529
  .tab-button[aria-selected="true"] { font-weight: 600; }
2892
3530
 
2893
- .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
+ }
2894
3542
  .mine-list-header {
2895
3543
  display: flex;
2896
3544
  align-items: center;
@@ -2930,9 +3578,19 @@ var WIDGET_STYLES = `
2930
3578
  flex-direction: column;
2931
3579
  gap: 4px;
2932
3580
  width: 100%;
2933
- 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);
2934
3592
  }
2935
- .mine-row:hover { border-color: var(--mfb-text-muted); }
3593
+ .mine-row:active { transform: scale(0.997); }
2936
3594
  .mine-row:focus-visible {
2937
3595
  outline: 2px solid var(--mfb-accent);
2938
3596
  outline-offset: 2px;
@@ -3300,7 +3958,7 @@ var WIDGET_STYLES = `
3300
3958
  background: var(--mfb-bg);
3301
3959
  border: 1px solid var(--mfb-border);
3302
3960
  border-radius: var(--mfb-radius);
3303
- padding: 8px 6px;
3961
+ padding: 10px 10px 10px 14px;
3304
3962
  cursor: pointer;
3305
3963
  display: flex;
3306
3964
  flex-direction: column;
@@ -3308,9 +3966,36 @@ var WIDGET_STYLES = `
3308
3966
  gap: 2px;
3309
3967
  font-family: inherit;
3310
3968
  color: inherit;
3311
- 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);
3312
3998
  }
3313
- .kpi-cell:hover { background: var(--mfb-surface); }
3314
3999
  .kpi-cell.is-active {
3315
4000
  border-color: var(--mfb-accent);
3316
4001
  background: rgba(59, 130, 246, 0.08);
@@ -3338,6 +4023,10 @@ var WIDGET_STYLES = `
3338
4023
  display: flex;
3339
4024
  flex-direction: column;
3340
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; }
3341
4030
  }
3342
4031
  .changelog-group {
3343
4032
  display: flex;
@@ -3369,10 +4058,437 @@ var WIDGET_STYLES = `
3369
4058
  font-variant-numeric: tabular-nums;
3370
4059
  font-weight: 600;
3371
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
+ }
3372
4488
  `;
3373
4489
 
3374
4490
  // src/widget/mount.tsx
3375
- 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";
3376
4492
  function mountWidget(options) {
3377
4493
  const shadow = options.host.attachShadow({ mode: "open" });
3378
4494
  const style = document.createElement("style");
@@ -3419,22 +4535,23 @@ function mountWidget(options) {
3419
4535
  const externalId = options.getExternalId?.();
3420
4536
  const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
3421
4537
  const showMineTab = Boolean(options.api && externalId);
3422
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
3423
- fabVisible && /* @__PURE__ */ jsx11(
4538
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
4539
+ fabVisible && /* @__PURE__ */ jsx12(
3424
4540
  Fab,
3425
4541
  {
3426
4542
  label: options.strings["fab.label"],
3427
4543
  onClick: () => rerender({ ...currentState, open: true })
3428
4544
  }
3429
4545
  ),
3430
- state.open && /* @__PURE__ */ jsxs10(
4546
+ state.open && /* @__PURE__ */ jsxs11(
3431
4547
  Modal,
3432
4548
  {
3433
4549
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
3434
4550
  closeLabel: options.strings["form.close"],
4551
+ expanded: state.tab === "board",
3435
4552
  children: [
3436
- showMineTab && /* @__PURE__ */ jsxs10("div", { class: "tab-strip", role: "tablist", children: [
3437
- /* @__PURE__ */ jsx11(
4553
+ showMineTab && /* @__PURE__ */ jsxs11("div", { class: "tab-strip", role: "tablist", children: [
4554
+ /* @__PURE__ */ jsx12(
3438
4555
  "button",
3439
4556
  {
3440
4557
  type: "button",
@@ -3445,7 +4562,7 @@ function mountWidget(options) {
3445
4562
  children: options.strings["tab.send"]
3446
4563
  }
3447
4564
  ),
3448
- /* @__PURE__ */ jsx11(
4565
+ /* @__PURE__ */ jsx12(
3449
4566
  "button",
3450
4567
  {
3451
4568
  type: "button",
@@ -3456,7 +4573,7 @@ function mountWidget(options) {
3456
4573
  children: options.strings["tab.mine"]
3457
4574
  }
3458
4575
  ),
3459
- /* @__PURE__ */ jsx11(
4576
+ /* @__PURE__ */ jsx12(
3460
4577
  "button",
3461
4578
  {
3462
4579
  type: "button",
@@ -3466,9 +4583,20 @@ function mountWidget(options) {
3466
4583
  onClick: () => rerender(clearSelected({ ...currentState, tab: "changelog" })),
3467
4584
  children: options.strings["tab.changelog"]
3468
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
+ }
3469
4597
  )
3470
4598
  ] }),
3471
- state.tab === "send" && /* @__PURE__ */ jsx11(
4599
+ state.tab === "send" && /* @__PURE__ */ jsx12(
3472
4600
  Form,
3473
4601
  {
3474
4602
  strings: options.strings,
@@ -3478,7 +4606,7 @@ function mountWidget(options) {
3478
4606
  ...state.error !== void 0 && { errorMessage: state.error }
3479
4607
  }
3480
4608
  ),
3481
- state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4609
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3482
4610
  MineList,
3483
4611
  {
3484
4612
  api: options.api,
@@ -3487,7 +4615,7 @@ function mountWidget(options) {
3487
4615
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3488
4616
  }
3489
4617
  ),
3490
- (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(
3491
4619
  ReportDetailView,
3492
4620
  {
3493
4621
  api: options.api,
@@ -3497,7 +4625,7 @@ function mountWidget(options) {
3497
4625
  onBack: () => rerender(clearSelected({ ...currentState }))
3498
4626
  }
3499
4627
  ),
3500
- state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4628
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3501
4629
  ChangelogList,
3502
4630
  {
3503
4631
  api: options.api,
@@ -3505,6 +4633,16 @@ function mountWidget(options) {
3505
4633
  strings: options.strings,
3506
4634
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3507
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
+ }
3508
4646
  )
3509
4647
  ]
3510
4648
  }
@@ -3558,15 +4696,13 @@ function createFeedback(config) {
3558
4696
  document.body.appendChild(host);
3559
4697
  }
3560
4698
  async function buildAndSubmit(values) {
3561
- const manualScreenshot = values.screenshot;
3562
- const screenshot = values.synthetic ? void 0 : manualScreenshot ?? await takeScreenshot(document.body, {
3563
- mask: [".mhosaic-feedback", "[data-mfb-mask]"]
3564
- });
4699
+ const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
3565
4700
  const technical_context = capture.snapshot();
3566
4701
  if (user) technical_context.user = user;
3567
4702
  if (metadata && Object.keys(metadata).length > 0) {
3568
4703
  technical_context.metadata = { ...metadata };
3569
4704
  }
4705
+ const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
3570
4706
  const payload = {
3571
4707
  description: values.description,
3572
4708
  feedback_type: values.feedback_type ?? "bug",
@@ -3574,13 +4710,13 @@ function createFeedback(config) {
3574
4710
  env,
3575
4711
  page_url: window.location.href,
3576
4712
  user_agent: navigator.userAgent,
3577
- capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
4713
+ capture_method: captureMethod,
3578
4714
  technical_context
3579
4715
  };
3580
- if ("0.11.0") {
3581
- payload.widget_version = "0.11.0";
4716
+ if ("0.12.0") {
4717
+ payload.widget_version = "0.12.0";
3582
4718
  }
3583
- if (screenshot) payload.screenshot = screenshot;
4719
+ if (manualScreenshot) payload.screenshot = manualScreenshot;
3584
4720
  if (values.synthetic) payload.synthetic = true;
3585
4721
  if (user?.id !== void 0 && user.id !== null && user.id !== "") {
3586
4722
  payload.user = {
@@ -3664,4 +4800,4 @@ function createFeedback(config) {
3664
4800
  export {
3665
4801
  createFeedback
3666
4802
  };
3667
- //# sourceMappingURL=chunk-AO3BXEG5.mjs.map
4803
+ //# sourceMappingURL=chunk-XSYC5TDL.mjs.map