@mhosaic/feedback 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-AO3BXEG5.mjs → chunk-XSYC5TDL.mjs} +1953 -817
- package/dist/chunk-XSYC5TDL.mjs.map +1 -0
- package/dist/embed.min.js +535 -23
- package/dist/embed.min.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/react.mjs +1 -1
- package/package.json +1 -1
- package/dist/chunk-AO3BXEG5.mjs.map +0 -1
|
@@ -134,7 +134,56 @@ function createApiClient(options) {
|
|
|
134
134
|
}
|
|
135
135
|
return response.json();
|
|
136
136
|
}
|
|
137
|
-
|
|
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/
|
|
762
|
-
import { useEffect, useMemo,
|
|
813
|
+
// src/widget/BoardView.tsx
|
|
814
|
+
import { useEffect as useEffect2, useMemo, useState as useState2 } from "preact/hooks";
|
|
763
815
|
|
|
764
|
-
// src/widget/
|
|
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
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return "
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
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
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
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
|
|
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
|
|
847
|
-
const
|
|
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 [
|
|
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
|
|
852
|
-
setRefreshing(true);
|
|
853
|
-
setError(null);
|
|
947
|
+
const fetchDetail = async () => {
|
|
854
948
|
try {
|
|
855
|
-
const next = await api.
|
|
949
|
+
const next = await api.getReport(reportId, externalId);
|
|
856
950
|
if (!mountedRef.current) return;
|
|
857
|
-
|
|
951
|
+
setDetail(next);
|
|
952
|
+
setError(null);
|
|
858
953
|
} catch (err) {
|
|
859
954
|
if (!mountedRef.current) return;
|
|
860
|
-
setError(err instanceof Error ? err.message :
|
|
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
|
|
960
|
+
void fetchDetail();
|
|
868
961
|
const timer = setInterval(() => {
|
|
869
|
-
void
|
|
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
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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
|
-
|
|
902
|
-
/* @__PURE__ */
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
929
|
-
|
|
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__ */
|
|
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
|
|
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
|
|
944
|
-
import { jsx as
|
|
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__ */
|
|
1067
|
-
arrow: /* @__PURE__ */
|
|
1068
|
-
pencil: /* @__PURE__ */
|
|
1069
|
-
text: /* @__PURE__ */
|
|
1070
|
-
highlight: /* @__PURE__ */
|
|
1071
|
-
/* @__PURE__ */
|
|
1072
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1075
|
-
/* @__PURE__ */
|
|
1076
|
-
/* @__PURE__ */
|
|
1077
|
-
/* @__PURE__ */
|
|
1078
|
-
/* @__PURE__ */
|
|
1079
|
-
/* @__PURE__ */
|
|
1080
|
-
/* @__PURE__ */
|
|
1081
|
-
/* @__PURE__ */
|
|
1082
|
-
/* @__PURE__ */
|
|
1083
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1086
|
-
redo: /* @__PURE__ */
|
|
1087
|
-
trash: /* @__PURE__ */
|
|
1088
|
-
close: /* @__PURE__ */
|
|
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 =
|
|
1092
|
-
const containerRef =
|
|
1093
|
-
const imageRef =
|
|
1094
|
-
const [tool, setTool] =
|
|
1095
|
-
const [color, setColor] =
|
|
1096
|
-
const [shapes, setShapes] =
|
|
1097
|
-
const [past, setPast] =
|
|
1098
|
-
const [future, setFuture] =
|
|
1099
|
-
const isDrawingRef =
|
|
1100
|
-
const [draftShape, setDraftShape] =
|
|
1101
|
-
const [imageLoaded, setImageLoaded] =
|
|
1102
|
-
const [saving, setSaving] =
|
|
1103
|
-
const scaleRef =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__ */
|
|
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__ */
|
|
1331
|
-
/* @__PURE__ */
|
|
1332
|
-
/* @__PURE__ */
|
|
1333
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1345
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1359
|
-
/* @__PURE__ */
|
|
1360
|
-
COLORS.map((c) => /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1383
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2258
|
+
/* @__PURE__ */ jsx7("span", { children: strings["annotator.undo"] })
|
|
1394
2259
|
]
|
|
1395
2260
|
}
|
|
1396
2261
|
),
|
|
1397
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2272
|
+
/* @__PURE__ */ jsx7("span", { children: strings["annotator.redo"] })
|
|
1408
2273
|
]
|
|
1409
2274
|
}
|
|
1410
2275
|
),
|
|
1411
|
-
/* @__PURE__ */
|
|
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__ */
|
|
2285
|
+
/* @__PURE__ */ jsx7("span", { children: strings["annotator.clear"] })
|
|
1421
2286
|
]
|
|
1422
2287
|
}
|
|
1423
2288
|
),
|
|
1424
|
-
/* @__PURE__ */
|
|
1425
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1443
|
-
/* @__PURE__ */
|
|
1444
|
-
/* @__PURE__ */
|
|
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
|
|
1487
|
-
var
|
|
1488
|
-
var
|
|
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] =
|
|
1491
|
-
const [feedbackType, setFeedbackType] =
|
|
1492
|
-
const [severity, setSeverity] =
|
|
1493
|
-
const [localError, setLocalError] =
|
|
1494
|
-
const [screenshotBlob, setScreenshotBlob] =
|
|
1495
|
-
const [screenshotPreview, setScreenshotPreview] =
|
|
1496
|
-
const [
|
|
1497
|
-
const [
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
2458
|
+
if (screenshotBlob) {
|
|
2459
|
+
values.screenshot = screenshotBlob;
|
|
2460
|
+
values.capture_method = screenshotMethod;
|
|
2461
|
+
}
|
|
1590
2462
|
onSubmit(values);
|
|
1591
2463
|
};
|
|
1592
|
-
return /* @__PURE__ */
|
|
1593
|
-
/* @__PURE__ */
|
|
1594
|
-
/* @__PURE__ */
|
|
1595
|
-
/* @__PURE__ */
|
|
1596
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1607
|
-
/* @__PURE__ */
|
|
1608
|
-
/* @__PURE__ */
|
|
1609
|
-
/* @__PURE__ */
|
|
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:
|
|
2487
|
+
children: TYPES2.map((t) => /* @__PURE__ */ jsx8("option", { value: t, children: strings[`type.${t}`] }))
|
|
1616
2488
|
}
|
|
1617
2489
|
)
|
|
1618
2490
|
] }),
|
|
1619
|
-
/* @__PURE__ */
|
|
1620
|
-
/* @__PURE__ */
|
|
1621
|
-
/* @__PURE__ */
|
|
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:
|
|
2499
|
+
children: SEVERITIES2.map((s) => /* @__PURE__ */ jsx8("option", { value: s, children: strings[`severity.${s}`] }))
|
|
1628
2500
|
}
|
|
1629
2501
|
)
|
|
1630
2502
|
] })
|
|
1631
2503
|
] }),
|
|
1632
|
-
/* @__PURE__ */
|
|
1633
|
-
/* @__PURE__ */
|
|
1634
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1647
|
-
/* @__PURE__ */
|
|
1648
|
-
/* @__PURE__ */
|
|
1649
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1657
|
-
/* @__PURE__ */
|
|
1658
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1665
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1689
|
-
/* @__PURE__ */
|
|
1690
|
-
/* @__PURE__ */
|
|
1691
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1694
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
1704
|
-
/* @__PURE__ */
|
|
1705
|
-
/* @__PURE__ */
|
|
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__ */
|
|
1708
|
-
status === "error" && errorMessage && /* @__PURE__ */
|
|
1709
|
-
status === "success" && /* @__PURE__ */
|
|
1710
|
-
/* @__PURE__ */
|
|
1711
|
-
/* @__PURE__ */
|
|
1712
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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
|
|
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
|
|
2638
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
|
|
1731
2639
|
function computeKpiCounts(rows) {
|
|
1732
2640
|
const counts = {
|
|
1733
2641
|
new: 0,
|
|
@@ -1794,468 +2702,151 @@ function KpiStrip({ rows, filter, onFilter, strings }) {
|
|
|
1794
2702
|
value: resolutionRate === null ? "\u2014" : `${resolutionRate}%`
|
|
1795
2703
|
}
|
|
1796
2704
|
];
|
|
1797
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
2708
|
+
return /* @__PURE__ */ jsxs8(
|
|
1801
2709
|
"button",
|
|
1802
2710
|
{
|
|
1803
2711
|
type: "button",
|
|
1804
2712
|
class: `kpi-cell kpi-cell--${c.key}${active ? " is-active" : ""}`,
|
|
1805
2713
|
onClick: () => onFilter(toggleTo),
|
|
1806
2714
|
"aria-pressed": active,
|
|
1807
|
-
children: [
|
|
1808
|
-
/* @__PURE__ */
|
|
1809
|
-
/* @__PURE__ */
|
|
1810
|
-
]
|
|
1811
|
-
}
|
|
1812
|
-
);
|
|
1813
|
-
}) });
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
// src/widget/MineList.tsx
|
|
1817
|
-
import { jsx as
|
|
1818
|
-
var
|
|
1819
|
-
function MineList({ api, externalId, strings, onSelect }) {
|
|
1820
|
-
const [rows, setRows] =
|
|
1821
|
-
const [error, setError] =
|
|
1822
|
-
const [refreshing, setRefreshing] =
|
|
1823
|
-
const [filter, setFilter] =
|
|
1824
|
-
const mountedRef =
|
|
1825
|
-
const fetchRows = async () => {
|
|
1826
|
-
setRefreshing(true);
|
|
1827
|
-
setError(null);
|
|
1828
|
-
try {
|
|
1829
|
-
const next = await api.listMine(externalId);
|
|
1830
|
-
if (!mountedRef.current) return;
|
|
1831
|
-
setRows(next);
|
|
1832
|
-
} catch (err) {
|
|
1833
|
-
if (!mountedRef.current) return;
|
|
1834
|
-
setError(err instanceof Error ? err.message : strings["mine.error"]);
|
|
1835
|
-
} finally {
|
|
1836
|
-
if (mountedRef.current) setRefreshing(false);
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
useEffect4(() => {
|
|
1840
|
-
mountedRef.current = true;
|
|
1841
|
-
void fetchRows();
|
|
1842
|
-
const timer = setInterval(() => {
|
|
1843
|
-
void fetchRows();
|
|
1844
|
-
}, POLL_MS2);
|
|
1845
|
-
return () => {
|
|
1846
|
-
mountedRef.current = false;
|
|
1847
|
-
clearInterval(timer);
|
|
1848
|
-
};
|
|
1849
|
-
}, [externalId]);
|
|
1850
|
-
const isEmpty = rows !== null && rows.length === 0;
|
|
1851
|
-
const isLoading = rows === null && !error;
|
|
1852
|
-
const visibleRows = rows ? rowsMatchingFilter(rows, filter) : null;
|
|
1853
|
-
const visibleEmpty = !!rows && rows.length > 0 && (visibleRows?.length ?? 0) === 0;
|
|
1854
|
-
return /* @__PURE__ */ jsxs6("div", { class: "mine-list", children: [
|
|
1855
|
-
/* @__PURE__ */ jsxs6("div", { class: "mine-list-header", children: [
|
|
1856
|
-
/* @__PURE__ */ jsx7("h2", { children: strings["tab.mine"] }),
|
|
1857
|
-
/* @__PURE__ */ jsx7(
|
|
1858
|
-
"button",
|
|
1859
|
-
{
|
|
1860
|
-
type: "button",
|
|
1861
|
-
class: "btn",
|
|
1862
|
-
onClick: () => {
|
|
1863
|
-
void fetchRows();
|
|
1864
|
-
},
|
|
1865
|
-
disabled: refreshing,
|
|
1866
|
-
children: refreshing ? strings["mine.loading"] : strings["mine.refresh"]
|
|
1867
|
-
}
|
|
1868
|
-
)
|
|
1869
|
-
] }),
|
|
1870
|
-
rows && rows.length > 0 && /* @__PURE__ */ jsx7(KpiStrip, { rows, filter, onFilter: setFilter, strings }),
|
|
1871
|
-
isLoading && /* @__PURE__ */ jsx7("div", { class: "mine-loading", children: strings["mine.loading"] }),
|
|
1872
|
-
error && /* @__PURE__ */ jsx7("div", { class: "error", children: error }),
|
|
1873
|
-
isEmpty && /* @__PURE__ */ jsxs6("div", { class: "mine-empty", children: [
|
|
1874
|
-
/* @__PURE__ */ jsx7("strong", { children: strings["mine.empty.title"] }),
|
|
1875
|
-
/* @__PURE__ */ jsx7("p", { children: strings["mine.empty.body"] })
|
|
1876
|
-
] }),
|
|
1877
|
-
visibleEmpty && /* @__PURE__ */ jsx7("div", { class: "mine-empty", children: /* @__PURE__ */ jsx7("p", { children: strings["mine.filter.empty"] }) }),
|
|
1878
|
-
visibleRows && visibleRows.length > 0 && /* @__PURE__ */ jsx7("ul", { class: "mine-rows", children: visibleRows.map((row) => /* @__PURE__ */ jsx7("li", { children: /* @__PURE__ */ jsx7(ReportRow, { row, strings, onClick: () => onSelect(row) }) })) })
|
|
1879
|
-
] });
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
// src/widget/Modal.tsx
|
|
1883
|
-
import { useEffect as useEffect5, useRef as useRef5 } from "preact/hooks";
|
|
1884
|
-
import { jsx as jsx8, jsxs as jsxs7 } from "preact/jsx-runtime";
|
|
1885
|
-
function Modal({ onDismiss, children, closeLabel = "Close" }) {
|
|
1886
|
-
const modalRef = useRef5(null);
|
|
1887
|
-
const previouslyFocused = useRef5(null);
|
|
1888
|
-
useEffect5(() => {
|
|
1889
|
-
previouslyFocused.current = document.activeElement;
|
|
1890
|
-
const onKey = (e) => {
|
|
1891
|
-
if (e.key !== "Escape") return;
|
|
1892
|
-
const root = modalRef.current?.getRootNode();
|
|
1893
|
-
if (root instanceof ShadowRoot && root.querySelector(".annotator-backdrop")) {
|
|
1894
|
-
return;
|
|
1895
|
-
}
|
|
1896
|
-
e.stopPropagation();
|
|
1897
|
-
onDismiss();
|
|
1898
|
-
};
|
|
1899
|
-
window.addEventListener("keydown", onKey);
|
|
1900
|
-
const first = modalRef.current?.querySelector(
|
|
1901
|
-
"textarea, input, select, button"
|
|
1902
|
-
);
|
|
1903
|
-
first?.focus();
|
|
1904
|
-
return () => {
|
|
1905
|
-
window.removeEventListener("keydown", onKey);
|
|
1906
|
-
const prev = previouslyFocused.current;
|
|
1907
|
-
if (prev && typeof prev.focus === "function") prev.focus();
|
|
1908
|
-
};
|
|
1909
|
-
}, [onDismiss]);
|
|
1910
|
-
return /* @__PURE__ */ jsx8(
|
|
1911
|
-
"div",
|
|
1912
|
-
{
|
|
1913
|
-
class: "backdrop",
|
|
1914
|
-
role: "presentation",
|
|
1915
|
-
onClick: (e) => {
|
|
1916
|
-
if (e.target === e.currentTarget) onDismiss();
|
|
1917
|
-
},
|
|
1918
|
-
children: /* @__PURE__ */ jsxs7("div", { ref: modalRef, class: "modal", role: "dialog", "aria-modal": "true", children: [
|
|
1919
|
-
/* @__PURE__ */ jsx8(
|
|
1920
|
-
"button",
|
|
1921
|
-
{
|
|
1922
|
-
type: "button",
|
|
1923
|
-
class: "modal-close",
|
|
1924
|
-
"aria-label": closeLabel,
|
|
1925
|
-
onClick: onDismiss,
|
|
1926
|
-
children: "\xD7"
|
|
1927
|
-
}
|
|
1928
|
-
),
|
|
1929
|
-
children
|
|
1930
|
-
] })
|
|
1931
|
-
}
|
|
1932
|
-
);
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
// src/widget/ReportDetailView.tsx
|
|
1936
|
-
import { useEffect as useEffect6, useRef as useRef6, useState as useState5 } from "preact/hooks";
|
|
1937
|
-
|
|
1938
|
-
// src/widget/CommentBubble.tsx
|
|
1939
|
-
import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
|
|
1940
|
-
function CommentBubble({ comment, strings }) {
|
|
1941
|
-
const isMine = comment.is_mine;
|
|
1942
|
-
const isAgent = !isMine && comment.author_source === "mcp";
|
|
1943
|
-
const isSystem = !isMine && comment.author_source === "system";
|
|
1944
|
-
const variant = isAgent ? "mcp" : isSystem ? "system" : "staff";
|
|
1945
|
-
const labelKey = variant === "mcp" ? "detail.author.mcp" : variant === "system" ? "detail.author.system" : "detail.author.staff";
|
|
1946
|
-
const label = comment.author_label || strings[labelKey];
|
|
1947
|
-
return /* @__PURE__ */ jsxs8("div", { class: `comment-bubble ${isMine ? "is-mine" : "is-other"}`, children: [
|
|
1948
|
-
!isMine && label && /* @__PURE__ */ jsx9("div", { class: `comment-author comment-author--${variant}`, children: label }),
|
|
1949
|
-
/* @__PURE__ */ jsx9("div", { class: "comment-body", children: comment.body }),
|
|
1950
|
-
/* @__PURE__ */ jsx9("div", { class: "comment-time", children: formatTime(comment.created_at) })
|
|
1951
|
-
] });
|
|
1952
|
-
}
|
|
1953
|
-
function formatTime(iso) {
|
|
1954
|
-
try {
|
|
1955
|
-
return new Date(iso).toLocaleString(void 0, {
|
|
1956
|
-
dateStyle: "short",
|
|
1957
|
-
timeStyle: "short"
|
|
1958
|
-
});
|
|
1959
|
-
} catch {
|
|
1960
|
-
return iso;
|
|
1961
|
-
}
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
// src/widget/ReportDetailView.tsx
|
|
1965
|
-
import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
|
|
1966
|
-
var POLL_MS3 = 3e4;
|
|
1967
|
-
function ReportDetailView({
|
|
1968
|
-
api,
|
|
1969
|
-
externalId,
|
|
1970
|
-
reportId,
|
|
1971
|
-
strings,
|
|
1972
|
-
onBack
|
|
1973
|
-
}) {
|
|
1974
|
-
const [detail, setDetail] = useState5(null);
|
|
1975
|
-
const [error, setError] = useState5(null);
|
|
1976
|
-
const [composeBody, setComposeBody] = useState5("");
|
|
1977
|
-
const [sending, setSending] = useState5(false);
|
|
1978
|
-
const [closing, setClosing] = useState5(false);
|
|
1979
|
-
const mountedRef = useRef6(true);
|
|
1980
|
-
const fetchDetail = async () => {
|
|
1981
|
-
try {
|
|
1982
|
-
const next = await api.getReport(reportId, externalId);
|
|
1983
|
-
if (!mountedRef.current) return;
|
|
1984
|
-
setDetail(next);
|
|
1985
|
-
setError(null);
|
|
1986
|
-
} catch (err) {
|
|
1987
|
-
if (!mountedRef.current) return;
|
|
1988
|
-
setError(err instanceof Error ? err.message : "load_failed");
|
|
1989
|
-
}
|
|
1990
|
-
};
|
|
1991
|
-
useEffect6(() => {
|
|
1992
|
-
mountedRef.current = true;
|
|
1993
|
-
void fetchDetail();
|
|
1994
|
-
const timer = setInterval(() => {
|
|
1995
|
-
void fetchDetail();
|
|
1996
|
-
}, POLL_MS3);
|
|
1997
|
-
return () => {
|
|
1998
|
-
mountedRef.current = false;
|
|
1999
|
-
clearInterval(timer);
|
|
2000
|
-
};
|
|
2001
|
-
}, [reportId, externalId]);
|
|
2002
|
-
const handleSend = async () => {
|
|
2003
|
-
if (!composeBody.trim() || sending) return;
|
|
2004
|
-
setSending(true);
|
|
2005
|
-
try {
|
|
2006
|
-
const nonce = `${reportId}:${Date.now()}`;
|
|
2007
|
-
const created = await api.addComment(reportId, externalId, composeBody.trim(), nonce);
|
|
2008
|
-
if (!mountedRef.current) return;
|
|
2009
|
-
setComposeBody("");
|
|
2010
|
-
setDetail(
|
|
2011
|
-
(prev) => prev ? { ...prev, comments: appendComment(prev.comments, created) } : prev
|
|
2012
|
-
);
|
|
2013
|
-
void fetchDetail();
|
|
2014
|
-
} catch (err) {
|
|
2015
|
-
if (!mountedRef.current) return;
|
|
2016
|
-
setError(err instanceof Error ? err.message : "comment_failed");
|
|
2017
|
-
} finally {
|
|
2018
|
-
if (mountedRef.current) setSending(false);
|
|
2019
|
-
}
|
|
2020
|
-
};
|
|
2021
|
-
const handleClose = async () => {
|
|
2022
|
-
if (closing) return;
|
|
2023
|
-
setClosing(true);
|
|
2715
|
+
children: [
|
|
2716
|
+
/* @__PURE__ */ jsx9("span", { class: "kpi-value", children: c.value }),
|
|
2717
|
+
/* @__PURE__ */ jsx9("span", { class: "kpi-label", children: c.label })
|
|
2718
|
+
]
|
|
2719
|
+
}
|
|
2720
|
+
);
|
|
2721
|
+
}) });
|
|
2722
|
+
}
|
|
2723
|
+
|
|
2724
|
+
// src/widget/MineList.tsx
|
|
2725
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
|
|
2726
|
+
var POLL_MS4 = 3e4;
|
|
2727
|
+
function MineList({ api, externalId, strings, onSelect }) {
|
|
2728
|
+
const [rows, setRows] = useState6(null);
|
|
2729
|
+
const [error, setError] = useState6(null);
|
|
2730
|
+
const [refreshing, setRefreshing] = useState6(false);
|
|
2731
|
+
const [filter, setFilter] = useState6("all");
|
|
2732
|
+
const mountedRef = useRef5(true);
|
|
2733
|
+
const fetchRows = async () => {
|
|
2734
|
+
setRefreshing(true);
|
|
2735
|
+
setError(null);
|
|
2024
2736
|
try {
|
|
2025
|
-
const next = await api.
|
|
2737
|
+
const next = await api.listMine(externalId);
|
|
2026
2738
|
if (!mountedRef.current) return;
|
|
2027
|
-
|
|
2739
|
+
setRows(next);
|
|
2028
2740
|
} catch (err) {
|
|
2029
2741
|
if (!mountedRef.current) return;
|
|
2030
|
-
setError(err instanceof Error ? err.message : "
|
|
2742
|
+
setError(err instanceof Error ? err.message : strings["mine.error"]);
|
|
2031
2743
|
} finally {
|
|
2032
|
-
if (mountedRef.current)
|
|
2744
|
+
if (mountedRef.current) setRefreshing(false);
|
|
2033
2745
|
}
|
|
2034
2746
|
};
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
/* @__PURE__ */ jsx10("
|
|
2053
|
-
|
|
2054
|
-
"
|
|
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
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
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__ */
|
|
2118
|
-
|
|
2119
|
-
|
|
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
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
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
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
children:
|
|
2829
|
+
ref: modalRef,
|
|
2830
|
+
class: `modal ${expanded ? "is-expanded" : ""}`,
|
|
2831
|
+
role: "dialog",
|
|
2832
|
+
"aria-modal": "true",
|
|
2833
|
+
children: [
|
|
2834
|
+
/* @__PURE__ */ jsx11(
|
|
2835
|
+
"button",
|
|
2836
|
+
{
|
|
2837
|
+
type: "button",
|
|
2838
|
+
class: "modal-close",
|
|
2839
|
+
"aria-label": closeLabel,
|
|
2840
|
+
onClick: onDismiss,
|
|
2841
|
+
children: "\xD7"
|
|
2842
|
+
}
|
|
2843
|
+
),
|
|
2844
|
+
children
|
|
2845
|
+
]
|
|
2131
2846
|
}
|
|
2132
2847
|
)
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
}
|
|
2136
|
-
function formatSubmittedAt(iso) {
|
|
2137
|
-
try {
|
|
2138
|
-
return new Date(iso).toLocaleString(void 0, {
|
|
2139
|
-
dateStyle: "medium",
|
|
2140
|
-
timeStyle: "short"
|
|
2141
|
-
});
|
|
2142
|
-
} catch {
|
|
2143
|
-
return iso;
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
var TECH_CONSOLE_LIMIT = 20;
|
|
2147
|
-
var TECH_NETWORK_LIMIT = 15;
|
|
2148
|
-
function TechnicalContextDrawer({ ctx, strings }) {
|
|
2149
|
-
const errors = ctx.errors ?? [];
|
|
2150
|
-
const consoleLogs = (ctx.consoleLogs ?? []).slice(-TECH_CONSOLE_LIMIT);
|
|
2151
|
-
const network = (ctx.networkRequests ?? []).slice(-TECH_NETWORK_LIMIT);
|
|
2152
|
-
const device = ctx.device;
|
|
2153
|
-
const hasAny = !!device || errors.length > 0 || consoleLogs.length > 0 || network.length > 0;
|
|
2154
|
-
if (!hasAny) return null;
|
|
2155
|
-
const errorCount = errors.length;
|
|
2156
|
-
const summary = errorCount > 0 ? `${strings["detail.tech.title"]} \xB7 ${errorCount} ${errorCount === 1 ? strings["detail.tech.errors_one"] : strings["detail.tech.errors_many"]}` : strings["detail.tech.title"];
|
|
2157
|
-
return /* @__PURE__ */ jsxs9("details", { class: "report-detail-tech", children: [
|
|
2158
|
-
/* @__PURE__ */ jsx10("summary", { children: summary }),
|
|
2159
|
-
/* @__PURE__ */ jsxs9("div", { class: "tech-body", children: [
|
|
2160
|
-
device && /* @__PURE__ */ jsx10(DeviceSection, { device, strings }),
|
|
2161
|
-
errors.length > 0 && /* @__PURE__ */ jsx10(ErrorsSection, { errors, strings }),
|
|
2162
|
-
consoleLogs.length > 0 && /* @__PURE__ */ jsx10(ConsoleSection, { logs: consoleLogs, strings }),
|
|
2163
|
-
network.length > 0 && /* @__PURE__ */ jsx10(NetworkSection, { rows: network, strings })
|
|
2164
|
-
] })
|
|
2165
|
-
] });
|
|
2166
|
-
}
|
|
2167
|
-
function DeviceSection({
|
|
2168
|
-
device,
|
|
2169
|
-
strings
|
|
2170
|
-
}) {
|
|
2171
|
-
const parts = [];
|
|
2172
|
-
if (device?.viewport) {
|
|
2173
|
-
const dpr = device.viewport.dpr ? ` @${device.viewport.dpr}x` : "";
|
|
2174
|
-
parts.push({
|
|
2175
|
-
label: strings["detail.tech.device.viewport"],
|
|
2176
|
-
value: `${device.viewport.w}\xD7${device.viewport.h}${dpr}`
|
|
2177
|
-
});
|
|
2178
|
-
}
|
|
2179
|
-
if (device?.platform) parts.push({ label: strings["detail.tech.device.platform"], value: device.platform });
|
|
2180
|
-
if (device?.language) parts.push({ label: strings["detail.tech.device.language"], value: device.language });
|
|
2181
|
-
if (device?.timezone) parts.push({ label: strings["detail.tech.device.timezone"], value: device.timezone });
|
|
2182
|
-
if (device?.connection) parts.push({ label: strings["detail.tech.device.connection"], value: device.connection });
|
|
2183
|
-
if (parts.length === 0) return null;
|
|
2184
|
-
return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
|
|
2185
|
-
/* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.device"] }),
|
|
2186
|
-
/* @__PURE__ */ jsx10("dl", { class: "tech-device", children: parts.map((p) => /* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
2187
|
-
/* @__PURE__ */ jsx10("dt", { children: p.label }),
|
|
2188
|
-
/* @__PURE__ */ jsx10("dd", { children: p.value })
|
|
2189
|
-
] })) })
|
|
2190
|
-
] });
|
|
2191
|
-
}
|
|
2192
|
-
function ErrorsSection({
|
|
2193
|
-
errors,
|
|
2194
|
-
strings
|
|
2195
|
-
}) {
|
|
2196
|
-
return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
|
|
2197
|
-
/* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.errors"] }),
|
|
2198
|
-
/* @__PURE__ */ jsx10("ul", { class: "tech-errors", children: errors.map((e) => /* @__PURE__ */ jsxs9("li", { children: [
|
|
2199
|
-
/* @__PURE__ */ jsx10("div", { class: "tech-errors-msg", children: e.message }),
|
|
2200
|
-
e.stack && /* @__PURE__ */ jsx10("pre", { class: "tech-errors-stack", children: truncateStack(e.stack) })
|
|
2201
|
-
] })) })
|
|
2202
|
-
] });
|
|
2203
|
-
}
|
|
2204
|
-
function ConsoleSection({
|
|
2205
|
-
logs,
|
|
2206
|
-
strings
|
|
2207
|
-
}) {
|
|
2208
|
-
return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
|
|
2209
|
-
/* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.console"] }),
|
|
2210
|
-
/* @__PURE__ */ jsx10("ul", { class: "tech-console", children: logs.map((l) => /* @__PURE__ */ jsxs9("li", { class: `tech-console-row tech-console-row--${l.level}`, children: [
|
|
2211
|
-
/* @__PURE__ */ jsx10("span", { class: "tech-console-level", children: l.level }),
|
|
2212
|
-
/* @__PURE__ */ jsx10("span", { class: "tech-console-msg", children: l.message })
|
|
2213
|
-
] })) })
|
|
2214
|
-
] });
|
|
2215
|
-
}
|
|
2216
|
-
function NetworkSection({
|
|
2217
|
-
rows,
|
|
2218
|
-
strings
|
|
2219
|
-
}) {
|
|
2220
|
-
return /* @__PURE__ */ jsxs9("div", { class: "tech-section", children: [
|
|
2221
|
-
/* @__PURE__ */ jsx10("h4", { children: strings["detail.tech.network"] }),
|
|
2222
|
-
/* @__PURE__ */ jsx10("ul", { class: "tech-network", children: rows.map((n) => {
|
|
2223
|
-
const failed = n.status >= 400 || n.status === 0;
|
|
2224
|
-
return /* @__PURE__ */ jsxs9("li", { class: `tech-network-row${failed ? " tech-network-row--fail" : ""}`, children: [
|
|
2225
|
-
/* @__PURE__ */ jsx10("span", { class: "tech-network-status", children: n.status || "\u2014" }),
|
|
2226
|
-
/* @__PURE__ */ jsx10("span", { class: "tech-network-method", children: n.method }),
|
|
2227
|
-
/* @__PURE__ */ jsx10("span", { class: "tech-network-url", title: n.url, children: truncateUrl(n.url, 56) }),
|
|
2228
|
-
/* @__PURE__ */ jsxs9("span", { class: "tech-network-time", children: [
|
|
2229
|
-
Math.round(n.durationMs),
|
|
2230
|
-
"ms"
|
|
2231
|
-
] })
|
|
2232
|
-
] });
|
|
2233
|
-
}) })
|
|
2234
|
-
] });
|
|
2235
|
-
}
|
|
2236
|
-
function truncateStack(stack) {
|
|
2237
|
-
const lines = stack.split("\n");
|
|
2238
|
-
if (lines.length <= 12) return stack;
|
|
2239
|
-
return lines.slice(0, 12).join("\n") + "\n \u2026";
|
|
2240
|
-
}
|
|
2241
|
-
function StatusHistorySection({ rows, strings }) {
|
|
2242
|
-
return /* @__PURE__ */ jsxs9("div", { class: "report-detail-history", children: [
|
|
2243
|
-
/* @__PURE__ */ jsx10("h3", { class: "report-detail-section", children: strings["detail.history"] }),
|
|
2244
|
-
/* @__PURE__ */ jsx10("ol", { class: "status-history", children: rows.map((r) => {
|
|
2245
|
-
const from = strings[`status.${r.from_status}`] ?? r.from_status;
|
|
2246
|
-
const to = strings[`status.${r.to_status}`] ?? r.to_status;
|
|
2247
|
-
const sourceKey = r.changed_by_source === "mcp" ? "detail.author.mcp" : r.changed_by_source === "system" ? "detail.author.system" : "detail.author.staff";
|
|
2248
|
-
return /* @__PURE__ */ jsxs9("li", { class: "status-history-row", children: [
|
|
2249
|
-
/* @__PURE__ */ jsx10("span", { class: "status-history-time", children: formatSubmittedAt(r.created_at) }),
|
|
2250
|
-
/* @__PURE__ */ jsxs9("span", { class: "status-history-transition", children: [
|
|
2251
|
-
/* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.from_status}`, children: from }),
|
|
2252
|
-
/* @__PURE__ */ jsx10("span", { class: "status-history-arrow", "aria-hidden": "true", children: "\u2192" }),
|
|
2253
|
-
/* @__PURE__ */ jsx10("span", { class: `pill pill-status pill-status--${r.to_status}`, children: to })
|
|
2254
|
-
] }),
|
|
2255
|
-
/* @__PURE__ */ jsx10("span", { class: `status-history-source status-history-source--${r.changed_by_source}`, children: strings[sourceKey] })
|
|
2256
|
-
] });
|
|
2257
|
-
}) })
|
|
2258
|
-
] });
|
|
2848
|
+
}
|
|
2849
|
+
);
|
|
2259
2850
|
}
|
|
2260
2851
|
|
|
2261
2852
|
// src/widget/styles.ts
|
|
@@ -2561,6 +3152,19 @@ var WIDGET_STYLES = `
|
|
|
2561
3152
|
border-color: color-mix(in srgb, var(--mfb-accent) 88%, black);
|
|
2562
3153
|
}
|
|
2563
3154
|
|
|
3155
|
+
/* Subdued button \u2014 borrows accent text but transparent background. Used
|
|
3156
|
+
* for the "Capture this page" alt-action that sits below the dropzone,
|
|
3157
|
+
* where a full --primary would compete with the form's main submit. */
|
|
3158
|
+
.btn--ghost {
|
|
3159
|
+
background: transparent;
|
|
3160
|
+
color: var(--mfb-accent);
|
|
3161
|
+
border-color: color-mix(in srgb, var(--mfb-accent) 40%, transparent);
|
|
3162
|
+
}
|
|
3163
|
+
.btn--ghost:hover {
|
|
3164
|
+
background: color-mix(in srgb, var(--mfb-accent) 8%, transparent);
|
|
3165
|
+
border-color: var(--mfb-accent);
|
|
3166
|
+
}
|
|
3167
|
+
|
|
2564
3168
|
.btn[disabled] { opacity: 0.6; cursor: not-allowed; }
|
|
2565
3169
|
|
|
2566
3170
|
.error { color: #dc2626; font-size: 13px; }
|
|
@@ -2624,6 +3228,40 @@ var WIDGET_STYLES = `
|
|
|
2624
3228
|
.screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
|
|
2625
3229
|
.screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
|
|
2626
3230
|
|
|
3231
|
+
/* "or \u2014 Capture this page" row sits below the dropzone. The thin "or"
|
|
3232
|
+
* separator borrows the pattern from auth forms so the second option
|
|
3233
|
+
* reads as an alternative, not a follow-up step. v0.12. */
|
|
3234
|
+
.screenshot-alt {
|
|
3235
|
+
display: flex;
|
|
3236
|
+
align-items: center;
|
|
3237
|
+
gap: 12px;
|
|
3238
|
+
margin-top: 8px;
|
|
3239
|
+
flex-wrap: wrap;
|
|
3240
|
+
}
|
|
3241
|
+
.screenshot-or {
|
|
3242
|
+
position: relative;
|
|
3243
|
+
font-size: var(--mfb-text-xs);
|
|
3244
|
+
color: var(--mfb-text-muted);
|
|
3245
|
+
text-transform: uppercase;
|
|
3246
|
+
letter-spacing: 0.08em;
|
|
3247
|
+
padding: 0 4px;
|
|
3248
|
+
}
|
|
3249
|
+
.screenshot-capture-page {
|
|
3250
|
+
display: inline-flex;
|
|
3251
|
+
align-items: center;
|
|
3252
|
+
gap: 6px;
|
|
3253
|
+
}
|
|
3254
|
+
.screenshot-capture-page:disabled {
|
|
3255
|
+
opacity: 0.55;
|
|
3256
|
+
cursor: progress;
|
|
3257
|
+
}
|
|
3258
|
+
.screenshot-capture-hint {
|
|
3259
|
+
font-size: var(--mfb-text-xs);
|
|
3260
|
+
color: var(--mfb-text-muted);
|
|
3261
|
+
flex: 1 1 100%;
|
|
3262
|
+
margin-top: 4px;
|
|
3263
|
+
}
|
|
3264
|
+
|
|
2627
3265
|
.screenshot-preview {
|
|
2628
3266
|
position: relative;
|
|
2629
3267
|
border: 1px solid var(--mfb-border);
|
|
@@ -2890,7 +3528,17 @@ var WIDGET_STYLES = `
|
|
|
2890
3528
|
}
|
|
2891
3529
|
.tab-button[aria-selected="true"] { font-weight: 600; }
|
|
2892
3530
|
|
|
2893
|
-
.mine-list {
|
|
3531
|
+
.mine-list {
|
|
3532
|
+
display: flex;
|
|
3533
|
+
flex-direction: column;
|
|
3534
|
+
gap: var(--mfb-space-3);
|
|
3535
|
+
/* Matches .board-view's entrance so switching tabs feels like a real
|
|
3536
|
+
* page transition rather than a flicker. */
|
|
3537
|
+
animation: mfb-board-in 280ms ease-out 40ms both;
|
|
3538
|
+
}
|
|
3539
|
+
@media (prefers-reduced-motion: reduce) {
|
|
3540
|
+
.mine-list { animation: none; }
|
|
3541
|
+
}
|
|
2894
3542
|
.mine-list-header {
|
|
2895
3543
|
display: flex;
|
|
2896
3544
|
align-items: center;
|
|
@@ -2930,9 +3578,19 @@ var WIDGET_STYLES = `
|
|
|
2930
3578
|
flex-direction: column;
|
|
2931
3579
|
gap: 4px;
|
|
2932
3580
|
width: 100%;
|
|
2933
|
-
transition:
|
|
3581
|
+
transition:
|
|
3582
|
+
border-color 140ms ease,
|
|
3583
|
+
background 140ms ease,
|
|
3584
|
+
transform 100ms ease,
|
|
3585
|
+
box-shadow 140ms ease;
|
|
3586
|
+
}
|
|
3587
|
+
.mine-row:hover {
|
|
3588
|
+
border-color: var(--mfb-border-strong);
|
|
3589
|
+
background: var(--mfb-bg);
|
|
3590
|
+
transform: translateY(-1px);
|
|
3591
|
+
box-shadow: var(--mfb-shadow);
|
|
2934
3592
|
}
|
|
2935
|
-
.mine-row:
|
|
3593
|
+
.mine-row:active { transform: scale(0.997); }
|
|
2936
3594
|
.mine-row:focus-visible {
|
|
2937
3595
|
outline: 2px solid var(--mfb-accent);
|
|
2938
3596
|
outline-offset: 2px;
|
|
@@ -3300,7 +3958,7 @@ var WIDGET_STYLES = `
|
|
|
3300
3958
|
background: var(--mfb-bg);
|
|
3301
3959
|
border: 1px solid var(--mfb-border);
|
|
3302
3960
|
border-radius: var(--mfb-radius);
|
|
3303
|
-
padding:
|
|
3961
|
+
padding: 10px 10px 10px 14px;
|
|
3304
3962
|
cursor: pointer;
|
|
3305
3963
|
display: flex;
|
|
3306
3964
|
flex-direction: column;
|
|
@@ -3308,9 +3966,36 @@ var WIDGET_STYLES = `
|
|
|
3308
3966
|
gap: 2px;
|
|
3309
3967
|
font-family: inherit;
|
|
3310
3968
|
color: inherit;
|
|
3311
|
-
|
|
3969
|
+
position: relative;
|
|
3970
|
+
overflow: hidden;
|
|
3971
|
+
transition:
|
|
3972
|
+
background 140ms ease,
|
|
3973
|
+
border-color 140ms ease,
|
|
3974
|
+
transform 100ms ease,
|
|
3975
|
+
box-shadow 140ms ease;
|
|
3976
|
+
}
|
|
3977
|
+
.kpi-cell::before {
|
|
3978
|
+
/* Vertical tone accent \u2014 matches the Board KPI cards' visual rhythm so
|
|
3979
|
+
* the two surfaces feel like they belong to the same product. v0.12
|
|
3980
|
+
* polish pass. */
|
|
3981
|
+
content: '';
|
|
3982
|
+
position: absolute;
|
|
3983
|
+
top: 0;
|
|
3984
|
+
left: 0;
|
|
3985
|
+
width: 3px;
|
|
3986
|
+
height: 100%;
|
|
3987
|
+
background: var(--mfb-border);
|
|
3988
|
+
transition: background 140ms ease;
|
|
3989
|
+
}
|
|
3990
|
+
.kpi-cell--new::before { background: #3b82f6; }
|
|
3991
|
+
.kpi-cell--in_progress::before { background: #f59e0b; }
|
|
3992
|
+
.kpi-cell--awaiting_validation::before { background: #a855f7; }
|
|
3993
|
+
.kpi-cell--resolved::before { background: #10b981; }
|
|
3994
|
+
.kpi-cell:hover {
|
|
3995
|
+
background: var(--mfb-surface);
|
|
3996
|
+
transform: translateY(-1px);
|
|
3997
|
+
box-shadow: var(--mfb-shadow);
|
|
3312
3998
|
}
|
|
3313
|
-
.kpi-cell:hover { background: var(--mfb-surface); }
|
|
3314
3999
|
.kpi-cell.is-active {
|
|
3315
4000
|
border-color: var(--mfb-accent);
|
|
3316
4001
|
background: rgba(59, 130, 246, 0.08);
|
|
@@ -3338,6 +4023,10 @@ var WIDGET_STYLES = `
|
|
|
3338
4023
|
display: flex;
|
|
3339
4024
|
flex-direction: column;
|
|
3340
4025
|
gap: 12px;
|
|
4026
|
+
animation: mfb-board-in 280ms ease-out 40ms both;
|
|
4027
|
+
}
|
|
4028
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4029
|
+
.changelog-groups { animation: none; }
|
|
3341
4030
|
}
|
|
3342
4031
|
.changelog-group {
|
|
3343
4032
|
display: flex;
|
|
@@ -3369,10 +4058,437 @@ var WIDGET_STYLES = `
|
|
|
3369
4058
|
font-variant-numeric: tabular-nums;
|
|
3370
4059
|
font-weight: 600;
|
|
3371
4060
|
}
|
|
4061
|
+
|
|
4062
|
+
/* ---- v0.12: Board tab + expanded modal ("transport" feel) ------------ */
|
|
4063
|
+
|
|
4064
|
+
/* When the Board tab is active, the modal grows toward near-fullscreen
|
|
4065
|
+
* on desktop and full-screen on mobile. The CSS transition gives the
|
|
4066
|
+
* sense of clicking through to a real page rather than switching tabs.
|
|
4067
|
+
* Width/height/max-* are animated together; padding too, since the
|
|
4068
|
+
* board's denser layout needs less of the modal's default padding. */
|
|
4069
|
+
.modal.is-expanded {
|
|
4070
|
+
width: min(1280px, calc(100vw - var(--mfb-space-5)));
|
|
4071
|
+
max-height: calc(100vh - var(--mfb-space-5));
|
|
4072
|
+
height: calc(100vh - var(--mfb-space-5));
|
|
4073
|
+
padding: var(--mfb-space-5);
|
|
4074
|
+
transition:
|
|
4075
|
+
width 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
4076
|
+
height 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
4077
|
+
max-height 320ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
4078
|
+
padding 320ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
4079
|
+
}
|
|
4080
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4081
|
+
.modal.is-expanded { transition: none; }
|
|
4082
|
+
}
|
|
4083
|
+
.backdrop.is-expanded { /* hook for any backdrop-level overrides */ }
|
|
4084
|
+
|
|
4085
|
+
@media (max-width: 640px) {
|
|
4086
|
+
.modal.is-expanded {
|
|
4087
|
+
width: 100vw;
|
|
4088
|
+
height: calc(100vh - var(--mfb-space-5));
|
|
4089
|
+
border-radius: var(--mfb-radius-lg) var(--mfb-radius-lg) 0 0;
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
|
|
4093
|
+
/* Decorative purple accent on the Board tab button so users discover
|
|
4094
|
+
* it's a different surface \u2014 subtle, not loud. */
|
|
4095
|
+
.tab-button--board.is-active {
|
|
4096
|
+
color: #6b21a8;
|
|
4097
|
+
box-shadow: inset 0 -2px 0 0 #a855f7;
|
|
4098
|
+
}
|
|
4099
|
+
.tab-button--board:not(.is-active):hover {
|
|
4100
|
+
color: #6b21a8;
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
/* ---- Board layout ---- */
|
|
4104
|
+
|
|
4105
|
+
.board-view {
|
|
4106
|
+
display: flex;
|
|
4107
|
+
flex-direction: column;
|
|
4108
|
+
gap: var(--mfb-space-4);
|
|
4109
|
+
flex: 1 1 auto;
|
|
4110
|
+
min-height: 0; /* allow inner panes to scroll */
|
|
4111
|
+
animation: mfb-board-in 280ms ease-out 40ms both;
|
|
4112
|
+
}
|
|
4113
|
+
@keyframes mfb-board-in {
|
|
4114
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
4115
|
+
to { opacity: 1; transform: translateY(0); }
|
|
4116
|
+
}
|
|
4117
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4118
|
+
.board-view { animation: none; }
|
|
4119
|
+
}
|
|
4120
|
+
|
|
4121
|
+
.board-header {
|
|
4122
|
+
display: flex;
|
|
4123
|
+
flex-direction: column;
|
|
4124
|
+
gap: var(--mfb-space-4);
|
|
4125
|
+
}
|
|
4126
|
+
.board-header-title {
|
|
4127
|
+
display: flex;
|
|
4128
|
+
align-items: center;
|
|
4129
|
+
gap: 12px;
|
|
4130
|
+
}
|
|
4131
|
+
.board-header-emoji {
|
|
4132
|
+
font-size: 26px;
|
|
4133
|
+
line-height: 1;
|
|
4134
|
+
}
|
|
4135
|
+
.board-header-h {
|
|
4136
|
+
margin: 0;
|
|
4137
|
+
font-size: var(--mfb-text-2xl);
|
|
4138
|
+
font-weight: 600;
|
|
4139
|
+
letter-spacing: -0.01em;
|
|
4140
|
+
}
|
|
4141
|
+
.board-header-sub {
|
|
4142
|
+
margin: 2px 0 0 0;
|
|
4143
|
+
color: var(--mfb-text-muted);
|
|
4144
|
+
font-size: var(--mfb-text-sm);
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
/* KPI strip \u2014 4 cards, equal width, subtle tone-tints by bucket. */
|
|
4148
|
+
.board-kpi-strip {
|
|
4149
|
+
display: grid;
|
|
4150
|
+
grid-template-columns: repeat(4, 1fr);
|
|
4151
|
+
gap: 12px;
|
|
4152
|
+
}
|
|
4153
|
+
.board-kpi-card {
|
|
4154
|
+
border: 1px solid var(--mfb-border);
|
|
4155
|
+
border-radius: var(--mfb-radius);
|
|
4156
|
+
padding: 14px 16px;
|
|
4157
|
+
background: var(--mfb-bg);
|
|
4158
|
+
display: flex;
|
|
4159
|
+
flex-direction: column;
|
|
4160
|
+
gap: 4px;
|
|
4161
|
+
position: relative;
|
|
4162
|
+
overflow: hidden;
|
|
4163
|
+
transition: transform 160ms ease, box-shadow 160ms ease;
|
|
4164
|
+
}
|
|
4165
|
+
.board-kpi-card::after {
|
|
4166
|
+
content: '';
|
|
4167
|
+
position: absolute;
|
|
4168
|
+
top: 0;
|
|
4169
|
+
left: 0;
|
|
4170
|
+
width: 3px;
|
|
4171
|
+
height: 100%;
|
|
4172
|
+
background: var(--mfb-border);
|
|
4173
|
+
transition: background 160ms ease;
|
|
4174
|
+
}
|
|
4175
|
+
.board-kpi-card:hover {
|
|
4176
|
+
transform: translateY(-1px);
|
|
4177
|
+
box-shadow: var(--mfb-shadow);
|
|
4178
|
+
}
|
|
4179
|
+
.board-kpi-card.tone-new::after { background: #3b82f6; }
|
|
4180
|
+
.board-kpi-card.tone-progress::after { background: #f59e0b; }
|
|
4181
|
+
.board-kpi-card.tone-validation::after { background: #a855f7; }
|
|
4182
|
+
.board-kpi-card.tone-rate::after { background: #10b981; }
|
|
4183
|
+
.board-kpi-value {
|
|
4184
|
+
font-size: 28px;
|
|
4185
|
+
font-weight: 700;
|
|
4186
|
+
line-height: 1;
|
|
4187
|
+
letter-spacing: -0.02em;
|
|
4188
|
+
color: var(--mfb-text);
|
|
4189
|
+
font-variant-numeric: tabular-nums;
|
|
4190
|
+
}
|
|
4191
|
+
.board-kpi-label {
|
|
4192
|
+
font-size: var(--mfb-text-xs);
|
|
4193
|
+
color: var(--mfb-text-muted);
|
|
4194
|
+
text-transform: uppercase;
|
|
4195
|
+
letter-spacing: 0.04em;
|
|
4196
|
+
font-weight: 500;
|
|
4197
|
+
}
|
|
4198
|
+
.board-kpi-strip.is-loading .board-kpi-value {
|
|
4199
|
+
opacity: 0.35;
|
|
4200
|
+
}
|
|
4201
|
+
@media (max-width: 720px) {
|
|
4202
|
+
.board-kpi-strip { grid-template-columns: repeat(2, 1fr); }
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
/* Filter row */
|
|
4206
|
+
.board-filters {
|
|
4207
|
+
display: flex;
|
|
4208
|
+
align-items: center;
|
|
4209
|
+
gap: 8px;
|
|
4210
|
+
flex-wrap: wrap;
|
|
4211
|
+
}
|
|
4212
|
+
.board-filter-select,
|
|
4213
|
+
.board-filter-search {
|
|
4214
|
+
padding: 6px 10px;
|
|
4215
|
+
border: 1px solid var(--mfb-border);
|
|
4216
|
+
border-radius: var(--mfb-radius);
|
|
4217
|
+
background: var(--mfb-bg);
|
|
4218
|
+
color: var(--mfb-text);
|
|
4219
|
+
font: inherit;
|
|
4220
|
+
font-size: var(--mfb-text-sm);
|
|
4221
|
+
height: 32px;
|
|
4222
|
+
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
4223
|
+
}
|
|
4224
|
+
.board-filter-select:focus-visible,
|
|
4225
|
+
.board-filter-search:focus-visible {
|
|
4226
|
+
outline: none;
|
|
4227
|
+
border-color: var(--mfb-accent);
|
|
4228
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
|
|
4229
|
+
}
|
|
4230
|
+
.board-filter-search { min-width: 180px; flex: 1 1 220px; }
|
|
4231
|
+
.board-filter-toggle {
|
|
4232
|
+
display: inline-flex;
|
|
4233
|
+
align-items: center;
|
|
4234
|
+
gap: 6px;
|
|
4235
|
+
font-size: var(--mfb-text-sm);
|
|
4236
|
+
color: var(--mfb-text);
|
|
4237
|
+
cursor: pointer;
|
|
4238
|
+
user-select: none;
|
|
4239
|
+
}
|
|
4240
|
+
.board-filter-toggle input { margin: 0; }
|
|
4241
|
+
.board-filter-clear {
|
|
4242
|
+
background: transparent;
|
|
4243
|
+
border: 0;
|
|
4244
|
+
color: var(--mfb-text-muted);
|
|
4245
|
+
font: inherit;
|
|
4246
|
+
font-size: var(--mfb-text-sm);
|
|
4247
|
+
display: inline-flex;
|
|
4248
|
+
align-items: center;
|
|
4249
|
+
gap: 4px;
|
|
4250
|
+
cursor: pointer;
|
|
4251
|
+
padding: 4px 6px;
|
|
4252
|
+
border-radius: var(--mfb-radius);
|
|
4253
|
+
transition: background 120ms ease, color 120ms ease;
|
|
4254
|
+
}
|
|
4255
|
+
.board-filter-clear:hover {
|
|
4256
|
+
color: var(--mfb-text);
|
|
4257
|
+
background: var(--mfb-surface);
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4260
|
+
/* Master/detail layout \u2014 two-column at >=900px, stacked below. */
|
|
4261
|
+
.board-body {
|
|
4262
|
+
display: grid;
|
|
4263
|
+
grid-template-columns: minmax(280px, 360px) 1fr;
|
|
4264
|
+
gap: var(--mfb-space-4);
|
|
4265
|
+
flex: 1 1 auto;
|
|
4266
|
+
min-height: 0;
|
|
4267
|
+
}
|
|
4268
|
+
@media (max-width: 900px) {
|
|
4269
|
+
.board-body { grid-template-columns: 1fr; }
|
|
4270
|
+
.board-detail-wrap:not(.has-selection) { display: none; }
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
.board-list-wrap {
|
|
4274
|
+
display: flex;
|
|
4275
|
+
flex-direction: column;
|
|
4276
|
+
gap: 6px;
|
|
4277
|
+
min-height: 0;
|
|
4278
|
+
overflow: hidden;
|
|
4279
|
+
}
|
|
4280
|
+
.board-list-count {
|
|
4281
|
+
font-size: var(--mfb-text-xs);
|
|
4282
|
+
color: var(--mfb-text-muted);
|
|
4283
|
+
padding: 0 4px;
|
|
4284
|
+
}
|
|
4285
|
+
.board-list {
|
|
4286
|
+
list-style: none;
|
|
4287
|
+
margin: 0;
|
|
4288
|
+
padding: 0;
|
|
4289
|
+
display: flex;
|
|
4290
|
+
flex-direction: column;
|
|
4291
|
+
gap: 8px;
|
|
4292
|
+
overflow-y: auto;
|
|
4293
|
+
flex: 1 1 auto;
|
|
4294
|
+
scrollbar-width: thin;
|
|
4295
|
+
scrollbar-gutter: stable;
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
.board-row {
|
|
4299
|
+
display: flex;
|
|
4300
|
+
flex-direction: column;
|
|
4301
|
+
gap: 6px;
|
|
4302
|
+
width: 100%;
|
|
4303
|
+
text-align: left;
|
|
4304
|
+
padding: 12px 14px;
|
|
4305
|
+
border-radius: var(--mfb-radius);
|
|
4306
|
+
background: var(--mfb-bg);
|
|
4307
|
+
border: 1px solid var(--mfb-border);
|
|
4308
|
+
cursor: pointer;
|
|
4309
|
+
font: inherit;
|
|
4310
|
+
color: inherit;
|
|
4311
|
+
/* Stagger reveal on first paint. */
|
|
4312
|
+
animation: mfb-row-in 280ms ease-out both;
|
|
4313
|
+
transition: border-color 140ms ease, background 140ms ease, transform 80ms ease;
|
|
4314
|
+
}
|
|
4315
|
+
.board-row:hover {
|
|
4316
|
+
border-color: var(--mfb-border-strong);
|
|
4317
|
+
background: var(--mfb-surface);
|
|
4318
|
+
}
|
|
4319
|
+
.board-row:active { transform: scale(0.997); }
|
|
4320
|
+
.board-row.is-selected {
|
|
4321
|
+
border-color: var(--mfb-accent);
|
|
4322
|
+
background: color-mix(in srgb, var(--mfb-accent) 6%, var(--mfb-bg));
|
|
4323
|
+
box-shadow: 0 0 0 2px color-mix(in srgb, var(--mfb-accent) 18%, transparent);
|
|
4324
|
+
}
|
|
4325
|
+
@keyframes mfb-row-in {
|
|
4326
|
+
from { opacity: 0; transform: translateY(4px); }
|
|
4327
|
+
to { opacity: 1; transform: translateY(0); }
|
|
4328
|
+
}
|
|
4329
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4330
|
+
.board-row { animation: none; }
|
|
4331
|
+
}
|
|
4332
|
+
/* Stagger first 6 rows so the list reveals top-down. */
|
|
4333
|
+
.board-row:nth-child(1) { animation-delay: 0ms; }
|
|
4334
|
+
.board-row:nth-child(2) { animation-delay: 40ms; }
|
|
4335
|
+
.board-row:nth-child(3) { animation-delay: 80ms; }
|
|
4336
|
+
.board-row:nth-child(4) { animation-delay: 120ms; }
|
|
4337
|
+
.board-row:nth-child(5) { animation-delay: 160ms; }
|
|
4338
|
+
.board-row:nth-child(6) { animation-delay: 200ms; }
|
|
4339
|
+
|
|
4340
|
+
.board-row-badges {
|
|
4341
|
+
display: flex;
|
|
4342
|
+
gap: 6px;
|
|
4343
|
+
align-items: center;
|
|
4344
|
+
flex-wrap: wrap;
|
|
4345
|
+
}
|
|
4346
|
+
.board-row-description {
|
|
4347
|
+
font-size: var(--mfb-text-sm);
|
|
4348
|
+
color: var(--mfb-text);
|
|
4349
|
+
line-height: 1.4;
|
|
4350
|
+
display: -webkit-box;
|
|
4351
|
+
-webkit-line-clamp: 2;
|
|
4352
|
+
-webkit-box-orient: vertical;
|
|
4353
|
+
overflow: hidden;
|
|
4354
|
+
}
|
|
4355
|
+
.board-row-meta {
|
|
4356
|
+
display: flex;
|
|
4357
|
+
align-items: center;
|
|
4358
|
+
gap: 10px;
|
|
4359
|
+
font-size: var(--mfb-text-xs);
|
|
4360
|
+
color: var(--mfb-text-muted);
|
|
4361
|
+
flex-wrap: wrap;
|
|
4362
|
+
}
|
|
4363
|
+
.board-row-author { font-weight: 500; color: var(--mfb-text); }
|
|
4364
|
+
.board-row-comments {
|
|
4365
|
+
display: inline-flex;
|
|
4366
|
+
align-items: center;
|
|
4367
|
+
gap: 3px;
|
|
4368
|
+
font-variant-numeric: tabular-nums;
|
|
4369
|
+
}
|
|
4370
|
+
.board-row-time { margin-left: auto; font-variant-numeric: tabular-nums; }
|
|
4371
|
+
|
|
4372
|
+
/* Status / severity badges shared with the detail panel. The tones
|
|
4373
|
+
* borrow PNR's badge palette \u2014 calm pastels that don't fight content. */
|
|
4374
|
+
.board-row-badges .badge {
|
|
4375
|
+
font-size: 11px;
|
|
4376
|
+
font-weight: 500;
|
|
4377
|
+
letter-spacing: 0.02em;
|
|
4378
|
+
padding: 2px 8px;
|
|
4379
|
+
border-radius: 999px;
|
|
4380
|
+
background: #f3f4f6;
|
|
4381
|
+
color: #374151;
|
|
4382
|
+
text-transform: lowercase;
|
|
4383
|
+
}
|
|
4384
|
+
.badge.status-new { background: #dbeafe; color: #1e40af; }
|
|
4385
|
+
.badge.status-in_progress { background: #fef3c7; color: #92400e; }
|
|
4386
|
+
.badge.status-awaiting_validation { background: #ede9fe; color: #6b21a8; }
|
|
4387
|
+
.badge.status-closed { background: #d1fae5; color: #065f46; }
|
|
4388
|
+
.badge.status-rejected { background: #fee2e2; color: #991b1b; }
|
|
4389
|
+
.badge.status-duplicate { background: #f3f4f6; color: #4b5563; }
|
|
4390
|
+
.badge.status-wontfix { background: #f3f4f6; color: #4b5563; }
|
|
4391
|
+
.badge.severity-blocker { background: #fee2e2; color: #991b1b; }
|
|
4392
|
+
.badge.severity-high { background: #fee2e2; color: #b91c1c; }
|
|
4393
|
+
.badge.severity-medium { background: #fef3c7; color: #92400e; }
|
|
4394
|
+
.badge.severity-low { background: #e0f2fe; color: #075985; }
|
|
4395
|
+
|
|
4396
|
+
/* Detail pane (within Board). Inherits from .report-detail.variant-board */
|
|
4397
|
+
.board-detail-wrap {
|
|
4398
|
+
border: 1px solid var(--mfb-border);
|
|
4399
|
+
border-radius: var(--mfb-radius);
|
|
4400
|
+
background: var(--mfb-bg);
|
|
4401
|
+
overflow-y: auto;
|
|
4402
|
+
min-height: 0;
|
|
4403
|
+
display: flex;
|
|
4404
|
+
flex-direction: column;
|
|
4405
|
+
}
|
|
4406
|
+
.board-detail-empty {
|
|
4407
|
+
flex: 1 1 auto;
|
|
4408
|
+
display: flex;
|
|
4409
|
+
flex-direction: column;
|
|
4410
|
+
align-items: center;
|
|
4411
|
+
justify-content: center;
|
|
4412
|
+
gap: 12px;
|
|
4413
|
+
color: var(--mfb-text-muted);
|
|
4414
|
+
padding: var(--mfb-space-5);
|
|
4415
|
+
text-align: center;
|
|
4416
|
+
font-size: var(--mfb-text-sm);
|
|
4417
|
+
}
|
|
4418
|
+
.board-detail-empty svg { opacity: 0.6; }
|
|
4419
|
+
|
|
4420
|
+
.report-detail.variant-board { padding: 16px 18px; }
|
|
4421
|
+
.report-detail-header--board {
|
|
4422
|
+
align-items: center;
|
|
4423
|
+
}
|
|
4424
|
+
.report-detail-back {
|
|
4425
|
+
display: inline-flex;
|
|
4426
|
+
align-items: center;
|
|
4427
|
+
gap: 4px;
|
|
4428
|
+
height: 28px;
|
|
4429
|
+
padding: 0 10px;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
/* Empty + loading + error states */
|
|
4433
|
+
.board-empty {
|
|
4434
|
+
display: flex;
|
|
4435
|
+
flex-direction: column;
|
|
4436
|
+
align-items: center;
|
|
4437
|
+
justify-content: center;
|
|
4438
|
+
gap: 8px;
|
|
4439
|
+
text-align: center;
|
|
4440
|
+
padding: var(--mfb-space-5);
|
|
4441
|
+
color: var(--mfb-text-muted);
|
|
4442
|
+
flex: 1 1 auto;
|
|
4443
|
+
}
|
|
4444
|
+
.board-empty h3 {
|
|
4445
|
+
margin: 0;
|
|
4446
|
+
font-size: var(--mfb-text-base);
|
|
4447
|
+
color: var(--mfb-text);
|
|
4448
|
+
}
|
|
4449
|
+
.board-empty p {
|
|
4450
|
+
margin: 0;
|
|
4451
|
+
font-size: var(--mfb-text-sm);
|
|
4452
|
+
}
|
|
4453
|
+
.board-error {
|
|
4454
|
+
padding: 12px 14px;
|
|
4455
|
+
border-radius: var(--mfb-radius);
|
|
4456
|
+
background: #fef2f2;
|
|
4457
|
+
color: #991b1b;
|
|
4458
|
+
font-size: var(--mfb-text-sm);
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
/* Skeleton rows pulse subtly so the user knows something's coming. */
|
|
4462
|
+
.board-list-skeleton .skeleton-row {
|
|
4463
|
+
cursor: default;
|
|
4464
|
+
pointer-events: none;
|
|
4465
|
+
}
|
|
4466
|
+
.skeleton-line {
|
|
4467
|
+
height: 12px;
|
|
4468
|
+
background: linear-gradient(
|
|
4469
|
+
90deg,
|
|
4470
|
+
var(--mfb-surface) 0%,
|
|
4471
|
+
color-mix(in srgb, var(--mfb-surface) 60%, var(--mfb-border)) 50%,
|
|
4472
|
+
var(--mfb-surface) 100%
|
|
4473
|
+
);
|
|
4474
|
+
background-size: 200% 100%;
|
|
4475
|
+
border-radius: 4px;
|
|
4476
|
+
animation: mfb-skeleton 1400ms ease-in-out infinite;
|
|
4477
|
+
}
|
|
4478
|
+
.skeleton-badges { width: 60%; height: 14px; }
|
|
4479
|
+
.skeleton-text { width: 90%; }
|
|
4480
|
+
.skeleton-text.short { width: 60%; }
|
|
4481
|
+
@keyframes mfb-skeleton {
|
|
4482
|
+
from { background-position: 100% 0; }
|
|
4483
|
+
to { background-position: -100% 0; }
|
|
4484
|
+
}
|
|
4485
|
+
@media (prefers-reduced-motion: reduce) {
|
|
4486
|
+
.skeleton-line { animation: none; }
|
|
4487
|
+
}
|
|
3372
4488
|
`;
|
|
3373
4489
|
|
|
3374
4490
|
// src/widget/mount.tsx
|
|
3375
|
-
import { Fragment as
|
|
4491
|
+
import { Fragment as Fragment3, jsx as jsx12, jsxs as jsxs11 } from "preact/jsx-runtime";
|
|
3376
4492
|
function mountWidget(options) {
|
|
3377
4493
|
const shadow = options.host.attachShadow({ mode: "open" });
|
|
3378
4494
|
const style = document.createElement("style");
|
|
@@ -3419,22 +4535,23 @@ function mountWidget(options) {
|
|
|
3419
4535
|
const externalId = options.getExternalId?.();
|
|
3420
4536
|
const fabVisible = options.showFAB && (options.getExternalId === void 0 ? true : Boolean(externalId));
|
|
3421
4537
|
const showMineTab = Boolean(options.api && externalId);
|
|
3422
|
-
return /* @__PURE__ */
|
|
3423
|
-
fabVisible && /* @__PURE__ */
|
|
4538
|
+
return /* @__PURE__ */ jsxs11(Fragment3, { children: [
|
|
4539
|
+
fabVisible && /* @__PURE__ */ jsx12(
|
|
3424
4540
|
Fab,
|
|
3425
4541
|
{
|
|
3426
4542
|
label: options.strings["fab.label"],
|
|
3427
4543
|
onClick: () => rerender({ ...currentState, open: true })
|
|
3428
4544
|
}
|
|
3429
4545
|
),
|
|
3430
|
-
state.open && /* @__PURE__ */
|
|
4546
|
+
state.open && /* @__PURE__ */ jsxs11(
|
|
3431
4547
|
Modal,
|
|
3432
4548
|
{
|
|
3433
4549
|
onDismiss: () => rerender(clearSelected({ ...currentState, open: false, status: "idle" })),
|
|
3434
4550
|
closeLabel: options.strings["form.close"],
|
|
4551
|
+
expanded: state.tab === "board",
|
|
3435
4552
|
children: [
|
|
3436
|
-
showMineTab && /* @__PURE__ */
|
|
3437
|
-
/* @__PURE__ */
|
|
4553
|
+
showMineTab && /* @__PURE__ */ jsxs11("div", { class: "tab-strip", role: "tablist", children: [
|
|
4554
|
+
/* @__PURE__ */ jsx12(
|
|
3438
4555
|
"button",
|
|
3439
4556
|
{
|
|
3440
4557
|
type: "button",
|
|
@@ -3445,7 +4562,7 @@ function mountWidget(options) {
|
|
|
3445
4562
|
children: options.strings["tab.send"]
|
|
3446
4563
|
}
|
|
3447
4564
|
),
|
|
3448
|
-
/* @__PURE__ */
|
|
4565
|
+
/* @__PURE__ */ jsx12(
|
|
3449
4566
|
"button",
|
|
3450
4567
|
{
|
|
3451
4568
|
type: "button",
|
|
@@ -3456,7 +4573,7 @@ function mountWidget(options) {
|
|
|
3456
4573
|
children: options.strings["tab.mine"]
|
|
3457
4574
|
}
|
|
3458
4575
|
),
|
|
3459
|
-
/* @__PURE__ */
|
|
4576
|
+
/* @__PURE__ */ jsx12(
|
|
3460
4577
|
"button",
|
|
3461
4578
|
{
|
|
3462
4579
|
type: "button",
|
|
@@ -3466,9 +4583,20 @@ function mountWidget(options) {
|
|
|
3466
4583
|
onClick: () => rerender(clearSelected({ ...currentState, tab: "changelog" })),
|
|
3467
4584
|
children: options.strings["tab.changelog"]
|
|
3468
4585
|
}
|
|
4586
|
+
),
|
|
4587
|
+
/* @__PURE__ */ jsx12(
|
|
4588
|
+
"button",
|
|
4589
|
+
{
|
|
4590
|
+
type: "button",
|
|
4591
|
+
role: "tab",
|
|
4592
|
+
"aria-selected": state.tab === "board",
|
|
4593
|
+
class: `tab-button tab-button--board ${state.tab === "board" ? "is-active" : ""}`,
|
|
4594
|
+
onClick: () => rerender(clearSelected({ ...currentState, tab: "board" })),
|
|
4595
|
+
children: options.strings["tab.board"]
|
|
4596
|
+
}
|
|
3469
4597
|
)
|
|
3470
4598
|
] }),
|
|
3471
|
-
state.tab === "send" && /* @__PURE__ */
|
|
4599
|
+
state.tab === "send" && /* @__PURE__ */ jsx12(
|
|
3472
4600
|
Form,
|
|
3473
4601
|
{
|
|
3474
4602
|
strings: options.strings,
|
|
@@ -3478,7 +4606,7 @@ function mountWidget(options) {
|
|
|
3478
4606
|
...state.error !== void 0 && { errorMessage: state.error }
|
|
3479
4607
|
}
|
|
3480
4608
|
),
|
|
3481
|
-
state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */
|
|
4609
|
+
state.tab === "mine" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
|
|
3482
4610
|
MineList,
|
|
3483
4611
|
{
|
|
3484
4612
|
api: options.api,
|
|
@@ -3487,7 +4615,7 @@ function mountWidget(options) {
|
|
|
3487
4615
|
onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
|
|
3488
4616
|
}
|
|
3489
4617
|
),
|
|
3490
|
-
(state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */
|
|
4618
|
+
(state.tab === "mine" || state.tab === "changelog") && options.api && externalId && state.selectedReportId && /* @__PURE__ */ jsx12(
|
|
3491
4619
|
ReportDetailView,
|
|
3492
4620
|
{
|
|
3493
4621
|
api: options.api,
|
|
@@ -3497,7 +4625,7 @@ function mountWidget(options) {
|
|
|
3497
4625
|
onBack: () => rerender(clearSelected({ ...currentState }))
|
|
3498
4626
|
}
|
|
3499
4627
|
),
|
|
3500
|
-
state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */
|
|
4628
|
+
state.tab === "changelog" && options.api && externalId && !state.selectedReportId && /* @__PURE__ */ jsx12(
|
|
3501
4629
|
ChangelogList,
|
|
3502
4630
|
{
|
|
3503
4631
|
api: options.api,
|
|
@@ -3505,6 +4633,16 @@ function mountWidget(options) {
|
|
|
3505
4633
|
strings: options.strings,
|
|
3506
4634
|
onSelect: (row) => rerender({ ...currentState, selectedReportId: row.id })
|
|
3507
4635
|
}
|
|
4636
|
+
),
|
|
4637
|
+
state.tab === "board" && options.api && externalId && // BoardView owns its own master/detail navigation — no
|
|
4638
|
+
// selectedReportId routing through the modal-level state.
|
|
4639
|
+
/* @__PURE__ */ jsx12(
|
|
4640
|
+
BoardView,
|
|
4641
|
+
{
|
|
4642
|
+
api: options.api,
|
|
4643
|
+
externalId,
|
|
4644
|
+
strings: options.strings
|
|
4645
|
+
}
|
|
3508
4646
|
)
|
|
3509
4647
|
]
|
|
3510
4648
|
}
|
|
@@ -3558,15 +4696,13 @@ function createFeedback(config) {
|
|
|
3558
4696
|
document.body.appendChild(host);
|
|
3559
4697
|
}
|
|
3560
4698
|
async function buildAndSubmit(values) {
|
|
3561
|
-
const manualScreenshot = values.screenshot;
|
|
3562
|
-
const screenshot = values.synthetic ? void 0 : manualScreenshot ?? await takeScreenshot(document.body, {
|
|
3563
|
-
mask: [".mhosaic-feedback", "[data-mfb-mask]"]
|
|
3564
|
-
});
|
|
4699
|
+
const manualScreenshot = values.synthetic ? void 0 : values.screenshot;
|
|
3565
4700
|
const technical_context = capture.snapshot();
|
|
3566
4701
|
if (user) technical_context.user = user;
|
|
3567
4702
|
if (metadata && Object.keys(metadata).length > 0) {
|
|
3568
4703
|
technical_context.metadata = { ...metadata };
|
|
3569
4704
|
}
|
|
4705
|
+
const captureMethod = manualScreenshot ? values.capture_method ?? "manual" : "none";
|
|
3570
4706
|
const payload = {
|
|
3571
4707
|
description: values.description,
|
|
3572
4708
|
feedback_type: values.feedback_type ?? "bug",
|
|
@@ -3574,13 +4710,13 @@ function createFeedback(config) {
|
|
|
3574
4710
|
env,
|
|
3575
4711
|
page_url: window.location.href,
|
|
3576
4712
|
user_agent: navigator.userAgent,
|
|
3577
|
-
capture_method:
|
|
4713
|
+
capture_method: captureMethod,
|
|
3578
4714
|
technical_context
|
|
3579
4715
|
};
|
|
3580
|
-
if ("0.
|
|
3581
|
-
payload.widget_version = "0.
|
|
4716
|
+
if ("0.12.0") {
|
|
4717
|
+
payload.widget_version = "0.12.0";
|
|
3582
4718
|
}
|
|
3583
|
-
if (
|
|
4719
|
+
if (manualScreenshot) payload.screenshot = manualScreenshot;
|
|
3584
4720
|
if (values.synthetic) payload.synthetic = true;
|
|
3585
4721
|
if (user?.id !== void 0 && user.id !== null && user.id !== "") {
|
|
3586
4722
|
payload.user = {
|
|
@@ -3664,4 +4800,4 @@ function createFeedback(config) {
|
|
|
3664
4800
|
export {
|
|
3665
4801
|
createFeedback
|
|
3666
4802
|
};
|
|
3667
|
-
//# sourceMappingURL=chunk-
|
|
4803
|
+
//# sourceMappingURL=chunk-XSYC5TDL.mjs.map
|