@mhosaic/feedback 0.11.0 → 0.12.1

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,10 +2702,10 @@ 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",
@@ -1805,457 +2713,140 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
1805
2713
  onClick: () => onFilter(toggleTo),
1806
2714
  "aria-pressed": active,
1807
2715
  children: [
1808
- /* @__PURE__ */ jsx6("span", { class: "kpi-value", children: c.value }),
1809
- /* @__PURE__ */ jsx6("span", { class: "kpi-label", children: c.label })
2716
+ /* @__PURE__ */ jsx9("span", { class: "kpi-value", children: c.value }),
2717
+ /* @__PURE__ */ jsx9("span", { class: "kpi-label", children: c.label })
1810
2718
  ]
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);
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
@@ -2414,6 +3005,17 @@ var WIDGET_STYLES = `
2414
3005
  after the modal becomes visible get stable coordinates. 220ms
2415
3006
  ease-out is snappy enough that human users barely register it. */
2416
3007
  animation: mfb-modal-in 220ms ease-out;
3008
+ /* Transition lives on the BASE rule with a SHORTER duration so the
3009
+ "back" trip (Board \u2192 other tab, where .is-expanded is removed)
3010
+ feels snappy. The grow direction overrides this with a slower
3011
+ curve in .modal.is-expanded below \u2014 the asymmetry is intentional:
3012
+ a slow grow reads as a transport, a slow shrink reads as
3013
+ hesitation. */
3014
+ transition:
3015
+ width 200ms cubic-bezier(0.4, 0, 0.2, 1),
3016
+ height 200ms cubic-bezier(0.4, 0, 0.2, 1),
3017
+ max-height 200ms cubic-bezier(0.4, 0, 0.2, 1),
3018
+ padding 200ms cubic-bezier(0.4, 0, 0.2, 1);
2417
3019
  }
2418
3020
 
2419
3021
  @keyframes mfb-modal-in {
@@ -2457,6 +3059,14 @@ var WIDGET_STYLES = `
2457
3059
  display: flex;
2458
3060
  flex-direction: column;
2459
3061
  gap: var(--mfb-space-5);
3062
+ /* Match the entrance motion the Board/MineList/Changelog containers
3063
+ * use so switching tabs (especially Board \u2192 Send, where the modal
3064
+ * is mid-shrink) doesn't show a content snap behind the transition.
3065
+ * 40ms delay lets the modal's shrink finish first. */
3066
+ animation: mfb-board-in 220ms ease-out 40ms both;
3067
+ }
3068
+ @media (prefers-reduced-motion: reduce) {
3069
+ .modal form { animation: none; }
2460
3070
  }
2461
3071
 
2462
3072
  .modal-close {
@@ -2561,6 +3171,19 @@ var WIDGET_STYLES = `
2561
3171
  border-color: color-mix(in srgb, var(--mfb-accent) 88%, black);
2562
3172
  }
2563
3173
 
3174
+ /* Subdued button \u2014 borrows accent text but transparent background. Used
3175
+ * for the "Capture this page" alt-action that sits below the dropzone,
3176
+ * where a full --primary would compete with the form's main submit. */
3177
+ .btn--ghost {
3178
+ background: transparent;
3179
+ color: var(--mfb-accent);
3180
+ border-color: color-mix(in srgb, var(--mfb-accent) 40%, transparent);
3181
+ }
3182
+ .btn--ghost:hover {
3183
+ background: color-mix(in srgb, var(--mfb-accent) 8%, transparent);
3184
+ border-color: var(--mfb-accent);
3185
+ }
3186
+
2564
3187
  .btn[disabled] { opacity: 0.6; cursor: not-allowed; }
2565
3188
 
2566
3189
  .error { color: #dc2626; font-size: 13px; }
@@ -2624,6 +3247,40 @@ var WIDGET_STYLES = `
2624
3247
  .screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
2625
3248
  .screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
2626
3249
 
3250
+ /* "or \u2014 Capture this page" row sits below the dropzone. The thin "or"
3251
+ * separator borrows the pattern from auth forms so the second option
3252
+ * reads as an alternative, not a follow-up step. v0.12. */
3253
+ .screenshot-alt {
3254
+ display: flex;
3255
+ align-items: center;
3256
+ gap: 12px;
3257
+ margin-top: 8px;
3258
+ flex-wrap: wrap;
3259
+ }
3260
+ .screenshot-or {
3261
+ position: relative;
3262
+ font-size: var(--mfb-text-xs);
3263
+ color: var(--mfb-text-muted);
3264
+ text-transform: uppercase;
3265
+ letter-spacing: 0.08em;
3266
+ padding: 0 4px;
3267
+ }
3268
+ .screenshot-capture-page {
3269
+ display: inline-flex;
3270
+ align-items: center;
3271
+ gap: 6px;
3272
+ }
3273
+ .screenshot-capture-page:disabled {
3274
+ opacity: 0.55;
3275
+ cursor: progress;
3276
+ }
3277
+ .screenshot-capture-hint {
3278
+ font-size: var(--mfb-text-xs);
3279
+ color: var(--mfb-text-muted);
3280
+ flex: 1 1 100%;
3281
+ margin-top: 4px;
3282
+ }
3283
+
2627
3284
  .screenshot-preview {
2628
3285
  position: relative;
2629
3286
  border: 1px solid var(--mfb-border);
@@ -2890,7 +3547,17 @@ var WIDGET_STYLES = `
2890
3547
  }
2891
3548
  .tab-button[aria-selected="true"] { font-weight: 600; }
2892
3549
 
2893
- .mine-list { display: flex; flex-direction: column; gap: var(--mfb-space-3); }
3550
+ .mine-list {
3551
+ display: flex;
3552
+ flex-direction: column;
3553
+ gap: var(--mfb-space-3);
3554
+ /* Matches .board-view's entrance so switching tabs feels like a real
3555
+ * page transition rather than a flicker. */
3556
+ animation: mfb-board-in 280ms ease-out 40ms both;
3557
+ }
3558
+ @media (prefers-reduced-motion: reduce) {
3559
+ .mine-list { animation: none; }
3560
+ }
2894
3561
  .mine-list-header {
2895
3562
  display: flex;
2896
3563
  align-items: center;
@@ -2930,9 +3597,19 @@ var WIDGET_STYLES = `
2930
3597
  flex-direction: column;
2931
3598
  gap: 4px;
2932
3599
  width: 100%;
2933
- transition: border-color 120ms ease, background 120ms ease;
3600
+ transition:
3601
+ border-color 140ms ease,
3602
+ background 140ms ease,
3603
+ transform 100ms ease,
3604
+ box-shadow 140ms ease;
3605
+ }
3606
+ .mine-row:hover {
3607
+ border-color: var(--mfb-border-strong);
3608
+ background: var(--mfb-bg);
3609
+ transform: translateY(-1px);
3610
+ box-shadow: var(--mfb-shadow);
2934
3611
  }
2935
- .mine-row:hover { border-color: var(--mfb-text-muted); }
3612
+ .mine-row:active { transform: scale(0.997); }
2936
3613
  .mine-row:focus-visible {
2937
3614
  outline: 2px solid var(--mfb-accent);
2938
3615
  outline-offset: 2px;
@@ -3300,7 +3977,7 @@ var WIDGET_STYLES = `
3300
3977
  background: var(--mfb-bg);
3301
3978
  border: 1px solid var(--mfb-border);
3302
3979
  border-radius: var(--mfb-radius);
3303
- padding: 8px 6px;
3980
+ padding: 10px 10px 10px 14px;
3304
3981
  cursor: pointer;
3305
3982
  display: flex;
3306
3983
  flex-direction: column;
@@ -3308,9 +3985,36 @@ var WIDGET_STYLES = `
3308
3985
  gap: 2px;
3309
3986
  font-family: inherit;
3310
3987
  color: inherit;
3311
- transition: background 120ms, border-color 120ms;
3988
+ position: relative;
3989
+ overflow: hidden;
3990
+ transition:
3991
+ background 140ms ease,
3992
+ border-color 140ms ease,
3993
+ transform 100ms ease,
3994
+ box-shadow 140ms ease;
3995
+ }
3996
+ .kpi-cell::before {
3997
+ /* Vertical tone accent \u2014 matches the Board KPI cards' visual rhythm so
3998
+ * the two surfaces feel like they belong to the same product. v0.12
3999
+ * polish pass. */
4000
+ content: '';
4001
+ position: absolute;
4002
+ top: 0;
4003
+ left: 0;
4004
+ width: 3px;
4005
+ height: 100%;
4006
+ background: var(--mfb-border);
4007
+ transition: background 140ms ease;
4008
+ }
4009
+ .kpi-cell--new::before { background: #3b82f6; }
4010
+ .kpi-cell--in_progress::before { background: #f59e0b; }
4011
+ .kpi-cell--awaiting_validation::before { background: #a855f7; }
4012
+ .kpi-cell--resolved::before { background: #10b981; }
4013
+ .kpi-cell:hover {
4014
+ background: var(--mfb-surface);
4015
+ transform: translateY(-1px);
4016
+ box-shadow: var(--mfb-shadow);
3312
4017
  }
3313
- .kpi-cell:hover { background: var(--mfb-surface); }
3314
4018
  .kpi-cell.is-active {
3315
4019
  border-color: var(--mfb-accent);
3316
4020
  background: rgba(59, 130, 246, 0.08);
@@ -3338,6 +4042,10 @@ var WIDGET_STYLES = `
3338
4042
  display: flex;
3339
4043
  flex-direction: column;
3340
4044
  gap: 12px;
4045
+ animation: mfb-board-in 280ms ease-out 40ms both;
4046
+ }
4047
+ @media (prefers-reduced-motion: reduce) {
4048
+ .changelog-groups { animation: none; }
3341
4049
  }
3342
4050
  .changelog-group {
3343
4051
  display: flex;
@@ -3369,10 +4077,437 @@ var WIDGET_STYLES = `
3369
4077
  font-variant-numeric: tabular-nums;
3370
4078
  font-weight: 600;
3371
4079
  }
4080
+
4081
+ /* ---- v0.12: Board tab + expanded modal ("transport" feel) ------------ */
4082
+
4083
+ /* When the Board tab is active, the modal grows toward near-fullscreen
4084
+ * on desktop and full-screen on mobile. The CSS transition gives the
4085
+ * sense of clicking through to a real page rather than switching tabs.
4086
+ * Width/height/max-* are animated together; padding too, since the
4087
+ * board's denser layout needs less of the modal's default padding. */
4088
+ .modal.is-expanded {
4089
+ width: min(1280px, calc(100vw - var(--mfb-space-5)));
4090
+ max-height: calc(100vh - var(--mfb-space-5));
4091
+ height: calc(100vh - var(--mfb-space-5));
4092
+ padding: var(--mfb-space-5);
4093
+ transition:
4094
+ width 320ms cubic-bezier(0.22, 1, 0.36, 1),
4095
+ height 320ms cubic-bezier(0.22, 1, 0.36, 1),
4096
+ max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
4097
+ padding 320ms cubic-bezier(0.22, 1, 0.36, 1);
4098
+ }
4099
+ @media (prefers-reduced-motion: reduce) {
4100
+ .modal.is-expanded { transition: none; }
4101
+ }
4102
+ .backdrop.is-expanded { /* hook for any backdrop-level overrides */ }
4103
+
4104
+ @media (max-width: 640px) {
4105
+ .modal.is-expanded {
4106
+ width: 100vw;
4107
+ height: calc(100vh - var(--mfb-space-5));
4108
+ border-radius: var(--mfb-radius-lg) var(--mfb-radius-lg) 0 0;
4109
+ }
4110
+ }
4111
+
4112
+ /* Decorative purple accent on the Board tab button so users discover
4113
+ * it's a different surface \u2014 subtle, not loud. */
4114
+ .tab-button--board.is-active {
4115
+ color: #6b21a8;
4116
+ box-shadow: inset 0 -2px 0 0 #a855f7;
4117
+ }
4118
+ .tab-button--board:not(.is-active):hover {
4119
+ color: #6b21a8;
4120
+ }
4121
+
4122
+ /* ---- Board layout ---- */
4123
+
4124
+ .board-view {
4125
+ display: flex;
4126
+ flex-direction: column;
4127
+ gap: var(--mfb-space-4);
4128
+ flex: 1 1 auto;
4129
+ min-height: 0; /* allow inner panes to scroll */
4130
+ animation: mfb-board-in 280ms ease-out 40ms both;
4131
+ }
4132
+ @keyframes mfb-board-in {
4133
+ from { opacity: 0; transform: translateY(8px); }
4134
+ to { opacity: 1; transform: translateY(0); }
4135
+ }
4136
+ @media (prefers-reduced-motion: reduce) {
4137
+ .board-view { animation: none; }
4138
+ }
4139
+
4140
+ .board-header {
4141
+ display: flex;
4142
+ flex-direction: column;
4143
+ gap: var(--mfb-space-4);
4144
+ }
4145
+ .board-header-title {
4146
+ display: flex;
4147
+ align-items: center;
4148
+ gap: 12px;
4149
+ }
4150
+ .board-header-emoji {
4151
+ font-size: 26px;
4152
+ line-height: 1;
4153
+ }
4154
+ .board-header-h {
4155
+ margin: 0;
4156
+ font-size: var(--mfb-text-2xl);
4157
+ font-weight: 600;
4158
+ letter-spacing: -0.01em;
4159
+ }
4160
+ .board-header-sub {
4161
+ margin: 2px 0 0 0;
4162
+ color: var(--mfb-text-muted);
4163
+ font-size: var(--mfb-text-sm);
4164
+ }
4165
+
4166
+ /* KPI strip \u2014 4 cards, equal width, subtle tone-tints by bucket. */
4167
+ .board-kpi-strip {
4168
+ display: grid;
4169
+ grid-template-columns: repeat(4, 1fr);
4170
+ gap: 12px;
4171
+ }
4172
+ .board-kpi-card {
4173
+ border: 1px solid var(--mfb-border);
4174
+ border-radius: var(--mfb-radius);
4175
+ padding: 14px 16px;
4176
+ background: var(--mfb-bg);
4177
+ display: flex;
4178
+ flex-direction: column;
4179
+ gap: 4px;
4180
+ position: relative;
4181
+ overflow: hidden;
4182
+ transition: transform 160ms ease, box-shadow 160ms ease;
4183
+ }
4184
+ .board-kpi-card::after {
4185
+ content: '';
4186
+ position: absolute;
4187
+ top: 0;
4188
+ left: 0;
4189
+ width: 3px;
4190
+ height: 100%;
4191
+ background: var(--mfb-border);
4192
+ transition: background 160ms ease;
4193
+ }
4194
+ .board-kpi-card:hover {
4195
+ transform: translateY(-1px);
4196
+ box-shadow: var(--mfb-shadow);
4197
+ }
4198
+ .board-kpi-card.tone-new::after { background: #3b82f6; }
4199
+ .board-kpi-card.tone-progress::after { background: #f59e0b; }
4200
+ .board-kpi-card.tone-validation::after { background: #a855f7; }
4201
+ .board-kpi-card.tone-rate::after { background: #10b981; }
4202
+ .board-kpi-value {
4203
+ font-size: 28px;
4204
+ font-weight: 700;
4205
+ line-height: 1;
4206
+ letter-spacing: -0.02em;
4207
+ color: var(--mfb-text);
4208
+ font-variant-numeric: tabular-nums;
4209
+ }
4210
+ .board-kpi-label {
4211
+ font-size: var(--mfb-text-xs);
4212
+ color: var(--mfb-text-muted);
4213
+ text-transform: uppercase;
4214
+ letter-spacing: 0.04em;
4215
+ font-weight: 500;
4216
+ }
4217
+ .board-kpi-strip.is-loading .board-kpi-value {
4218
+ opacity: 0.35;
4219
+ }
4220
+ @media (max-width: 720px) {
4221
+ .board-kpi-strip { grid-template-columns: repeat(2, 1fr); }
4222
+ }
4223
+
4224
+ /* Filter row */
4225
+ .board-filters {
4226
+ display: flex;
4227
+ align-items: center;
4228
+ gap: 8px;
4229
+ flex-wrap: wrap;
4230
+ }
4231
+ .board-filter-select,
4232
+ .board-filter-search {
4233
+ padding: 6px 10px;
4234
+ border: 1px solid var(--mfb-border);
4235
+ border-radius: var(--mfb-radius);
4236
+ background: var(--mfb-bg);
4237
+ color: var(--mfb-text);
4238
+ font: inherit;
4239
+ font-size: var(--mfb-text-sm);
4240
+ height: 32px;
4241
+ transition: border-color 120ms ease, box-shadow 120ms ease;
4242
+ }
4243
+ .board-filter-select:focus-visible,
4244
+ .board-filter-search:focus-visible {
4245
+ outline: none;
4246
+ border-color: var(--mfb-accent);
4247
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
4248
+ }
4249
+ .board-filter-search { min-width: 180px; flex: 1 1 220px; }
4250
+ .board-filter-toggle {
4251
+ display: inline-flex;
4252
+ align-items: center;
4253
+ gap: 6px;
4254
+ font-size: var(--mfb-text-sm);
4255
+ color: var(--mfb-text);
4256
+ cursor: pointer;
4257
+ user-select: none;
4258
+ }
4259
+ .board-filter-toggle input { margin: 0; }
4260
+ .board-filter-clear {
4261
+ background: transparent;
4262
+ border: 0;
4263
+ color: var(--mfb-text-muted);
4264
+ font: inherit;
4265
+ font-size: var(--mfb-text-sm);
4266
+ display: inline-flex;
4267
+ align-items: center;
4268
+ gap: 4px;
4269
+ cursor: pointer;
4270
+ padding: 4px 6px;
4271
+ border-radius: var(--mfb-radius);
4272
+ transition: background 120ms ease, color 120ms ease;
4273
+ }
4274
+ .board-filter-clear:hover {
4275
+ color: var(--mfb-text);
4276
+ background: var(--mfb-surface);
4277
+ }
4278
+
4279
+ /* Master/detail layout \u2014 two-column at >=900px, stacked below. */
4280
+ .board-body {
4281
+ display: grid;
4282
+ grid-template-columns: minmax(280px, 360px) 1fr;
4283
+ gap: var(--mfb-space-4);
4284
+ flex: 1 1 auto;
4285
+ min-height: 0;
4286
+ }
4287
+ @media (max-width: 900px) {
4288
+ .board-body { grid-template-columns: 1fr; }
4289
+ .board-detail-wrap:not(.has-selection) { display: none; }
4290
+ }
4291
+
4292
+ .board-list-wrap {
4293
+ display: flex;
4294
+ flex-direction: column;
4295
+ gap: 6px;
4296
+ min-height: 0;
4297
+ overflow: hidden;
4298
+ }
4299
+ .board-list-count {
4300
+ font-size: var(--mfb-text-xs);
4301
+ color: var(--mfb-text-muted);
4302
+ padding: 0 4px;
4303
+ }
4304
+ .board-list {
4305
+ list-style: none;
4306
+ margin: 0;
4307
+ padding: 0;
4308
+ display: flex;
4309
+ flex-direction: column;
4310
+ gap: 8px;
4311
+ overflow-y: auto;
4312
+ flex: 1 1 auto;
4313
+ scrollbar-width: thin;
4314
+ scrollbar-gutter: stable;
4315
+ }
4316
+
4317
+ .board-row {
4318
+ display: flex;
4319
+ flex-direction: column;
4320
+ gap: 6px;
4321
+ width: 100%;
4322
+ text-align: left;
4323
+ padding: 12px 14px;
4324
+ border-radius: var(--mfb-radius);
4325
+ background: var(--mfb-bg);
4326
+ border: 1px solid var(--mfb-border);
4327
+ cursor: pointer;
4328
+ font: inherit;
4329
+ color: inherit;
4330
+ /* Stagger reveal on first paint. */
4331
+ animation: mfb-row-in 280ms ease-out both;
4332
+ transition: border-color 140ms ease, background 140ms ease, transform 80ms ease;
4333
+ }
4334
+ .board-row:hover {
4335
+ border-color: var(--mfb-border-strong);
4336
+ background: var(--mfb-surface);
4337
+ }
4338
+ .board-row:active { transform: scale(0.997); }
4339
+ .board-row.is-selected {
4340
+ border-color: var(--mfb-accent);
4341
+ background: color-mix(in srgb, var(--mfb-accent) 6%, var(--mfb-bg));
4342
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
4343
+ }
4344
+ @keyframes mfb-row-in {
4345
+ from { opacity: 0; transform: translateY(4px); }
4346
+ to { opacity: 1; transform: translateY(0); }
4347
+ }
4348
+ @media (prefers-reduced-motion: reduce) {
4349
+ .board-row { animation: none; }
4350
+ }
4351
+ /* Stagger first 6 rows so the list reveals top-down. */
4352
+ .board-row:nth-child(1) { animation-delay: 0ms; }
4353
+ .board-row:nth-child(2) { animation-delay: 40ms; }
4354
+ .board-row:nth-child(3) { animation-delay: 80ms; }
4355
+ .board-row:nth-child(4) { animation-delay: 120ms; }
4356
+ .board-row:nth-child(5) { animation-delay: 160ms; }
4357
+ .board-row:nth-child(6) { animation-delay: 200ms; }
4358
+
4359
+ .board-row-badges {
4360
+ display: flex;
4361
+ gap: 6px;
4362
+ align-items: center;
4363
+ flex-wrap: wrap;
4364
+ }
4365
+ .board-row-description {
4366
+ font-size: var(--mfb-text-sm);
4367
+ color: var(--mfb-text);
4368
+ line-height: 1.4;
4369
+ display: -webkit-box;
4370
+ -webkit-line-clamp: 2;
4371
+ -webkit-box-orient: vertical;
4372
+ overflow: hidden;
4373
+ }
4374
+ .board-row-meta {
4375
+ display: flex;
4376
+ align-items: center;
4377
+ gap: 10px;
4378
+ font-size: var(--mfb-text-xs);
4379
+ color: var(--mfb-text-muted);
4380
+ flex-wrap: wrap;
4381
+ }
4382
+ .board-row-author { font-weight: 500; color: var(--mfb-text); }
4383
+ .board-row-comments {
4384
+ display: inline-flex;
4385
+ align-items: center;
4386
+ gap: 3px;
4387
+ font-variant-numeric: tabular-nums;
4388
+ }
4389
+ .board-row-time { margin-left: auto; font-variant-numeric: tabular-nums; }
4390
+
4391
+ /* Status / severity badges shared with the detail panel. The tones
4392
+ * borrow PNR's badge palette \u2014 calm pastels that don't fight content. */
4393
+ .board-row-badges .badge {
4394
+ font-size: 11px;
4395
+ font-weight: 500;
4396
+ letter-spacing: 0.02em;
4397
+ padding: 2px 8px;
4398
+ border-radius: 999px;
4399
+ background: #f3f4f6;
4400
+ color: #374151;
4401
+ text-transform: lowercase;
4402
+ }
4403
+ .badge.status-new { background: #dbeafe; color: #1e40af; }
4404
+ .badge.status-in_progress { background: #fef3c7; color: #92400e; }
4405
+ .badge.status-awaiting_validation { background: #ede9fe; color: #6b21a8; }
4406
+ .badge.status-closed { background: #d1fae5; color: #065f46; }
4407
+ .badge.status-rejected { background: #fee2e2; color: #991b1b; }
4408
+ .badge.status-duplicate { background: #f3f4f6; color: #4b5563; }
4409
+ .badge.status-wontfix { background: #f3f4f6; color: #4b5563; }
4410
+ .badge.severity-blocker { background: #fee2e2; color: #991b1b; }
4411
+ .badge.severity-high { background: #fee2e2; color: #b91c1c; }
4412
+ .badge.severity-medium { background: #fef3c7; color: #92400e; }
4413
+ .badge.severity-low { background: #e0f2fe; color: #075985; }
4414
+
4415
+ /* Detail pane (within Board). Inherits from .report-detail.variant-board */
4416
+ .board-detail-wrap {
4417
+ border: 1px solid var(--mfb-border);
4418
+ border-radius: var(--mfb-radius);
4419
+ background: var(--mfb-bg);
4420
+ overflow-y: auto;
4421
+ min-height: 0;
4422
+ display: flex;
4423
+ flex-direction: column;
4424
+ }
4425
+ .board-detail-empty {
4426
+ flex: 1 1 auto;
4427
+ display: flex;
4428
+ flex-direction: column;
4429
+ align-items: center;
4430
+ justify-content: center;
4431
+ gap: 12px;
4432
+ color: var(--mfb-text-muted);
4433
+ padding: var(--mfb-space-5);
4434
+ text-align: center;
4435
+ font-size: var(--mfb-text-sm);
4436
+ }
4437
+ .board-detail-empty svg { opacity: 0.6; }
4438
+
4439
+ .report-detail.variant-board { padding: 16px 18px; }
4440
+ .report-detail-header--board {
4441
+ align-items: center;
4442
+ }
4443
+ .report-detail-back {
4444
+ display: inline-flex;
4445
+ align-items: center;
4446
+ gap: 4px;
4447
+ height: 28px;
4448
+ padding: 0 10px;
4449
+ }
4450
+
4451
+ /* Empty + loading + error states */
4452
+ .board-empty {
4453
+ display: flex;
4454
+ flex-direction: column;
4455
+ align-items: center;
4456
+ justify-content: center;
4457
+ gap: 8px;
4458
+ text-align: center;
4459
+ padding: var(--mfb-space-5);
4460
+ color: var(--mfb-text-muted);
4461
+ flex: 1 1 auto;
4462
+ }
4463
+ .board-empty h3 {
4464
+ margin: 0;
4465
+ font-size: var(--mfb-text-base);
4466
+ color: var(--mfb-text);
4467
+ }
4468
+ .board-empty p {
4469
+ margin: 0;
4470
+ font-size: var(--mfb-text-sm);
4471
+ }
4472
+ .board-error {
4473
+ padding: 12px 14px;
4474
+ border-radius: var(--mfb-radius);
4475
+ background: #fef2f2;
4476
+ color: #991b1b;
4477
+ font-size: var(--mfb-text-sm);
4478
+ }
4479
+
4480
+ /* Skeleton rows pulse subtly so the user knows something's coming. */
4481
+ .board-list-skeleton .skeleton-row {
4482
+ cursor: default;
4483
+ pointer-events: none;
4484
+ }
4485
+ .skeleton-line {
4486
+ height: 12px;
4487
+ background: linear-gradient(
4488
+ 90deg,
4489
+ var(--mfb-surface) 0%,
4490
+ color-mix(in srgb, var(--mfb-surface) 60%, var(--mfb-border)) 50%,
4491
+ var(--mfb-surface) 100%
4492
+ );
4493
+ background-size: 200% 100%;
4494
+ border-radius: 4px;
4495
+ animation: mfb-skeleton 1400ms ease-in-out infinite;
4496
+ }
4497
+ .skeleton-badges { width: 60%; height: 14px; }
4498
+ .skeleton-text { width: 90%; }
4499
+ .skeleton-text.short { width: 60%; }
4500
+ @keyframes mfb-skeleton {
4501
+ from { background-position: 100% 0; }
4502
+ to { background-position: -100% 0; }
4503
+ }
4504
+ @media (prefers-reduced-motion: reduce) {
4505
+ .skeleton-line { animation: none; }
4506
+ }
3372
4507
  `;
3373
4508
 
3374
4509
  // src/widget/mount.tsx
3375
- import { Fragment as Fragment2, jsx as jsx11, jsxs as jsxs10 } from "preact/jsx-runtime";
4510
+ import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "preact/jsx-runtime";
3376
4511
  function mountWidget(options) {
3377
4512
  const shadow = options.host.attachShadow({ mode: "open" });
3378
4513
  const style = document.createElement("style");
@@ -3419,22 +4554,23 @@ function mountWidget(options) {
3419
4554
  const externalId = options.getExternalId?.();
3420
4555
  const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
3421
4556
  const showMineTab = Boolean(options.api && externalId);
3422
- return /* @__PURE__ */ jsxs10(Fragment2, { children: [
3423
- fabVisible && /* @__PURE__ */ jsx11(
4557
+ return /* @__PURE__ */ jsxs11(Fragment3, { children: [
4558
+ fabVisible && /* @__PURE__ */ jsx12(
3424
4559
  Fab,
3425
4560
  {
3426
4561
  label: options.strings["fab.label"],
3427
4562
  onClick: () => rerender({ ...currentState, open: true })
3428
4563
  }
3429
4564
  ),
3430
- state.open && /* @__PURE__ */ jsxs10(
4565
+ state.open && /* @__PURE__ */ jsxs11(
3431
4566
  Modal,
3432
4567
  {
3433
4568
  onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
3434
4569
  closeLabel: options.strings["form.close"],
4570
+ expanded: state.tab === "board",
3435
4571
  children: [
3436
- showMineTab && /* @__PURE__ */ jsxs10("div", { class: "tab-strip", role: "tablist", children: [
3437
- /* @__PURE__ */ jsx11(
4572
+ showMineTab && /* @__PURE__ */ jsxs11("div", { class: "tab-strip", role: "tablist", children: [
4573
+ /* @__PURE__ */ jsx12(
3438
4574
  "button",
3439
4575
  {
3440
4576
  type: "button",
@@ -3445,7 +4581,7 @@ function mountWidget(options) {
3445
4581
  children: options.strings["tab.send"]
3446
4582
  }
3447
4583
  ),
3448
- /* @__PURE__ */ jsx11(
4584
+ /* @__PURE__ */ jsx12(
3449
4585
  "button",
3450
4586
  {
3451
4587
  type: "button",
@@ -3456,7 +4592,7 @@ function mountWidget(options) {
3456
4592
  children: options.strings["tab.mine"]
3457
4593
  }
3458
4594
  ),
3459
- /* @__PURE__ */ jsx11(
4595
+ /* @__PURE__ */ jsx12(
3460
4596
  "button",
3461
4597
  {
3462
4598
  type: "button",
@@ -3466,9 +4602,20 @@ function mountWidget(options) {
3466
4602
  onClick: () => rerender(clearSelected({ ...currentState, tab: "changelog" })),
3467
4603
  children: options.strings["tab.changelog"]
3468
4604
  }
4605
+ ),
4606
+ /* @__PURE__ */ jsx12(
4607
+ "button",
4608
+ {
4609
+ type: "button",
4610
+ role: "tab",
4611
+ "aria-selected": state.tab === "board",
4612
+ class: `tab-button tab-button--board ${state.tab === "board" ? "is-active" : ""}`,
4613
+ onClick: () => rerender(clearSelected({ ...currentState, tab: "board" })),
4614
+ children: options.strings["tab.board"]
4615
+ }
3469
4616
  )
3470
4617
  ] }),
3471
- state.tab === "send" && /* @__PURE__ */ jsx11(
4618
+ state.tab === "send" && /* @__PURE__ */ jsx12(
3472
4619
  Form,
3473
4620
  {
3474
4621
  strings: options.strings,
@@ -3478,7 +4625,7 @@ function mountWidget(options) {
3478
4625
  ...state.error !== void 0 && { errorMessage: state.error }
3479
4626
  }
3480
4627
  ),
3481
- state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4628
+ state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3482
4629
  MineList,
3483
4630
  {
3484
4631
  api: options.api,
@@ -3487,7 +4634,7 @@ function mountWidget(options) {
3487
4634
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3488
4635
  }
3489
4636
  ),
3490
- (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx11(
4637
+ (state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx12(
3491
4638
  ReportDetailView,
3492
4639
  {
3493
4640
  api: options.api,
@@ -3497,7 +4644,7 @@ function mountWidget(options) {
3497
4644
  onBack: () => rerender(clearSelected({ ...currentState }))
3498
4645
  }
3499
4646
  ),
3500
- state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx11(
4647
+ state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
3501
4648
  ChangelogList,
3502
4649
  {
3503
4650
  api: options.api,
@@ -3505,6 +4652,16 @@ function mountWidget(options) {
3505
4652
  strings: options.strings,
3506
4653
  onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
3507
4654
  }
4655
+ ),
4656
+ state.tab === "board" && options.api && externalId && // BoardView owns its own master/detail navigation — no
4657
+ // selectedReportId routing through the modal-level state.
4658
+ /* @__PURE__ */ jsx12(
4659
+ BoardView,
4660
+ {
4661
+ api: options.api,
4662
+ externalId,
4663
+ strings: options.strings
4664
+ }
3508
4665
  )
3509
4666
  ]
3510
4667
  }
@@ -3558,15 +4715,13 @@ function createFeedback(config) {
3558
4715
  document.body.appendChild(host);
3559
4716
  }
3560
4717
  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
- });
4718
+ const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
3565
4719
  const technical_context = capture.snapshot();
3566
4720
  if (user) technical_context.user = user;
3567
4721
  if (metadata && Object.keys(metadata).length > 0) {
3568
4722
  technical_context.metadata = { ...metadata };
3569
4723
  }
4724
+ const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
3570
4725
  const payload = {
3571
4726
  description: values.description,
3572
4727
  feedback_type: values.feedback_type ?? "bug",
@@ -3574,13 +4729,13 @@ function createFeedback(config) {
3574
4729
  env,
3575
4730
  page_url: window.location.href,
3576
4731
  user_agent: navigator.userAgent,
3577
- capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
4732
+ capture_method: captureMethod,
3578
4733
  technical_context
3579
4734
  };
3580
- if ("0.11.0") {
3581
- payload.widget_version = "0.11.0";
4735
+ if ("0.12.1") {
4736
+ payload.widget_version = "0.12.1";
3582
4737
  }
3583
- if (screenshot) payload.screenshot = screenshot;
4738
+ if (manualScreenshot) payload.screenshot = manualScreenshot;
3584
4739
  if (values.synthetic) payload.synthetic = true;
3585
4740
  if (user?.id !== void 0 && user.id !== null && user.id !== "") {
3586
4741
  payload.user = {
@@ -3664,4 +4819,4 @@ function createFeedback(config) {
3664
4819
  export {
3665
4820
  createFeedback
3666
4821
  };
3667
- //# sourceMappingURL=chunk-AO3BXEG5.mjs.map
4822
+ //# sourceMappingURL=chunk-BKLI6IGE.mjs.map