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