@mhosaic/feedback 0.9.1 → 0.11.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-MDLVMRQV.mjs → chunk-AO3BXEG5.mjs} +592 -157
- package/dist/chunk-AO3BXEG5.mjs.map +1 -0
- package/dist/chunk-DKE6ELJ4.mjs +30 -0
- package/dist/chunk-DKE6ELJ4.mjs.map +1 -0
- package/dist/embed.min.js +216 -74
- 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-MDLVMRQV.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) {
|
|
@@ -128,7 +138,28 @@ function createApiClient(options) {
|
|
|
128
138
|
}
|
|
129
139
|
|
|
130
140
|
// src/capture/urlSanitizer.ts
|
|
131
|
-
var
|
|
141
|
+
var SENSITIVE_NAME = /token|key|password|secret|auth|session|sig|credential|bearer|cookie/i;
|
|
142
|
+
var SENSITIVE_VALUE_PATTERNS = [
|
|
143
|
+
// JWT: three base64url segments separated by dots, header begins with eyJ.
|
|
144
|
+
/^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/,
|
|
145
|
+
// Mhosaic feedback keys (the project's own format).
|
|
146
|
+
/^(?:sk|pk)_proj_[A-Za-z0-9_-]{16,}$/,
|
|
147
|
+
// GitHub PATs (ghp/gho/ghu/ghs/ghr prefixes, 36+ chars).
|
|
148
|
+
/^gh[pousr]_[A-Za-z0-9]{30,}$/,
|
|
149
|
+
// Stripe live/test secret keys.
|
|
150
|
+
/^(?:sk|pk|rk|whsec)_(?:live|test)_[A-Za-z0-9]{16,}$/,
|
|
151
|
+
// AWS access key id.
|
|
152
|
+
/^AKIA[0-9A-Z]{12,}$/,
|
|
153
|
+
// Slack bot tokens.
|
|
154
|
+
/^xox[abprso]-[A-Za-z0-9-]{12,}$/,
|
|
155
|
+
// Generic high-entropy: hex 40+ chars (SHA-1, SHA-256) or long alnum 32+.
|
|
156
|
+
/^[a-f0-9]{40,}$/i,
|
|
157
|
+
// Long opaque base64-ish string (common for OAuth codes & session ids).
|
|
158
|
+
/^[A-Za-z0-9_-]{40,}$/
|
|
159
|
+
];
|
|
160
|
+
function looksLikeCredential(value) {
|
|
161
|
+
return SENSITIVE_VALUE_PATTERNS.some((re) => re.test(value));
|
|
162
|
+
}
|
|
132
163
|
function sanitizeUrl(url) {
|
|
133
164
|
try {
|
|
134
165
|
const isAbsolute = /^https?:\/\//i.test(url);
|
|
@@ -138,9 +169,14 @@ function sanitizeUrl(url) {
|
|
|
138
169
|
const u = new URL(url, base);
|
|
139
170
|
const clean = new URLSearchParams();
|
|
140
171
|
u.searchParams.forEach((value, name) => {
|
|
141
|
-
|
|
172
|
+
if (SENSITIVE_NAME.test(name) || looksLikeCredential(value)) {
|
|
173
|
+
clean.set(name, "[redacted]");
|
|
174
|
+
} else {
|
|
175
|
+
clean.set(name, value);
|
|
176
|
+
}
|
|
142
177
|
});
|
|
143
178
|
u.search = clean.toString();
|
|
179
|
+
u.hash = u.hash ? "#[redacted]" : "";
|
|
144
180
|
return u.toString();
|
|
145
181
|
} catch {
|
|
146
182
|
return url;
|
|
@@ -152,7 +188,16 @@ function collectDevice() {
|
|
|
152
188
|
const nav = navigator;
|
|
153
189
|
const connection = nav.connection?.effectiveType;
|
|
154
190
|
const deviceMemory = nav.deviceMemory;
|
|
155
|
-
const
|
|
191
|
+
const rawReferrer = document.referrer || void 0;
|
|
192
|
+
const referrer = rawReferrer !== void 0 ? sanitizeUrl(rawReferrer) : void 0;
|
|
193
|
+
const sanitizedHere = sanitizeUrl(window.location.pathname + window.location.search);
|
|
194
|
+
let pathname;
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(sanitizedHere, window.location.origin);
|
|
197
|
+
pathname = parsed.pathname + parsed.search;
|
|
198
|
+
} catch {
|
|
199
|
+
pathname = window.location.pathname;
|
|
200
|
+
}
|
|
156
201
|
return {
|
|
157
202
|
viewport: { w: window.innerWidth, h: window.innerHeight, dpr: window.devicePixelRatio || 1 },
|
|
158
203
|
screen: { w: window.screen.width, h: window.screen.height },
|
|
@@ -165,8 +210,12 @@ function collectDevice() {
|
|
|
165
210
|
...deviceMemory !== void 0 && { deviceMemory },
|
|
166
211
|
hardwareConcurrency: nav.hardwareConcurrency,
|
|
167
212
|
...referrer !== void 0 && { referrer },
|
|
168
|
-
|
|
169
|
-
|
|
213
|
+
// Hosts commonly stuff credentials / PII into page titles
|
|
214
|
+
// ("Inbox (3) — bob@x.com", "Reset password — token=abc"). Cap to a
|
|
215
|
+
// sane length and run through the same scrubber the console/error
|
|
216
|
+
// paths use.
|
|
217
|
+
title: scrubCredentials(document.title || "").slice(0, 200),
|
|
218
|
+
pathname
|
|
170
219
|
};
|
|
171
220
|
}
|
|
172
221
|
|
|
@@ -196,7 +245,8 @@ function installConsolePatch(buffer) {
|
|
|
196
245
|
originals[level] = original;
|
|
197
246
|
console[level] = function patched(...args) {
|
|
198
247
|
try {
|
|
199
|
-
const
|
|
248
|
+
const rawMessage = args.map(safeStringify).join(" ");
|
|
249
|
+
const message = scrubCredentials(rawMessage).slice(0, 2e3);
|
|
200
250
|
const entry = { level, message, ts: Date.now() };
|
|
201
251
|
if (level === "error") {
|
|
202
252
|
const stack = new Error().stack;
|
|
@@ -229,13 +279,14 @@ function installFetchPatch(buffer, sanitize) {
|
|
|
229
279
|
buffer.push({ url: sanitize(url), method, status: response.status, durationMs: Math.round(performance.now() - start), ts: Date.now() });
|
|
230
280
|
return response;
|
|
231
281
|
} catch (err) {
|
|
282
|
+
const rawErr = err instanceof Error ? err.message : String(err);
|
|
232
283
|
buffer.push({
|
|
233
284
|
url: sanitize(url),
|
|
234
285
|
method,
|
|
235
286
|
status: 0,
|
|
236
287
|
durationMs: Math.round(performance.now() - start),
|
|
237
288
|
ts: Date.now(),
|
|
238
|
-
error:
|
|
289
|
+
error: scrubCredentials(rawErr)
|
|
239
290
|
});
|
|
240
291
|
throw err;
|
|
241
292
|
}
|
|
@@ -282,9 +333,10 @@ function installErrorHandlers(buffer) {
|
|
|
282
333
|
if (typeof window === "undefined") return () => {
|
|
283
334
|
};
|
|
284
335
|
const onError = (e) => {
|
|
285
|
-
const
|
|
336
|
+
const rawStack = e.error instanceof Error ? e.error.stack : void 0;
|
|
337
|
+
const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
|
|
286
338
|
buffer.push({
|
|
287
|
-
message: e.message || "Unknown error",
|
|
339
|
+
message: scrubCredentials(e.message || "Unknown error"),
|
|
288
340
|
...stack !== void 0 && { stack },
|
|
289
341
|
ts: Date.now(),
|
|
290
342
|
source: "window.error"
|
|
@@ -292,16 +344,17 @@ function installErrorHandlers(buffer) {
|
|
|
292
344
|
};
|
|
293
345
|
const onRejection = (e) => {
|
|
294
346
|
const reason = e.reason;
|
|
295
|
-
const
|
|
347
|
+
const rawMessage = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : (() => {
|
|
296
348
|
try {
|
|
297
349
|
return JSON.stringify(reason);
|
|
298
350
|
} catch {
|
|
299
351
|
return String(reason);
|
|
300
352
|
}
|
|
301
353
|
})();
|
|
302
|
-
const
|
|
354
|
+
const rawStack = reason instanceof Error ? reason.stack : void 0;
|
|
355
|
+
const stack = rawStack !== void 0 ? scrubCredentials(rawStack) : void 0;
|
|
303
356
|
buffer.push({
|
|
304
|
-
message,
|
|
357
|
+
message: scrubCredentials(rawMessage),
|
|
305
358
|
...stack !== void 0 && { stack },
|
|
306
359
|
ts: Date.now(),
|
|
307
360
|
source: "unhandledrejection"
|
|
@@ -330,7 +383,7 @@ function createPerformanceCollector(slowResourceMs = 1e3) {
|
|
|
330
383
|
} else if (entry.entryType === "resource") {
|
|
331
384
|
const e = entry;
|
|
332
385
|
if (e.duration > slowResourceMs) {
|
|
333
|
-
slowResources.push({ name: e.name, duration: e.duration, initiatorType: e.initiatorType });
|
|
386
|
+
slowResources.push({ name: sanitizeUrl(e.name), duration: e.duration, initiatorType: e.initiatorType });
|
|
334
387
|
while (slowResources.length > 20) slowResources.shift();
|
|
335
388
|
}
|
|
336
389
|
}
|
|
@@ -413,6 +466,19 @@ function installCapture(options = {}) {
|
|
|
413
466
|
}
|
|
414
467
|
|
|
415
468
|
// src/screenshot/index.ts
|
|
469
|
+
var DEFAULT_REDACTED_INPUT_SELECTORS = [
|
|
470
|
+
'input[type="password"]',
|
|
471
|
+
'input[type="tel"]',
|
|
472
|
+
'input[autocomplete*="cc-"]',
|
|
473
|
+
'input[autocomplete*="current-password"]',
|
|
474
|
+
'input[autocomplete*="new-password"]',
|
|
475
|
+
'input[autocomplete*="one-time-code"]',
|
|
476
|
+
'input[name*="cvv" i]',
|
|
477
|
+
'input[name*="ccv" i]',
|
|
478
|
+
'input[name*="cardnum" i]',
|
|
479
|
+
'input[name*="card-num" i]'
|
|
480
|
+
];
|
|
481
|
+
var REDACTION_MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
416
482
|
function isMaskedElement(el) {
|
|
417
483
|
return el.hasAttribute("data-mfb-mask") || el.classList.contains("mfb-mask");
|
|
418
484
|
}
|
|
@@ -421,13 +487,33 @@ function prepareMaskMatcher(selectors) {
|
|
|
421
487
|
if (!joined) return isMaskedElement;
|
|
422
488
|
return (el) => isMaskedElement(el) || el.matches(joined);
|
|
423
489
|
}
|
|
490
|
+
function redactSensitiveInputs(clonedDoc, extraSelectors) {
|
|
491
|
+
const selectors = [
|
|
492
|
+
...DEFAULT_REDACTED_INPUT_SELECTORS,
|
|
493
|
+
...extraSelectors,
|
|
494
|
+
"[data-mfb-mask]",
|
|
495
|
+
".mfb-mask"
|
|
496
|
+
].join(",");
|
|
497
|
+
const elements = clonedDoc.querySelectorAll(selectors);
|
|
498
|
+
elements.forEach((el) => {
|
|
499
|
+
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
500
|
+
const len = Math.min(Math.max(el.value.length || 8, 4), 16);
|
|
501
|
+
el.value = REDACTION_MASK.slice(0, len);
|
|
502
|
+
el.setAttribute("value", el.value);
|
|
503
|
+
} else if (el instanceof HTMLElement) {
|
|
504
|
+
el.textContent = REDACTION_MASK;
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
}
|
|
424
508
|
async function takeScreenshot(target, options = {}) {
|
|
425
509
|
if (typeof document === "undefined") return null;
|
|
426
510
|
try {
|
|
427
511
|
const html2canvas = (await import("html2canvas-pro")).default;
|
|
428
|
-
const
|
|
512
|
+
const extraSelectors = options.mask ?? [];
|
|
513
|
+
const matcher = prepareMaskMatcher(extraSelectors);
|
|
429
514
|
const canvas = await html2canvas(target, {
|
|
430
515
|
ignoreElements: (el) => matcher(el),
|
|
516
|
+
onclone: (clonedDoc) => redactSensitiveInputs(clonedDoc, extraSelectors),
|
|
431
517
|
useCORS: true,
|
|
432
518
|
logging: false,
|
|
433
519
|
backgroundColor: null
|
|
@@ -476,8 +562,12 @@ var DEFAULT_STRINGS = {
|
|
|
476
562
|
"annotator.tool.arrow": "Arrow",
|
|
477
563
|
"annotator.tool.freehand": "Freehand",
|
|
478
564
|
"annotator.tool.text": "Text",
|
|
565
|
+
"annotator.tool.highlight": "Highlight",
|
|
566
|
+
"annotator.tool.blur": "Blur (hide sensitive data)",
|
|
479
567
|
"annotator.text_prompt": "Enter text:",
|
|
568
|
+
"annotator.color_picker": "Custom color",
|
|
480
569
|
"annotator.undo": "Undo",
|
|
570
|
+
"annotator.redo": "Redo",
|
|
481
571
|
"annotator.clear": "Clear all",
|
|
482
572
|
"annotator.count_suffix": "annotations",
|
|
483
573
|
"annotator.loading": "Loading\u2026",
|
|
@@ -578,8 +668,12 @@ var FRENCH_STRINGS = {
|
|
|
578
668
|
"annotator.tool.arrow": "Fl\xE8che",
|
|
579
669
|
"annotator.tool.freehand": "Dessin libre",
|
|
580
670
|
"annotator.tool.text": "Texte",
|
|
671
|
+
"annotator.tool.highlight": "Surligner",
|
|
672
|
+
"annotator.tool.blur": "Flouter (donn\xE9es sensibles)",
|
|
581
673
|
"annotator.text_prompt": "Entrez le texte :",
|
|
674
|
+
"annotator.color_picker": "Couleur personnalis\xE9e",
|
|
582
675
|
"annotator.undo": "Annuler",
|
|
676
|
+
"annotator.redo": "Refaire",
|
|
583
677
|
"annotator.clear": "Tout effacer",
|
|
584
678
|
"annotator.count_suffix": "annotations",
|
|
585
679
|
"annotator.loading": "Chargement\u2026",
|
|
@@ -846,10 +940,11 @@ function Fab({ label, onClick }) {
|
|
|
846
940
|
import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "preact/hooks";
|
|
847
941
|
|
|
848
942
|
// src/widget/Annotator.tsx
|
|
849
|
-
import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "preact/hooks";
|
|
943
|
+
import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2, useState as useState2 } from "preact/hooks";
|
|
850
944
|
import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
851
945
|
var COLORS = ["#ef4444", "#f59e0b", "#10b981", "#3b82f6", "#ffffff"];
|
|
852
|
-
|
|
946
|
+
var HIGHLIGHT_ALPHA = 0.35;
|
|
947
|
+
function drawShape(ctx, shape, sourceImage) {
|
|
853
948
|
ctx.save();
|
|
854
949
|
ctx.strokeStyle = shape.color;
|
|
855
950
|
ctx.fillStyle = shape.color;
|
|
@@ -882,9 +977,22 @@ function drawShape(ctx, shape) {
|
|
|
882
977
|
ctx.fillRect(shape.x - padding, shape.y - padding, w, hh);
|
|
883
978
|
ctx.fillStyle = shape.color;
|
|
884
979
|
ctx.fillText(shape.text, shape.x, shape.y);
|
|
980
|
+
} else if (shape.kind === "highlight") {
|
|
981
|
+
ctx.globalAlpha = HIGHLIGHT_ALPHA;
|
|
982
|
+
ctx.fillRect(
|
|
983
|
+
normalizeStart(shape.x, shape.w),
|
|
984
|
+
normalizeStart(shape.y, shape.h),
|
|
985
|
+
Math.abs(shape.w),
|
|
986
|
+
Math.abs(shape.h)
|
|
987
|
+
);
|
|
988
|
+
} else if (shape.kind === "blur") {
|
|
989
|
+
drawBlur(ctx, shape, sourceImage);
|
|
885
990
|
}
|
|
886
991
|
ctx.restore();
|
|
887
992
|
}
|
|
993
|
+
function normalizeStart(start, size) {
|
|
994
|
+
return size < 0 ? start + size : start;
|
|
995
|
+
}
|
|
888
996
|
function drawArrow(ctx, x1, y1, x2, y2) {
|
|
889
997
|
const headLen = 14;
|
|
890
998
|
const angle = Math.atan2(y2 - y1, x2 - x1);
|
|
@@ -905,12 +1013,77 @@ function drawArrow(ctx, x1, y1, x2, y2) {
|
|
|
905
1013
|
ctx.closePath();
|
|
906
1014
|
ctx.fill();
|
|
907
1015
|
}
|
|
1016
|
+
function drawBlur(ctx, shape, sourceImage) {
|
|
1017
|
+
if (!sourceImage) return;
|
|
1018
|
+
const x = normalizeStart(shape.x, shape.w);
|
|
1019
|
+
const y = normalizeStart(shape.y, shape.h);
|
|
1020
|
+
const w = Math.abs(shape.w);
|
|
1021
|
+
const h2 = Math.abs(shape.h);
|
|
1022
|
+
if (w < 2 || h2 < 2) return;
|
|
1023
|
+
const tile = Math.max(4, shape.tile);
|
|
1024
|
+
for (let yy = y; yy < y + h2; yy += tile) {
|
|
1025
|
+
for (let xx = x; xx < x + w; xx += tile) {
|
|
1026
|
+
const tw = Math.min(tile, x + w - xx);
|
|
1027
|
+
const th = Math.min(tile, y + h2 - yy);
|
|
1028
|
+
ctx.drawImage(
|
|
1029
|
+
sourceImage,
|
|
1030
|
+
xx,
|
|
1031
|
+
yy,
|
|
1032
|
+
tw,
|
|
1033
|
+
th,
|
|
1034
|
+
// source rect from the image
|
|
1035
|
+
xx,
|
|
1036
|
+
yy,
|
|
1037
|
+
tw,
|
|
1038
|
+
th
|
|
1039
|
+
// dest rect on the canvas (same coords)
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
ctx.imageSmoothingEnabled = false;
|
|
1044
|
+
for (let yy = y; yy < y + h2; yy += tile) {
|
|
1045
|
+
for (let xx = x; xx < x + w; xx += tile) {
|
|
1046
|
+
const tw = Math.min(tile, x + w - xx);
|
|
1047
|
+
const th = Math.min(tile, y + h2 - yy);
|
|
1048
|
+
ctx.drawImage(
|
|
1049
|
+
sourceImage,
|
|
1050
|
+
xx,
|
|
1051
|
+
yy,
|
|
1052
|
+
1,
|
|
1053
|
+
1,
|
|
1054
|
+
// single source pixel
|
|
1055
|
+
xx,
|
|
1056
|
+
yy,
|
|
1057
|
+
tw,
|
|
1058
|
+
th
|
|
1059
|
+
// stretched dest tile
|
|
1060
|
+
);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
ctx.imageSmoothingEnabled = true;
|
|
1064
|
+
}
|
|
908
1065
|
var Icon = {
|
|
909
1066
|
rect: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("rect", { x: "2", y: "3", width: "12", height: "10", fill: "none", stroke: "currentColor", "stroke-width": "1.5" }) }),
|
|
910
1067
|
arrow: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M2 8h11M9 4l4 4-4 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
|
|
911
1068
|
pencil: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M11.5 2.5l2 2L5 13H3v-2l8.5-8.5z", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linejoin": "round" }) }),
|
|
912
1069
|
text: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 3h10M8 3v10M5 13h6", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) }),
|
|
1070
|
+
highlight: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
|
|
1071
|
+
/* @__PURE__ */ jsx4("path", { d: "M3 13l3-3L11 5l-2-2-5 5-3 3v2h2zM10 4l2 2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }),
|
|
1072
|
+
/* @__PURE__ */ jsx4("path", { d: "M2 14h12", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" })
|
|
1073
|
+
] }),
|
|
1074
|
+
blur: /* @__PURE__ */ jsxs3("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: [
|
|
1075
|
+
/* @__PURE__ */ jsx4("rect", { x: "2", y: "2", width: "4", height: "4", fill: "currentColor", opacity: "0.85" }),
|
|
1076
|
+
/* @__PURE__ */ jsx4("rect", { x: "7", y: "2", width: "3", height: "3", fill: "currentColor", opacity: "0.55" }),
|
|
1077
|
+
/* @__PURE__ */ jsx4("rect", { x: "11", y: "2", width: "3", height: "4", fill: "currentColor", opacity: "0.4" }),
|
|
1078
|
+
/* @__PURE__ */ jsx4("rect", { x: "2", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.6" }),
|
|
1079
|
+
/* @__PURE__ */ jsx4("rect", { x: "6", y: "7", width: "3", height: "3", fill: "currentColor", opacity: "0.85" }),
|
|
1080
|
+
/* @__PURE__ */ jsx4("rect", { x: "10", y: "7", width: "4", height: "3", fill: "currentColor", opacity: "0.5" }),
|
|
1081
|
+
/* @__PURE__ */ jsx4("rect", { x: "2", y: "11", width: "4", height: "3", fill: "currentColor", opacity: "0.4" }),
|
|
1082
|
+
/* @__PURE__ */ jsx4("rect", { x: "7", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.65" }),
|
|
1083
|
+
/* @__PURE__ */ jsx4("rect", { x: "11", y: "11", width: "3", height: "3", fill: "currentColor", opacity: "0.85" })
|
|
1084
|
+
] }),
|
|
913
1085
|
undo: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 7l3-3M4 7l3 3M4 7h6a3 3 0 0 1 0 6H7", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
|
|
1086
|
+
redo: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M12 7l-3-3M12 7l-3 3M12 7H6a3 3 0 0 0 0 6h2", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
|
|
914
1087
|
trash: /* @__PURE__ */ jsx4("svg", { width: "14", height: "14", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M3 4h10M6 4V2.5h4V4M5 4l.5 9h5L11 4", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round" }) }),
|
|
915
1088
|
close: /* @__PURE__ */ jsx4("svg", { width: "16", height: "16", viewBox: "0 0 16 16", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("path", { d: "M4 4l8 8M12 4l-8 8", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round" }) })
|
|
916
1089
|
};
|
|
@@ -921,12 +1094,50 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
921
1094
|
const [tool, setTool] = useState2("rectangle");
|
|
922
1095
|
const [color, setColor] = useState2(COLORS[0]);
|
|
923
1096
|
const [shapes, setShapes] = useState2([]);
|
|
1097
|
+
const [past, setPast] = useState2([]);
|
|
1098
|
+
const [future, setFuture] = useState2([]);
|
|
924
1099
|
const isDrawingRef = useRef2(false);
|
|
925
1100
|
const [draftShape, setDraftShape] = useState2(null);
|
|
926
1101
|
const [imageLoaded, setImageLoaded] = useState2(false);
|
|
927
1102
|
const [saving, setSaving] = useState2(false);
|
|
928
1103
|
const scaleRef = useRef2(1);
|
|
929
|
-
|
|
1104
|
+
function addShape(shape) {
|
|
1105
|
+
setShapes((s) => {
|
|
1106
|
+
setPast((p) => [...p, s]);
|
|
1107
|
+
return [...s, shape];
|
|
1108
|
+
});
|
|
1109
|
+
setFuture([]);
|
|
1110
|
+
}
|
|
1111
|
+
function clearShapes() {
|
|
1112
|
+
setShapes((s) => {
|
|
1113
|
+
setPast((p) => [...p, s]);
|
|
1114
|
+
return [];
|
|
1115
|
+
});
|
|
1116
|
+
setFuture([]);
|
|
1117
|
+
}
|
|
1118
|
+
function undo() {
|
|
1119
|
+
setPast((p) => {
|
|
1120
|
+
if (p.length === 0) return p;
|
|
1121
|
+
const prev = p[p.length - 1];
|
|
1122
|
+
setShapes((s) => {
|
|
1123
|
+
setFuture((f) => [s, ...f]);
|
|
1124
|
+
return prev;
|
|
1125
|
+
});
|
|
1126
|
+
return p.slice(0, -1);
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
function redo() {
|
|
1130
|
+
setFuture((f) => {
|
|
1131
|
+
if (f.length === 0) return f;
|
|
1132
|
+
const next = f[0];
|
|
1133
|
+
setShapes((s) => {
|
|
1134
|
+
setPast((p) => [...p, s]);
|
|
1135
|
+
return next;
|
|
1136
|
+
});
|
|
1137
|
+
return f.slice(1);
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
useLayoutEffect(() => {
|
|
930
1141
|
const onKey = (e) => {
|
|
931
1142
|
if (e.key === "Escape") {
|
|
932
1143
|
e.stopPropagation();
|
|
@@ -936,6 +1147,25 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
936
1147
|
window.addEventListener("keydown", onKey);
|
|
937
1148
|
return () => window.removeEventListener("keydown", onKey);
|
|
938
1149
|
}, [onCancel]);
|
|
1150
|
+
useEffect2(() => {
|
|
1151
|
+
const onKey = (e) => {
|
|
1152
|
+
const mod = e.metaKey || e.ctrlKey;
|
|
1153
|
+
if (!mod) return;
|
|
1154
|
+
const k = e.key.toLowerCase();
|
|
1155
|
+
if (k === "z") {
|
|
1156
|
+
e.preventDefault();
|
|
1157
|
+
e.stopPropagation();
|
|
1158
|
+
if (e.shiftKey) redo();
|
|
1159
|
+
else undo();
|
|
1160
|
+
} else if (k === "y") {
|
|
1161
|
+
e.preventDefault();
|
|
1162
|
+
e.stopPropagation();
|
|
1163
|
+
redo();
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
window.addEventListener("keydown", onKey);
|
|
1167
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
1168
|
+
}, [past, future, shapes]);
|
|
939
1169
|
useEffect2(() => {
|
|
940
1170
|
const url = URL.createObjectURL(imageBlob);
|
|
941
1171
|
const img = new Image();
|
|
@@ -953,7 +1183,7 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
953
1183
|
const img = imageRef.current;
|
|
954
1184
|
const container = containerRef.current;
|
|
955
1185
|
const maxW = container.clientWidth - 16;
|
|
956
|
-
const maxH = window.innerHeight * 0.
|
|
1186
|
+
const maxH = window.innerHeight * 0.65;
|
|
957
1187
|
const fitScale = Math.min(maxW / img.width, maxH / img.height, 1);
|
|
958
1188
|
scaleRef.current = fitScale;
|
|
959
1189
|
const canvas = canvasRef.current;
|
|
@@ -974,8 +1204,8 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
974
1204
|
if (!ctx) return;
|
|
975
1205
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
976
1206
|
ctx.drawImage(img, 0, 0);
|
|
977
|
-
for (const s of shapes) drawShape(ctx, s);
|
|
978
|
-
if (draftShape) drawShape(ctx, draftShape);
|
|
1207
|
+
for (const s of shapes) drawShape(ctx, s, img);
|
|
1208
|
+
if (draftShape) drawShape(ctx, draftShape, img);
|
|
979
1209
|
}
|
|
980
1210
|
function getCanvasCoords(e) {
|
|
981
1211
|
const canvas = canvasRef.current;
|
|
@@ -988,22 +1218,21 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
988
1218
|
const handleMouseDown = (e) => {
|
|
989
1219
|
if (!imageLoaded) return;
|
|
990
1220
|
const { x, y } = getCanvasCoords(e);
|
|
991
|
-
const
|
|
1221
|
+
const canvasW = canvasRef.current.width;
|
|
1222
|
+
const lineWidth = Math.max(3, Math.round(canvasW / 400));
|
|
1223
|
+
const blurTile = Math.max(8, Math.round(canvasW / 80));
|
|
992
1224
|
if (tool === "text") {
|
|
993
1225
|
const text = window.prompt(strings["annotator.text_prompt"]);
|
|
994
1226
|
if (text && text.trim()) {
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
lineWidth: 1
|
|
1005
|
-
}
|
|
1006
|
-
]);
|
|
1227
|
+
addShape({
|
|
1228
|
+
kind: "text",
|
|
1229
|
+
x,
|
|
1230
|
+
y,
|
|
1231
|
+
text: text.trim(),
|
|
1232
|
+
color,
|
|
1233
|
+
fontSize: Math.max(16, Math.round(canvasW / 50)),
|
|
1234
|
+
lineWidth: 1
|
|
1235
|
+
});
|
|
1007
1236
|
}
|
|
1008
1237
|
return;
|
|
1009
1238
|
}
|
|
@@ -1014,31 +1243,60 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
1014
1243
|
setDraftShape({ kind: "arrow", x1: x, y1: y, x2: x, y2: y, color, lineWidth });
|
|
1015
1244
|
} else if (tool === "freehand") {
|
|
1016
1245
|
setDraftShape({ kind: "freehand", points: [{ x, y }], color, lineWidth });
|
|
1246
|
+
} else if (tool === "highlight") {
|
|
1247
|
+
setDraftShape({
|
|
1248
|
+
kind: "highlight",
|
|
1249
|
+
x,
|
|
1250
|
+
y,
|
|
1251
|
+
w: 0,
|
|
1252
|
+
h: 0,
|
|
1253
|
+
color: color === "#ffffff" ? "#fde047" : color,
|
|
1254
|
+
lineWidth: 1
|
|
1255
|
+
});
|
|
1256
|
+
} else if (tool === "blur") {
|
|
1257
|
+
setDraftShape({
|
|
1258
|
+
kind: "blur",
|
|
1259
|
+
x,
|
|
1260
|
+
y,
|
|
1261
|
+
w: 0,
|
|
1262
|
+
h: 0,
|
|
1263
|
+
color: "#000000",
|
|
1264
|
+
lineWidth: 0,
|
|
1265
|
+
tile: blurTile
|
|
1266
|
+
});
|
|
1017
1267
|
}
|
|
1018
1268
|
};
|
|
1019
1269
|
const handleMouseMove = (e) => {
|
|
1020
|
-
if (!isDrawingRef.current
|
|
1270
|
+
if (!isDrawingRef.current) return;
|
|
1021
1271
|
const { x, y } = getCanvasCoords(e);
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1272
|
+
setDraftShape((current) => {
|
|
1273
|
+
if (!current) return current;
|
|
1274
|
+
if (current.kind === "rectangle" || current.kind === "highlight" || current.kind === "blur") {
|
|
1275
|
+
return { ...current, w: x - current.x, h: y - current.y };
|
|
1276
|
+
}
|
|
1277
|
+
if (current.kind === "arrow") {
|
|
1278
|
+
return { ...current, x2: x, y2: y };
|
|
1279
|
+
}
|
|
1280
|
+
if (current.kind === "freehand") {
|
|
1281
|
+
return { ...current, points: [...current.points, { x, y }] };
|
|
1282
|
+
}
|
|
1283
|
+
return current;
|
|
1284
|
+
});
|
|
1029
1285
|
};
|
|
1030
1286
|
const handleMouseUp = () => {
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1287
|
+
setDraftShape((current) => {
|
|
1288
|
+
if (isDrawingRef.current && current) {
|
|
1289
|
+
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(
|
|
1290
|
+
current.x2 - current.x1,
|
|
1291
|
+
current.y2 - current.y1
|
|
1292
|
+
) < 4 || current.kind === "freehand" && current.points.length < 3;
|
|
1293
|
+
if (!isTiny) {
|
|
1294
|
+
addShape(current);
|
|
1295
|
+
}
|
|
1038
1296
|
}
|
|
1039
|
-
|
|
1297
|
+
return null;
|
|
1298
|
+
});
|
|
1040
1299
|
isDrawingRef.current = false;
|
|
1041
|
-
setDraftShape(null);
|
|
1042
1300
|
};
|
|
1043
1301
|
const handleSave = async () => {
|
|
1044
1302
|
const canvas = canvasRef.current;
|
|
@@ -1057,7 +1315,9 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
1057
1315
|
{ id: "rectangle", icon: Icon.rect, label: strings["annotator.tool.rectangle"] },
|
|
1058
1316
|
{ id: "arrow", icon: Icon.arrow, label: strings["annotator.tool.arrow"] },
|
|
1059
1317
|
{ id: "freehand", icon: Icon.pencil, label: strings["annotator.tool.freehand"] },
|
|
1060
|
-
{ id: "text", icon: Icon.text, label: strings["annotator.tool.text"] }
|
|
1318
|
+
{ id: "text", icon: Icon.text, label: strings["annotator.tool.text"] },
|
|
1319
|
+
{ id: "highlight", icon: Icon.highlight, label: strings["annotator.tool.highlight"] },
|
|
1320
|
+
{ id: "blur", icon: Icon.blur, label: strings["annotator.tool.blur"] }
|
|
1061
1321
|
];
|
|
1062
1322
|
return /* @__PURE__ */ jsx4(
|
|
1063
1323
|
"div",
|
|
@@ -1096,26 +1356,38 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
1096
1356
|
t.id
|
|
1097
1357
|
)) }),
|
|
1098
1358
|
/* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
|
|
1099
|
-
/* @__PURE__ */
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1359
|
+
/* @__PURE__ */ jsxs3("div", { class: "annotator-colors", children: [
|
|
1360
|
+
COLORS.map((c) => /* @__PURE__ */ jsx4(
|
|
1361
|
+
"button",
|
|
1362
|
+
{
|
|
1363
|
+
type: "button",
|
|
1364
|
+
onClick: () => setColor(c),
|
|
1365
|
+
"aria-label": c,
|
|
1366
|
+
"aria-pressed": color === c,
|
|
1367
|
+
class: `annotator-color ${color === c ? "is-active" : ""}`,
|
|
1368
|
+
style: { backgroundColor: c }
|
|
1369
|
+
},
|
|
1370
|
+
c
|
|
1371
|
+
)),
|
|
1372
|
+
/* @__PURE__ */ jsx4("label", { class: "annotator-color annotator-color-picker", title: strings["annotator.color_picker"], children: /* @__PURE__ */ jsx4(
|
|
1373
|
+
"input",
|
|
1374
|
+
{
|
|
1375
|
+
type: "color",
|
|
1376
|
+
value: color,
|
|
1377
|
+
onInput: (e) => setColor(e.target.value),
|
|
1378
|
+
"aria-label": strings["annotator.color_picker"]
|
|
1379
|
+
}
|
|
1380
|
+
) })
|
|
1381
|
+
] }),
|
|
1111
1382
|
/* @__PURE__ */ jsx4("span", { class: "annotator-sep" }),
|
|
1112
1383
|
/* @__PURE__ */ jsxs3(
|
|
1113
1384
|
"button",
|
|
1114
1385
|
{
|
|
1115
1386
|
type: "button",
|
|
1116
1387
|
class: "annotator-btn",
|
|
1117
|
-
onClick:
|
|
1118
|
-
disabled:
|
|
1388
|
+
onClick: undo,
|
|
1389
|
+
disabled: past.length === 0,
|
|
1390
|
+
title: strings["annotator.undo"],
|
|
1119
1391
|
children: [
|
|
1120
1392
|
Icon.undo,
|
|
1121
1393
|
/* @__PURE__ */ jsx4("span", { children: strings["annotator.undo"] })
|
|
@@ -1127,7 +1399,21 @@ function Annotator({ imageBlob, strings, onSave, onCancel }) {
|
|
|
1127
1399
|
{
|
|
1128
1400
|
type: "button",
|
|
1129
1401
|
class: "annotator-btn",
|
|
1130
|
-
onClick:
|
|
1402
|
+
onClick: redo,
|
|
1403
|
+
disabled: future.length === 0,
|
|
1404
|
+
title: strings["annotator.redo"],
|
|
1405
|
+
children: [
|
|
1406
|
+
Icon.redo,
|
|
1407
|
+
/* @__PURE__ */ jsx4("span", { children: strings["annotator.redo"] })
|
|
1408
|
+
]
|
|
1409
|
+
}
|
|
1410
|
+
),
|
|
1411
|
+
/* @__PURE__ */ jsxs3(
|
|
1412
|
+
"button",
|
|
1413
|
+
{
|
|
1414
|
+
type: "button",
|
|
1415
|
+
class: "annotator-btn",
|
|
1416
|
+
onClick: clearShapes,
|
|
1131
1417
|
disabled: shapes.length === 0,
|
|
1132
1418
|
children: [
|
|
1133
1419
|
Icon.trash,
|
|
@@ -1359,25 +1645,27 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
1359
1645
|
),
|
|
1360
1646
|
screenshotPreview ? /* @__PURE__ */ jsxs4("div", { class: "screenshot-preview", children: [
|
|
1361
1647
|
/* @__PURE__ */ jsx5("img", { src: screenshotPreview, alt: "" }),
|
|
1362
|
-
/* @__PURE__ */
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1648
|
+
/* @__PURE__ */ jsxs4("div", { class: "screenshot-preview-actions", children: [
|
|
1649
|
+
/* @__PURE__ */ jsxs4(
|
|
1650
|
+
"button",
|
|
1651
|
+
{
|
|
1652
|
+
type: "button",
|
|
1653
|
+
class: "btn btn--primary screenshot-annotate",
|
|
1654
|
+
onClick: () => setAnnotatorOpen(true),
|
|
1655
|
+
children: [
|
|
1656
|
+
/* @__PURE__ */ jsxs4("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
|
|
1657
|
+
/* @__PURE__ */ jsx5("path", { d: "M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" }),
|
|
1658
|
+
/* @__PURE__ */ jsx5("path", { d: "M18.5 2.5a2.12 2.12 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" })
|
|
1659
|
+
] }),
|
|
1660
|
+
strings["form.screenshot.annotate"]
|
|
1661
|
+
]
|
|
1662
|
+
}
|
|
1663
|
+
),
|
|
1664
|
+
/* @__PURE__ */ jsxs4("button", { type: "button", class: "btn", onClick: clearScreenshot, children: [
|
|
1665
|
+
/* @__PURE__ */ jsx5("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx5("path", { d: "M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m3 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6h14z" }) }),
|
|
1666
|
+
strings["form.screenshot.remove"]
|
|
1667
|
+
] })
|
|
1668
|
+
] })
|
|
1381
1669
|
] }) : /* @__PURE__ */ jsxs4(
|
|
1382
1670
|
"div",
|
|
1383
1671
|
{
|
|
@@ -1397,6 +1685,11 @@ function Form({ strings, onSubmit, onCancel, status, errorMessage }) {
|
|
|
1397
1685
|
onDragLeave: handleDragLeave,
|
|
1398
1686
|
onDrop: handleDrop,
|
|
1399
1687
|
children: [
|
|
1688
|
+
/* @__PURE__ */ jsxs4("svg", { class: "screenshot-icon", width: "28", height: "28", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "1.6", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: [
|
|
1689
|
+
/* @__PURE__ */ jsx5("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
|
|
1690
|
+
/* @__PURE__ */ jsx5("polyline", { points: "17 8 12 3 7 8" }),
|
|
1691
|
+
/* @__PURE__ */ jsx5("line", { x1: "12", y1: "3", x2: "12", y2: "15" })
|
|
1692
|
+
] }),
|
|
1400
1693
|
/* @__PURE__ */ jsxs4("div", { class: "screenshot-cta", children: [
|
|
1401
1694
|
/* @__PURE__ */ jsx5("strong", { children: strings["form.screenshot.cta_click"] }),
|
|
1402
1695
|
", ",
|
|
@@ -1972,13 +2265,39 @@ var WIDGET_STYLES = `
|
|
|
1972
2265
|
--mfb-accent-contrast: #ffffff;
|
|
1973
2266
|
--mfb-bg: #ffffff;
|
|
1974
2267
|
--mfb-surface: #f9fafb;
|
|
2268
|
+
--mfb-surface-2: #f3f4f6;
|
|
1975
2269
|
--mfb-text: #0a0a0a;
|
|
1976
2270
|
--mfb-text-muted: #6b7280;
|
|
1977
2271
|
--mfb-border: #e5e7eb;
|
|
2272
|
+
--mfb-border-strong: #d1d5db;
|
|
1978
2273
|
--mfb-radius: 8px;
|
|
2274
|
+
--mfb-radius-lg: 14px;
|
|
1979
2275
|
--mfb-font: system-ui, -apple-system, sans-serif;
|
|
1980
2276
|
--mfb-z-index: 2147483640;
|
|
1981
2277
|
|
|
2278
|
+
/* Spacing scale \u2014 4px base, used everywhere instead of magic numbers. */
|
|
2279
|
+
--mfb-space-1: 4px;
|
|
2280
|
+
--mfb-space-2: 8px;
|
|
2281
|
+
--mfb-space-3: 12px;
|
|
2282
|
+
--mfb-space-4: 16px;
|
|
2283
|
+
--mfb-space-5: 24px;
|
|
2284
|
+
--mfb-space-6: 32px;
|
|
2285
|
+
--mfb-space-7: 48px;
|
|
2286
|
+
|
|
2287
|
+
/* Type scale \u2014 fixed sizes, no fluid scaling. Body 14px sits in the
|
|
2288
|
+
"comfortable on every viewport" range; headings step up gracefully. */
|
|
2289
|
+
--mfb-text-xs: 11px;
|
|
2290
|
+
--mfb-text-sm: 13px;
|
|
2291
|
+
--mfb-text-base: 14px;
|
|
2292
|
+
--mfb-text-md: 15px;
|
|
2293
|
+
--mfb-text-lg: 17px;
|
|
2294
|
+
--mfb-text-xl: 20px;
|
|
2295
|
+
|
|
2296
|
+
/* Elevation \u2014 three tiers for layering. */
|
|
2297
|
+
--mfb-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
2298
|
+
--mfb-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08), 0 2px 4px rgba(0, 0, 0, 0.12);
|
|
2299
|
+
--mfb-shadow-lg: 0 24px 56px rgba(0, 0, 0, 0.18), 0 8px 20px rgba(0, 0, 0, 0.10);
|
|
2300
|
+
|
|
1982
2301
|
all: initial;
|
|
1983
2302
|
font-family: var(--mfb-font);
|
|
1984
2303
|
color: var(--mfb-text);
|
|
@@ -1988,11 +2307,14 @@ var WIDGET_STYLES = `
|
|
|
1988
2307
|
|
|
1989
2308
|
@media (prefers-color-scheme: dark) {
|
|
1990
2309
|
:host {
|
|
1991
|
-
--mfb-bg: #
|
|
1992
|
-
--mfb-surface: #
|
|
1993
|
-
--mfb-
|
|
1994
|
-
--mfb-text
|
|
1995
|
-
--mfb-
|
|
2310
|
+
--mfb-bg: #0f172a;
|
|
2311
|
+
--mfb-surface: #1e293b;
|
|
2312
|
+
--mfb-surface-2: #253349;
|
|
2313
|
+
--mfb-text: #f8fafc;
|
|
2314
|
+
--mfb-text-muted: #94a3b8;
|
|
2315
|
+
--mfb-border: #334155;
|
|
2316
|
+
--mfb-border-strong: #475569;
|
|
2317
|
+
--mfb-shadow-lg: 0 24px 56px rgba(0, 0, 0, 0.55), 0 8px 20px rgba(0, 0, 0, 0.40);
|
|
1996
2318
|
}
|
|
1997
2319
|
}
|
|
1998
2320
|
|
|
@@ -2051,60 +2373,98 @@ var WIDGET_STYLES = `
|
|
|
2051
2373
|
.fab:hover, .fab:active { transform: none; }
|
|
2052
2374
|
}
|
|
2053
2375
|
|
|
2376
|
+
/* Backdrop \u2014 fade in with a slight blur for depth. The blur gives the
|
|
2377
|
+
modal that "page that opens" weight: the underlying page recedes
|
|
2378
|
+
visually so the widget feels foregrounded, not pasted on. */
|
|
2054
2379
|
.backdrop {
|
|
2055
2380
|
position: fixed;
|
|
2056
2381
|
inset: 0;
|
|
2057
|
-
background: rgba(
|
|
2382
|
+
background: rgba(15, 23, 42, 0.55);
|
|
2383
|
+
backdrop-filter: blur(6px);
|
|
2384
|
+
-webkit-backdrop-filter: blur(6px);
|
|
2058
2385
|
display: grid;
|
|
2059
2386
|
place-items: center;
|
|
2387
|
+
animation: mfb-backdrop-in 220ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
@keyframes mfb-backdrop-in {
|
|
2391
|
+
from { opacity: 0; backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); }
|
|
2392
|
+
to { opacity: 1; backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
|
|
2060
2393
|
}
|
|
2061
2394
|
|
|
2062
2395
|
.modal {
|
|
2063
2396
|
background: var(--mfb-bg);
|
|
2064
|
-
border-radius:
|
|
2065
|
-
box-shadow:
|
|
2066
|
-
/*
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
padding:
|
|
2397
|
+
border-radius: var(--mfb-radius-lg);
|
|
2398
|
+
box-shadow: var(--mfb-shadow-lg);
|
|
2399
|
+
/* 720px is the "page that opens" sweet spot \u2014 wide enough to feel
|
|
2400
|
+
like a workspace, narrow enough to not dominate the screen. Was
|
|
2401
|
+
440px before; the bump trades a denser modal for a calmer canvas. */
|
|
2402
|
+
width: min(720px, calc(100vw - var(--mfb-space-7)));
|
|
2403
|
+
padding: var(--mfb-space-6);
|
|
2071
2404
|
display: flex;
|
|
2072
2405
|
flex-direction: column;
|
|
2073
|
-
|
|
2074
|
-
field group, .field uses a tighter 6px label-to-input gap. */
|
|
2075
|
-
gap: 18px;
|
|
2406
|
+
gap: var(--mfb-space-5);
|
|
2076
2407
|
position: relative;
|
|
2077
|
-
/* Cap
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
max-height: calc(100vh - 32px);
|
|
2408
|
+
/* Cap height with breathing room. The form scrolls internally if
|
|
2409
|
+
the user attaches a tall screenshot. */
|
|
2410
|
+
max-height: min(820px, calc(100vh - var(--mfb-space-7)));
|
|
2081
2411
|
overflow-y: auto;
|
|
2412
|
+
/* Entrance: opacity-only. Avoids any geometric shift during the
|
|
2413
|
+
animation so interaction tests that read boundingBox immediately
|
|
2414
|
+
after the modal becomes visible get stable coordinates. 220ms
|
|
2415
|
+
ease-out is snappy enough that human users barely register it. */
|
|
2416
|
+
animation: mfb-modal-in 220ms ease-out;
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
@keyframes mfb-modal-in {
|
|
2420
|
+
from { opacity: 0; }
|
|
2421
|
+
to { opacity: 1; }
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
/* Mobile: full-height sheet that slides up from the bottom. The
|
|
2425
|
+
slide-up gesture matches platform-native sheets users already know. */
|
|
2426
|
+
@media (max-width: 640px) {
|
|
2427
|
+
.backdrop { place-items: end center; }
|
|
2428
|
+
.modal {
|
|
2429
|
+
width: 100vw;
|
|
2430
|
+
height: calc(100vh - var(--mfb-space-7));
|
|
2431
|
+
max-height: none;
|
|
2432
|
+
border-radius: var(--mfb-radius-lg) var(--mfb-radius-lg) 0 0;
|
|
2433
|
+
padding: var(--mfb-space-5) var(--mfb-space-4) var(--mfb-space-4);
|
|
2434
|
+
animation: mfb-sheet-up 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
2435
|
+
}
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
@keyframes mfb-sheet-up {
|
|
2439
|
+
from { transform: translateY(100%); }
|
|
2440
|
+
to { transform: translateY(0); }
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
@media (prefers-reduced-motion: reduce) {
|
|
2444
|
+
.backdrop, .modal { animation: none; }
|
|
2082
2445
|
}
|
|
2083
2446
|
|
|
2084
2447
|
.modal h2 {
|
|
2085
2448
|
margin: 0;
|
|
2086
|
-
font-size:
|
|
2449
|
+
font-size: var(--mfb-text-xl);
|
|
2087
2450
|
font-weight: 600;
|
|
2088
|
-
padding-right:
|
|
2089
|
-
letter-spacing: -0.
|
|
2451
|
+
padding-right: var(--mfb-space-6);
|
|
2452
|
+
letter-spacing: -0.015em;
|
|
2453
|
+
line-height: 1.2;
|
|
2090
2454
|
}
|
|
2091
2455
|
|
|
2092
|
-
/* The form sits inside .modal as a single flex child, so the modal-level
|
|
2093
|
-
gap doesn't separate the form's *own* children. Mirror the column +
|
|
2094
|
-
gap pattern on the form itself; 22px lands in the middle of the
|
|
2095
|
-
20-24px field-to-field range the design critic recommended. */
|
|
2096
2456
|
.modal form {
|
|
2097
2457
|
display: flex;
|
|
2098
2458
|
flex-direction: column;
|
|
2099
|
-
gap:
|
|
2459
|
+
gap: var(--mfb-space-5);
|
|
2100
2460
|
}
|
|
2101
2461
|
|
|
2102
2462
|
.modal-close {
|
|
2103
2463
|
position: absolute;
|
|
2104
|
-
top:
|
|
2105
|
-
right:
|
|
2106
|
-
width:
|
|
2107
|
-
height:
|
|
2464
|
+
top: var(--mfb-space-3);
|
|
2465
|
+
right: var(--mfb-space-3);
|
|
2466
|
+
width: 36px;
|
|
2467
|
+
height: 36px;
|
|
2108
2468
|
display: grid;
|
|
2109
2469
|
place-items: center;
|
|
2110
2470
|
background: transparent;
|
|
@@ -2115,36 +2475,39 @@ var WIDGET_STYLES = `
|
|
|
2115
2475
|
font-size: 22px;
|
|
2116
2476
|
line-height: 1;
|
|
2117
2477
|
cursor: pointer;
|
|
2478
|
+
transition: background 120ms ease, color 120ms ease;
|
|
2118
2479
|
}
|
|
2119
2480
|
.modal-close:hover { background: var(--mfb-surface); color: var(--mfb-text); }
|
|
2120
2481
|
.modal-close:focus-visible { outline: 2px solid var(--mfb-accent); outline-offset: 2px; }
|
|
2121
2482
|
|
|
2122
|
-
/* Each field group: label
|
|
2123
|
-
|
|
2124
|
-
.field { display: flex; flex-direction: column; gap:
|
|
2483
|
+
/* Each field group: label + input stacked with breathing room.
|
|
2484
|
+
The 24px modal-level gap separates groups. */
|
|
2485
|
+
.field { display: flex; flex-direction: column; gap: var(--mfb-space-2); font-size: var(--mfb-text-sm); }
|
|
2125
2486
|
|
|
2126
2487
|
.field label {
|
|
2127
2488
|
color: var(--mfb-text-muted);
|
|
2128
2489
|
font-weight: 500;
|
|
2129
|
-
font-size:
|
|
2130
|
-
letter-spacing: 0.
|
|
2490
|
+
font-size: var(--mfb-text-xs);
|
|
2491
|
+
letter-spacing: 0.03em;
|
|
2492
|
+
text-transform: uppercase;
|
|
2131
2493
|
}
|
|
2132
2494
|
|
|
2133
2495
|
.field input, .field select, .field textarea {
|
|
2134
2496
|
font-family: inherit;
|
|
2135
|
-
font-size:
|
|
2497
|
+
font-size: var(--mfb-text-base);
|
|
2136
2498
|
color: inherit;
|
|
2137
|
-
padding:
|
|
2499
|
+
padding: 11px 14px;
|
|
2138
2500
|
border: 1px solid var(--mfb-border);
|
|
2139
2501
|
border-radius: var(--mfb-radius);
|
|
2140
2502
|
background: var(--mfb-surface);
|
|
2141
|
-
transition: border-color 120ms ease, box-shadow 120ms ease;
|
|
2503
|
+
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
2142
2504
|
}
|
|
2143
2505
|
|
|
2144
|
-
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--mfb-
|
|
2506
|
+
.field input:hover, .field select:hover, .field textarea:hover { border-color: var(--mfb-border-strong); }
|
|
2145
2507
|
.field input:focus, .field select:focus, .field textarea:focus {
|
|
2146
2508
|
outline: none;
|
|
2147
2509
|
border-color: var(--mfb-accent);
|
|
2510
|
+
background: var(--mfb-bg);
|
|
2148
2511
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--mfb-accent) 22%, transparent);
|
|
2149
2512
|
}
|
|
2150
2513
|
|
|
@@ -2158,34 +2521,35 @@ var WIDGET_STYLES = `
|
|
|
2158
2521
|
background-position: right 10px center;
|
|
2159
2522
|
}
|
|
2160
2523
|
|
|
2161
|
-
.field textarea { min-height:
|
|
2524
|
+
.field textarea { min-height: 120px; resize: vertical; line-height: 1.5; }
|
|
2162
2525
|
|
|
2163
|
-
.row { display: flex; gap:
|
|
2526
|
+
.row { display: flex; gap: var(--mfb-space-3); }
|
|
2164
2527
|
.row > * { flex: 1; }
|
|
2165
2528
|
|
|
2166
|
-
/* Footer separation
|
|
2167
|
-
Stops Cancel/Send from floating against the dropzone or page-URL footer. */
|
|
2529
|
+
/* Footer: subtle separation via border, slightly more vertical space. */
|
|
2168
2530
|
.actions {
|
|
2169
2531
|
display: flex;
|
|
2170
|
-
gap:
|
|
2532
|
+
gap: var(--mfb-space-2);
|
|
2171
2533
|
justify-content: flex-end;
|
|
2172
|
-
padding-top:
|
|
2173
|
-
margin-top:
|
|
2534
|
+
padding-top: var(--mfb-space-4);
|
|
2535
|
+
margin-top: var(--mfb-space-1);
|
|
2174
2536
|
border-top: 1px solid var(--mfb-border);
|
|
2175
2537
|
}
|
|
2176
2538
|
|
|
2177
2539
|
.btn {
|
|
2178
|
-
padding:
|
|
2540
|
+
padding: 10px 18px;
|
|
2179
2541
|
border-radius: var(--mfb-radius);
|
|
2180
2542
|
border: 1px solid var(--mfb-border);
|
|
2181
2543
|
background: var(--mfb-bg);
|
|
2182
2544
|
color: var(--mfb-text);
|
|
2183
2545
|
font: inherit;
|
|
2546
|
+
font-size: var(--mfb-text-sm);
|
|
2184
2547
|
font-weight: 500;
|
|
2185
2548
|
cursor: pointer;
|
|
2186
|
-
transition: background 120ms ease, border-color 120ms ease;
|
|
2549
|
+
transition: background 120ms ease, border-color 120ms ease, transform 80ms ease;
|
|
2187
2550
|
}
|
|
2188
|
-
.btn:hover { background: var(--mfb-surface); }
|
|
2551
|
+
.btn:hover { background: var(--mfb-surface); border-color: var(--mfb-border-strong); }
|
|
2552
|
+
.btn:active { transform: scale(0.98); }
|
|
2189
2553
|
|
|
2190
2554
|
.btn--primary {
|
|
2191
2555
|
background: var(--mfb-accent);
|
|
@@ -2216,28 +2580,49 @@ var WIDGET_STYLES = `
|
|
|
2216
2580
|
border: 0;
|
|
2217
2581
|
}
|
|
2218
2582
|
|
|
2583
|
+
/* Dropzone \u2014 bigger, more inviting; the icon + heading + sub-line
|
|
2584
|
+
hierarchy makes it feel like a deliberate target, not an afterthought. */
|
|
2219
2585
|
.screenshot-dropzone {
|
|
2220
|
-
border:
|
|
2221
|
-
border-radius: var(--mfb-radius);
|
|
2222
|
-
padding:
|
|
2586
|
+
border: 1.5px dashed var(--mfb-border-strong);
|
|
2587
|
+
border-radius: var(--mfb-radius-lg);
|
|
2588
|
+
padding: var(--mfb-space-6) var(--mfb-space-4);
|
|
2223
2589
|
text-align: center;
|
|
2224
2590
|
cursor: pointer;
|
|
2225
2591
|
background: var(--mfb-surface);
|
|
2226
|
-
transition: border-color
|
|
2592
|
+
transition: border-color 160ms ease, background 160ms ease, transform 120ms ease;
|
|
2593
|
+
display: flex;
|
|
2594
|
+
flex-direction: column;
|
|
2595
|
+
align-items: center;
|
|
2596
|
+
gap: var(--mfb-space-2);
|
|
2597
|
+
}
|
|
2598
|
+
.screenshot-dropzone:hover {
|
|
2599
|
+
border-color: var(--mfb-accent);
|
|
2600
|
+
background: color-mix(in srgb, var(--mfb-accent) 4%, var(--mfb-surface));
|
|
2227
2601
|
}
|
|
2228
|
-
.screenshot-dropzone:hover { border-color: var(--mfb-text-muted); }
|
|
2229
2602
|
.screenshot-dropzone.is-dragover {
|
|
2230
2603
|
border-color: var(--mfb-accent);
|
|
2231
2604
|
border-style: solid;
|
|
2232
|
-
background: color-mix(in srgb, var(--mfb-accent)
|
|
2605
|
+
background: color-mix(in srgb, var(--mfb-accent) 10%, var(--mfb-surface));
|
|
2606
|
+
transform: scale(1.005);
|
|
2233
2607
|
}
|
|
2234
2608
|
.screenshot-dropzone:focus-visible {
|
|
2235
2609
|
outline: 2px solid var(--mfb-accent);
|
|
2236
2610
|
outline-offset: 2px;
|
|
2237
2611
|
}
|
|
2238
|
-
.screenshot-
|
|
2612
|
+
.screenshot-icon {
|
|
2613
|
+
color: var(--mfb-text-muted);
|
|
2614
|
+
opacity: 0.7;
|
|
2615
|
+
margin-bottom: var(--mfb-space-1);
|
|
2616
|
+
transition: color 160ms ease, opacity 160ms ease;
|
|
2617
|
+
}
|
|
2618
|
+
.screenshot-dropzone:hover .screenshot-icon,
|
|
2619
|
+
.screenshot-dropzone.is-dragover .screenshot-icon {
|
|
2620
|
+
color: var(--mfb-accent);
|
|
2621
|
+
opacity: 1;
|
|
2622
|
+
}
|
|
2623
|
+
.screenshot-cta { font-size: var(--mfb-text-base); color: var(--mfb-text); font-weight: 500; }
|
|
2239
2624
|
.screenshot-cta strong { color: var(--mfb-accent); font-weight: 600; }
|
|
2240
|
-
.screenshot-formats { font-size:
|
|
2625
|
+
.screenshot-formats { font-size: var(--mfb-text-xs); color: var(--mfb-text-muted); }
|
|
2241
2626
|
|
|
2242
2627
|
.screenshot-preview {
|
|
2243
2628
|
position: relative;
|
|
@@ -2252,11 +2637,38 @@ var WIDGET_STYLES = `
|
|
|
2252
2637
|
display: block;
|
|
2253
2638
|
width: 100%;
|
|
2254
2639
|
height: auto;
|
|
2255
|
-
max-height:
|
|
2640
|
+
max-height: 280px;
|
|
2256
2641
|
object-fit: contain;
|
|
2257
2642
|
background: #1a1a1a;
|
|
2258
2643
|
}
|
|
2644
|
+
.screenshot-preview-actions {
|
|
2645
|
+
display: flex;
|
|
2646
|
+
gap: var(--mfb-space-2);
|
|
2647
|
+
padding: var(--mfb-space-2);
|
|
2648
|
+
border-top: 1px solid var(--mfb-border);
|
|
2649
|
+
background: var(--mfb-bg);
|
|
2650
|
+
}
|
|
2651
|
+
.screenshot-preview-actions .btn {
|
|
2652
|
+
flex: 1;
|
|
2653
|
+
padding: 8px 12px;
|
|
2654
|
+
font-size: var(--mfb-text-sm);
|
|
2655
|
+
display: inline-flex;
|
|
2656
|
+
align-items: center;
|
|
2657
|
+
justify-content: center;
|
|
2658
|
+
gap: 6px;
|
|
2659
|
+
}
|
|
2660
|
+
.screenshot-preview-actions .btn--primary {
|
|
2661
|
+
background: var(--mfb-bg);
|
|
2662
|
+
color: var(--mfb-accent);
|
|
2663
|
+
border-color: var(--mfb-accent);
|
|
2664
|
+
}
|
|
2665
|
+
.screenshot-preview-actions .btn--primary:hover {
|
|
2666
|
+
background: color-mix(in srgb, var(--mfb-accent) 8%, var(--mfb-bg));
|
|
2667
|
+
border-color: var(--mfb-accent);
|
|
2668
|
+
color: var(--mfb-accent);
|
|
2669
|
+
}
|
|
2259
2670
|
.screenshot-remove {
|
|
2671
|
+
/* Kept for legacy markup that uses the old corner-cross button. */
|
|
2260
2672
|
position: absolute;
|
|
2261
2673
|
top: 6px;
|
|
2262
2674
|
right: 6px;
|
|
@@ -2274,6 +2686,7 @@ var WIDGET_STYLES = `
|
|
|
2274
2686
|
}
|
|
2275
2687
|
.screenshot-remove:hover { background: #fff; }
|
|
2276
2688
|
.screenshot-annotate {
|
|
2689
|
+
/* Kept for legacy; new markup uses .screenshot-preview-actions. */
|
|
2277
2690
|
border-radius: 0;
|
|
2278
2691
|
border-width: 0;
|
|
2279
2692
|
border-top: 1px solid var(--mfb-border);
|
|
@@ -2377,11 +2790,31 @@ var WIDGET_STYLES = `
|
|
|
2377
2790
|
cursor: pointer;
|
|
2378
2791
|
padding: 0;
|
|
2379
2792
|
transition: transform 120ms ease;
|
|
2793
|
+
position: relative;
|
|
2380
2794
|
}
|
|
2381
2795
|
.annotator-color.is-active {
|
|
2382
2796
|
transform: scale(1.12);
|
|
2383
2797
|
border-color: var(--mfb-text);
|
|
2384
2798
|
}
|
|
2799
|
+
/* Color-picker swatch: rainbow gradient fill so users see this isn't
|
|
2800
|
+
a preset, plus a tiny native <input type="color"> overlaid invisibly. */
|
|
2801
|
+
.annotator-color-picker {
|
|
2802
|
+
display: grid;
|
|
2803
|
+
place-items: center;
|
|
2804
|
+
background: conic-gradient(from 180deg, #ef4444, #f59e0b, #10b981, #3b82f6, #8b5cf6, #ec4899, #ef4444);
|
|
2805
|
+
overflow: hidden;
|
|
2806
|
+
}
|
|
2807
|
+
.annotator-color-picker input[type="color"] {
|
|
2808
|
+
position: absolute;
|
|
2809
|
+
inset: 0;
|
|
2810
|
+
width: 100%;
|
|
2811
|
+
height: 100%;
|
|
2812
|
+
border: 0;
|
|
2813
|
+
padding: 0;
|
|
2814
|
+
margin: 0;
|
|
2815
|
+
opacity: 0;
|
|
2816
|
+
cursor: pointer;
|
|
2817
|
+
}
|
|
2385
2818
|
.annotator-btn {
|
|
2386
2819
|
display: inline-flex;
|
|
2387
2820
|
align-items: center;
|
|
@@ -2431,37 +2864,39 @@ var WIDGET_STYLES = `
|
|
|
2431
2864
|
|
|
2432
2865
|
.tab-strip {
|
|
2433
2866
|
display: flex;
|
|
2434
|
-
gap:
|
|
2867
|
+
gap: var(--mfb-space-1);
|
|
2435
2868
|
border-bottom: 1px solid var(--mfb-border);
|
|
2436
|
-
margin: 0
|
|
2437
|
-
padding: 0
|
|
2869
|
+
margin: 0;
|
|
2870
|
+
padding: 0;
|
|
2438
2871
|
}
|
|
2439
2872
|
.tab-button {
|
|
2440
2873
|
appearance: none;
|
|
2441
2874
|
background: transparent;
|
|
2442
2875
|
border: 0;
|
|
2443
2876
|
border-bottom: 2px solid transparent;
|
|
2444
|
-
padding:
|
|
2877
|
+
padding: var(--mfb-space-3) var(--mfb-space-4);
|
|
2878
|
+
margin-bottom: -1px;
|
|
2445
2879
|
font: inherit;
|
|
2446
|
-
font-size:
|
|
2880
|
+
font-size: var(--mfb-text-sm);
|
|
2447
2881
|
font-weight: 500;
|
|
2448
2882
|
color: var(--mfb-text-muted);
|
|
2449
2883
|
cursor: pointer;
|
|
2884
|
+
transition: color 120ms ease, border-color 120ms ease;
|
|
2450
2885
|
}
|
|
2451
2886
|
.tab-button:hover { color: var(--mfb-text); }
|
|
2452
2887
|
.tab-button.is-active {
|
|
2453
|
-
color: var(--mfb-
|
|
2888
|
+
color: var(--mfb-accent);
|
|
2454
2889
|
border-bottom-color: var(--mfb-accent);
|
|
2455
2890
|
}
|
|
2456
2891
|
.tab-button[aria-selected="true"] { font-weight: 600; }
|
|
2457
2892
|
|
|
2458
|
-
.mine-list { display: flex; flex-direction: column; gap:
|
|
2893
|
+
.mine-list { display: flex; flex-direction: column; gap: var(--mfb-space-3); }
|
|
2459
2894
|
.mine-list-header {
|
|
2460
2895
|
display: flex;
|
|
2461
2896
|
align-items: center;
|
|
2462
2897
|
justify-content: space-between;
|
|
2463
2898
|
}
|
|
2464
|
-
.mine-list-header h2 { margin: 0; font-size:
|
|
2899
|
+
.mine-list-header h2 { margin: 0; font-size: var(--mfb-text-lg); font-weight: 600; letter-spacing: -0.01em; }
|
|
2465
2900
|
.mine-loading { color: var(--mfb-text-muted); font-size: 13px; }
|
|
2466
2901
|
.mine-empty {
|
|
2467
2902
|
text-align: center;
|
|
@@ -3142,8 +3577,8 @@ function createFeedback(config) {
|
|
|
3142
3577
|
capture_method: screenshot ? manualScreenshot ? "manual" : "html2canvas" : "none",
|
|
3143
3578
|
technical_context
|
|
3144
3579
|
};
|
|
3145
|
-
if ("0.
|
|
3146
|
-
payload.widget_version = "0.
|
|
3580
|
+
if ("0.11.0") {
|
|
3581
|
+
payload.widget_version = "0.11.0";
|
|
3147
3582
|
}
|
|
3148
3583
|
if (screenshot) payload.screenshot = screenshot;
|
|
3149
3584
|
if (values.synthetic) payload.synthetic = true;
|
|
@@ -3229,4 +3664,4 @@ function createFeedback(config) {
|
|
|
3229
3664
|
export {
|
|
3230
3665
|
createFeedback
|
|
3231
3666
|
};
|
|
3232
|
-
//# sourceMappingURL=chunk-
|
|
3667
|
+
//# sourceMappingURL=chunk-AO3BXEG5.mjs.map
|