qapture2 0.2.4 → 0.3.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/README.md +145 -23
- package/dist/bin/init.cjs +31 -266
- package/dist/{chunk-DPJW626S.cjs → chunk-6KAXMN77.cjs} +2269 -993
- package/dist/{chunk-RC7ZUQ5X.js → chunk-ESRQOU32.js} +2270 -994
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +27 -5
- package/dist/index.d.ts +27 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +1 -1
|
@@ -81,7 +81,7 @@ function createIdb(namespace) {
|
|
|
81
81
|
if (!isIdbAvailable()) {
|
|
82
82
|
return {
|
|
83
83
|
getAll: () => Promise.resolve([]),
|
|
84
|
-
put: () => Promise.resolve(),
|
|
84
|
+
put: () => Promise.resolve(true),
|
|
85
85
|
delete: () => Promise.resolve(),
|
|
86
86
|
clear: () => Promise.resolve()
|
|
87
87
|
};
|
|
@@ -98,7 +98,9 @@ function createIdb(namespace) {
|
|
|
98
98
|
put: async (record) => {
|
|
99
99
|
try {
|
|
100
100
|
await run(dbName, NOTES_STORE, "readwrite", (s) => s.put(record));
|
|
101
|
+
return true;
|
|
101
102
|
} catch {
|
|
103
|
+
return false;
|
|
102
104
|
}
|
|
103
105
|
},
|
|
104
106
|
delete: async (id) => {
|
|
@@ -139,25 +141,6 @@ function deleteQaDatabase(namespace) {
|
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
// src/config/schema.ts
|
|
142
|
-
var DEFAULT_THEME = {
|
|
143
|
-
primary: "#4f46e5",
|
|
144
|
-
// indigo-600
|
|
145
|
-
primaryDark: "#3730a3",
|
|
146
|
-
// indigo-800
|
|
147
|
-
accent: "#7c3aed",
|
|
148
|
-
// violet-600
|
|
149
|
-
accentDark: "#6d28d9",
|
|
150
|
-
// violet-700
|
|
151
|
-
sage: "#6b7280",
|
|
152
|
-
// gray-500
|
|
153
|
-
cream: "#f8fafc",
|
|
154
|
-
// slate-50
|
|
155
|
-
mauve: "#a78bfa",
|
|
156
|
-
// violet-400
|
|
157
|
-
surface: "#ffffff",
|
|
158
|
-
ink: "#1f2937"
|
|
159
|
-
// gray-800
|
|
160
|
-
};
|
|
161
144
|
var DEFAULTS = {
|
|
162
145
|
namespace: "qapture",
|
|
163
146
|
brandLabel: "Qapture",
|
|
@@ -165,7 +148,8 @@ var DEFAULTS = {
|
|
|
165
148
|
rtl: false,
|
|
166
149
|
visible: void 0,
|
|
167
150
|
alwaysVisible: false,
|
|
168
|
-
hotkey: "shift+alt+q"
|
|
151
|
+
hotkey: "shift+alt+q",
|
|
152
|
+
captureContext: true
|
|
169
153
|
};
|
|
170
154
|
var VALID_RISKS = /* @__PURE__ */ new Set(["red", "amber", "green"]);
|
|
171
155
|
function isNonEmptyString(v) {
|
|
@@ -182,28 +166,6 @@ function isValidBilingual(v) {
|
|
|
182
166
|
}
|
|
183
167
|
return false;
|
|
184
168
|
}
|
|
185
|
-
function coerceTheme(input) {
|
|
186
|
-
if (!input || typeof input !== "object") return { ...DEFAULT_THEME };
|
|
187
|
-
const out = { ...DEFAULT_THEME };
|
|
188
|
-
const keys = [
|
|
189
|
-
"primary",
|
|
190
|
-
"primaryDark",
|
|
191
|
-
"accent",
|
|
192
|
-
"accentDark",
|
|
193
|
-
"sage",
|
|
194
|
-
"cream",
|
|
195
|
-
"mauve",
|
|
196
|
-
"surface",
|
|
197
|
-
"ink"
|
|
198
|
-
];
|
|
199
|
-
for (const k of keys) {
|
|
200
|
-
const v = input[k];
|
|
201
|
-
if (typeof v === "string" && v.trim().length > 0) {
|
|
202
|
-
out[k] = v.trim();
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
207
169
|
function coerceCredentials(raw, warnings) {
|
|
208
170
|
if (!Array.isArray(raw)) return [];
|
|
209
171
|
const out = [];
|
|
@@ -280,6 +242,13 @@ function coerceJourney(raw, warnings) {
|
|
|
280
242
|
path: s["path"].trim(),
|
|
281
243
|
what: s["what"]
|
|
282
244
|
};
|
|
245
|
+
if (s["expect"] !== void 0) {
|
|
246
|
+
if (isValidBilingual(s["expect"])) {
|
|
247
|
+
step.expect = s["expect"];
|
|
248
|
+
} else {
|
|
249
|
+
warnings.push(`journey[${i}].steps[${j}] (path="${String(s["path"])}"): invalid "expect" \u2014 ignored`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
283
252
|
if (s["risk"] !== void 0) {
|
|
284
253
|
if (VALID_RISKS.has(s["risk"])) {
|
|
285
254
|
step.risk = s["risk"];
|
|
@@ -316,7 +285,6 @@ function validateConfig(input) {
|
|
|
316
285
|
return {
|
|
317
286
|
config: {
|
|
318
287
|
namespace: DEFAULTS.namespace,
|
|
319
|
-
theme: { ...DEFAULT_THEME },
|
|
320
288
|
brand: { label: DEFAULTS.brandLabel },
|
|
321
289
|
loginField: { ...DEFAULTS.loginField },
|
|
322
290
|
credentials: [],
|
|
@@ -325,7 +293,8 @@ function validateConfig(input) {
|
|
|
325
293
|
rtl: DEFAULTS.rtl,
|
|
326
294
|
visible: DEFAULTS.visible,
|
|
327
295
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
328
|
-
hotkey: DEFAULTS.hotkey
|
|
296
|
+
hotkey: DEFAULTS.hotkey,
|
|
297
|
+
captureContext: DEFAULTS.captureContext
|
|
329
298
|
},
|
|
330
299
|
warnings
|
|
331
300
|
};
|
|
@@ -335,7 +304,6 @@ function validateConfig(input) {
|
|
|
335
304
|
return {
|
|
336
305
|
config: {
|
|
337
306
|
namespace: DEFAULTS.namespace,
|
|
338
|
-
theme: { ...DEFAULT_THEME },
|
|
339
307
|
brand: { label: DEFAULTS.brandLabel },
|
|
340
308
|
loginField: { ...DEFAULTS.loginField },
|
|
341
309
|
credentials: [],
|
|
@@ -344,14 +312,19 @@ function validateConfig(input) {
|
|
|
344
312
|
rtl: DEFAULTS.rtl,
|
|
345
313
|
visible: DEFAULTS.visible,
|
|
346
314
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
347
|
-
hotkey: DEFAULTS.hotkey
|
|
315
|
+
hotkey: DEFAULTS.hotkey,
|
|
316
|
+
captureContext: DEFAULTS.captureContext
|
|
348
317
|
},
|
|
349
318
|
warnings
|
|
350
319
|
};
|
|
351
320
|
}
|
|
352
321
|
const raw = input;
|
|
353
322
|
const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
|
|
354
|
-
|
|
323
|
+
if (raw["theme"] !== void 0) {
|
|
324
|
+
warnings.push(
|
|
325
|
+
'theme: custom themes were removed in Qapture 0.3.0 \u2014 the widget now ships one fixed, self-contained design. The "theme" key is ignored; remove it from your qa.config to silence this warning.'
|
|
326
|
+
);
|
|
327
|
+
}
|
|
355
328
|
let brandLabel = DEFAULTS.brandLabel;
|
|
356
329
|
if (raw["brand"] !== void 0 && raw["brand"] !== null && typeof raw["brand"] === "object") {
|
|
357
330
|
const b = raw["brand"];
|
|
@@ -373,6 +346,7 @@ function validateConfig(input) {
|
|
|
373
346
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
374
347
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
375
348
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
349
|
+
const captureContext = typeof raw["captureContext"] === "boolean" ? raw["captureContext"] : DEFAULTS.captureContext;
|
|
376
350
|
let visible = DEFAULTS.visible;
|
|
377
351
|
if (raw["visible"] !== void 0) {
|
|
378
352
|
if (typeof raw["visible"] === "boolean") {
|
|
@@ -384,7 +358,6 @@ function validateConfig(input) {
|
|
|
384
358
|
return {
|
|
385
359
|
config: {
|
|
386
360
|
namespace,
|
|
387
|
-
theme,
|
|
388
361
|
brand: { label: brandLabel },
|
|
389
362
|
loginField,
|
|
390
363
|
credentials,
|
|
@@ -393,7 +366,8 @@ function validateConfig(input) {
|
|
|
393
366
|
rtl,
|
|
394
367
|
visible,
|
|
395
368
|
alwaysVisible,
|
|
396
|
-
hotkey
|
|
369
|
+
hotkey,
|
|
370
|
+
captureContext
|
|
397
371
|
},
|
|
398
372
|
warnings
|
|
399
373
|
};
|
|
@@ -404,6 +378,103 @@ var QA_CSS = `
|
|
|
404
378
|
/* \u2500\u2500 Reset \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
405
379
|
*, *::before, *::after { box-sizing: border-box; }
|
|
406
380
|
|
|
381
|
+
/* \u2500\u2500 Design tokens (Graphite \u2014 v0.3.0) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
382
|
+
Single source of truth for every colour, shadow, radius, font, motion
|
|
383
|
+
duration, and z-index the widget uses. Nothing here is themeable \u2014
|
|
384
|
+
qapture 0.3.0 ships one fixed, self-contained design. */
|
|
385
|
+
:host {
|
|
386
|
+
/* Surfaces */
|
|
387
|
+
--qa-surface-0: #101215;
|
|
388
|
+
--qa-surface-1: #181B20;
|
|
389
|
+
--qa-surface-2: #20242B;
|
|
390
|
+
--qa-surface-3: #2A2F37;
|
|
391
|
+
|
|
392
|
+
/* Ink */
|
|
393
|
+
--qa-ink-hi: #F4F5F7;
|
|
394
|
+
--qa-ink-mid: #A8AEB8;
|
|
395
|
+
--qa-ink-lo: #6B717C;
|
|
396
|
+
--qa-ink-faint: #4A4F58;
|
|
397
|
+
|
|
398
|
+
/* Accent */
|
|
399
|
+
--qa-accent: #4D9CFF;
|
|
400
|
+
--qa-accent-hover: #6FB0FF;
|
|
401
|
+
--qa-accent-active: #3B84E6;
|
|
402
|
+
--qa-on-accent: #0A0C10;
|
|
403
|
+
--qa-accent-tint: rgba(77,156,255,0.14);
|
|
404
|
+
--qa-accent-border: rgba(77,156,255,0.45);
|
|
405
|
+
|
|
406
|
+
/* Semantic */
|
|
407
|
+
--qa-danger: #FF6B6B;
|
|
408
|
+
--qa-danger-tint: rgba(255,107,107,0.14);
|
|
409
|
+
--qa-warn: #FBBF24;
|
|
410
|
+
--qa-warn-tint: rgba(251,191,36,0.14);
|
|
411
|
+
--qa-success: #34D399;
|
|
412
|
+
--qa-success-tint: rgba(52,211,153,0.14);
|
|
413
|
+
--qa-neutral: #5B616B;
|
|
414
|
+
|
|
415
|
+
/* Borders */
|
|
416
|
+
--qa-border-subtle: rgba(255,255,255,0.08);
|
|
417
|
+
--qa-border-strong: rgba(255,255,255,0.14);
|
|
418
|
+
|
|
419
|
+
/* Scrims */
|
|
420
|
+
--qa-scrim-dialog: rgba(8,9,12,0.50);
|
|
421
|
+
--qa-scrim-capture: rgba(8,9,12,0.32);
|
|
422
|
+
--qa-scrim-spot: rgba(8,9,12,0.55);
|
|
423
|
+
|
|
424
|
+
/* Elevation */
|
|
425
|
+
--qa-sheen: inset 0 1px 0 rgba(255,255,255,0.06);
|
|
426
|
+
--qa-elev-1: 0 1px 2px rgba(0,0,0,0.40);
|
|
427
|
+
--qa-elev-2: 0 8px 24px -8px rgba(0,0,0,0.55);
|
|
428
|
+
--qa-elev-3: 0 24px 60px -16px rgba(0,0,0,0.65);
|
|
429
|
+
|
|
430
|
+
/* Radius */
|
|
431
|
+
--qa-radius-sm: 6px;
|
|
432
|
+
--qa-radius-md: 10px;
|
|
433
|
+
--qa-radius-lg: 14px;
|
|
434
|
+
|
|
435
|
+
/* Fonts (same stack for Arabic \u2014 no separate Arabic typeface) */
|
|
436
|
+
--qa-font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
437
|
+
--qa-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
438
|
+
|
|
439
|
+
/* Motion */
|
|
440
|
+
--qa-dur-1: 120ms;
|
|
441
|
+
--qa-dur-2: 180ms;
|
|
442
|
+
--qa-dur-3: 240ms;
|
|
443
|
+
--qa-ease: cubic-bezier(0.4,0,0.2,1);
|
|
444
|
+
--qa-ease-out: cubic-bezier(0.16,1,0.3,1);
|
|
445
|
+
|
|
446
|
+
/* Z-index scale */
|
|
447
|
+
--qa-z-fab: 9990;
|
|
448
|
+
--qa-z-panel: 9995;
|
|
449
|
+
--qa-z-capture-dim: 10090;
|
|
450
|
+
--qa-z-capture-highlight: 10092;
|
|
451
|
+
--qa-z-capture-region-move: 10093;
|
|
452
|
+
--qa-z-capture-region-handle: 10094;
|
|
453
|
+
--qa-z-capture-hint: 10095;
|
|
454
|
+
--qa-z-capture-ui: 10096;
|
|
455
|
+
--qa-z-toast: 10097;
|
|
456
|
+
|
|
457
|
+
font-family: var(--qa-font);
|
|
458
|
+
color: var(--qa-ink-hi);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Respect the user's OS-level motion preference: kill durations everywhere,
|
|
462
|
+
including the token defaults so any var(--qa-dur-*)-based rule inherits
|
|
463
|
+
the kill for free. */
|
|
464
|
+
@media (prefers-reduced-motion: reduce) {
|
|
465
|
+
:host {
|
|
466
|
+
--qa-dur-1: 0ms;
|
|
467
|
+
--qa-dur-2: 0ms;
|
|
468
|
+
--qa-dur-3: 0ms;
|
|
469
|
+
}
|
|
470
|
+
*, *::before, *::after {
|
|
471
|
+
animation-duration: 0.01ms !important;
|
|
472
|
+
animation-iteration-count: 1 !important;
|
|
473
|
+
transition-duration: 0.01ms !important;
|
|
474
|
+
scroll-behavior: auto !important;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
407
478
|
/* \u2500\u2500 Position \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
408
479
|
.qa-fixed { position: fixed; }
|
|
409
480
|
.qa-absolute { position: absolute; }
|
|
@@ -419,17 +490,18 @@ var QA_CSS = `
|
|
|
419
490
|
.qa-left-half { left: 50%; }
|
|
420
491
|
.qa-right-0 { right: 0; }
|
|
421
492
|
|
|
422
|
-
/* z-index
|
|
493
|
+
/* z-index \u2014 values mirror the --qa-z-* tokens above; class NAMES are kept
|
|
494
|
+
verbatim (scripts/browser-test.mjs string-matches .qa-z-10093/.qa-z-10094). */
|
|
423
495
|
.qa-z-1 { z-index: 1; }
|
|
424
496
|
.qa-z-50 { z-index: 50; }
|
|
425
497
|
.qa-z-100 { z-index: 100; }
|
|
426
|
-
.qa-z-10090 { z-index:
|
|
427
|
-
.qa-z-10092 { z-index:
|
|
498
|
+
.qa-z-10090 { z-index: var(--qa-z-capture-dim); }
|
|
499
|
+
.qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
|
|
428
500
|
/* region-handle layering */
|
|
429
|
-
.qa-z-10093 { z-index:
|
|
430
|
-
.qa-z-10094 { z-index:
|
|
431
|
-
.qa-z-10095 { z-index:
|
|
432
|
-
.qa-z-10096 { z-index:
|
|
501
|
+
.qa-z-10093 { z-index: var(--qa-z-capture-region-move); }
|
|
502
|
+
.qa-z-10094 { z-index: var(--qa-z-capture-region-handle); }
|
|
503
|
+
.qa-z-10095 { z-index: var(--qa-z-capture-hint); }
|
|
504
|
+
.qa-z-10096 { z-index: var(--qa-z-capture-ui); }
|
|
433
505
|
|
|
434
506
|
/* \u2500\u2500 Display / Flex \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
435
507
|
.qa-flex { display: flex; }
|
|
@@ -543,7 +615,12 @@ var QA_CSS = `
|
|
|
543
615
|
.qa-border-t { border-top-width: 1px; border-top-style: solid; }
|
|
544
616
|
.qa-border-b { border-bottom-width: 1px; border-bottom-style: solid; }
|
|
545
617
|
.qa-border-white { border-color: #ffffff; }
|
|
546
|
-
.qa-border-white-40 { border-color:
|
|
618
|
+
.qa-border-white-40 { border-color: var(--qa-border-strong); }
|
|
619
|
+
|
|
620
|
+
/* Semantic border colour (combine with .qa-border for width+style) */
|
|
621
|
+
.qa-border-subtle { border-color: var(--qa-border-subtle); }
|
|
622
|
+
.qa-border-strong { border-color: var(--qa-border-strong); }
|
|
623
|
+
.qa-border-accent { border-color: var(--qa-accent-border); }
|
|
547
624
|
|
|
548
625
|
/* \u2500\u2500 Rounded \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
549
626
|
.qa-rounded { border-radius: 0.25rem; }
|
|
@@ -557,6 +634,13 @@ var QA_CSS = `
|
|
|
557
634
|
.qa-shadow-lg { box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -1px rgba(0,0,0,0.06); }
|
|
558
635
|
.qa-shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
|
|
559
636
|
|
|
637
|
+
/* Semantic elevation \u2014 each layers --qa-sheen (a 1px inner highlight) on top
|
|
638
|
+
of the matching --qa-elev-* drop shadow, so raised surfaces read as
|
|
639
|
+
subtly lit from above rather than flat dark rectangles. */
|
|
640
|
+
.qa-elev-1 { box-shadow: var(--qa-elev-1), var(--qa-sheen); }
|
|
641
|
+
.qa-elev-2 { box-shadow: var(--qa-elev-2), var(--qa-sheen); }
|
|
642
|
+
.qa-elev-3 { box-shadow: var(--qa-elev-3), var(--qa-sheen); }
|
|
643
|
+
|
|
560
644
|
/* \u2500\u2500 Typography \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
561
645
|
.qa-text-10 { font-size: 10px; }
|
|
562
646
|
.qa-text-11 { font-size: 11px; }
|
|
@@ -567,7 +651,7 @@ var QA_CSS = `
|
|
|
567
651
|
.qa-font-medium { font-weight: 500; }
|
|
568
652
|
.qa-font-semibold { font-weight: 600; }
|
|
569
653
|
.qa-font-bold { font-weight: 700; }
|
|
570
|
-
.qa-font-mono { font-family:
|
|
654
|
+
.qa-font-mono { font-family: var(--qa-font-mono); }
|
|
571
655
|
.qa-leading-relaxed { line-height: 1.625; }
|
|
572
656
|
.qa-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
573
657
|
.qa-whitespace-pre-wrap { white-space: pre-wrap; }
|
|
@@ -590,19 +674,53 @@ var QA_CSS = `
|
|
|
590
674
|
/* \u2500\u2500 Colors \u2014 text \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
591
675
|
.qa-text-white { color: #ffffff; }
|
|
592
676
|
.qa-text-current { color: currentColor; }
|
|
593
|
-
|
|
594
|
-
.qa-text-slate-
|
|
595
|
-
.qa-text-slate-
|
|
596
|
-
.qa-text-
|
|
597
|
-
.qa-text-
|
|
598
|
-
.qa-text-red-
|
|
677
|
+
/* legacy slate scale, restyled onto the ink levels in place */
|
|
678
|
+
.qa-text-slate-300 { color: var(--qa-ink-faint); }
|
|
679
|
+
.qa-text-slate-400 { color: var(--qa-ink-lo); }
|
|
680
|
+
.qa-text-slate-500 { color: var(--qa-ink-mid); }
|
|
681
|
+
.qa-text-green-600 { color: var(--qa-success); }
|
|
682
|
+
.qa-text-red-500 { color: var(--qa-danger); }
|
|
683
|
+
.qa-text-red-600 { color: var(--qa-danger); }
|
|
684
|
+
|
|
685
|
+
/* Semantic text levels */
|
|
686
|
+
.qa-text-hi { color: var(--qa-ink-hi); }
|
|
687
|
+
.qa-text-mid { color: var(--qa-ink-mid); }
|
|
688
|
+
.qa-text-lo { color: var(--qa-ink-lo); }
|
|
689
|
+
.qa-text-faint { color: var(--qa-ink-faint); }
|
|
690
|
+
.qa-text-accent { color: var(--qa-accent); }
|
|
691
|
+
.qa-text-on-accent { color: var(--qa-on-accent); }
|
|
692
|
+
.qa-text-danger { color: var(--qa-danger); }
|
|
693
|
+
.qa-text-warn { color: var(--qa-warn); }
|
|
694
|
+
.qa-text-success { color: var(--qa-success); }
|
|
599
695
|
|
|
600
696
|
/* \u2500\u2500 Colors \u2014 background \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
601
|
-
|
|
602
|
-
|
|
697
|
+
/* legacy names, restyled onto Graphite tokens in place \u2014 components keep
|
|
698
|
+
using these class names unchanged. */
|
|
699
|
+
.qa-bg-white { background-color: var(--qa-surface-1); }
|
|
700
|
+
.qa-bg-white-25 { background-color: var(--qa-surface-3); }
|
|
603
701
|
.qa-bg-transparent { background-color: transparent; }
|
|
604
|
-
|
|
605
|
-
.
|
|
702
|
+
/* These two were 3%/5% black tints for a light theme, which is inert on a
|
|
703
|
+
dark surface. Restyled as low-alpha WHITE lifts of the same two
|
|
704
|
+
intensities \u2014 still legible as a step above the base surface. */
|
|
705
|
+
.qa-bg-black-3 { background-color: rgba(255,255,255,0.03); }
|
|
706
|
+
.qa-bg-black-5 { background-color: rgba(255,255,255,0.05); }
|
|
707
|
+
|
|
708
|
+
/* Semantic surfaces */
|
|
709
|
+
.qa-bg-0 { background-color: var(--qa-surface-0); }
|
|
710
|
+
.qa-bg-1 { background-color: var(--qa-surface-1); }
|
|
711
|
+
.qa-bg-2 { background-color: var(--qa-surface-2); }
|
|
712
|
+
.qa-bg-3 { background-color: var(--qa-surface-3); }
|
|
713
|
+
|
|
714
|
+
.qa-bg-accent {
|
|
715
|
+
background-color: var(--qa-accent);
|
|
716
|
+
color: var(--qa-on-accent);
|
|
717
|
+
}
|
|
718
|
+
.qa-bg-accent:hover { background-color: var(--qa-accent-hover); }
|
|
719
|
+
|
|
720
|
+
.qa-bg-accent-tint { background-color: var(--qa-accent-tint); }
|
|
721
|
+
.qa-bg-danger-tint { background-color: var(--qa-danger-tint); }
|
|
722
|
+
.qa-bg-warn-tint { background-color: var(--qa-warn-tint); }
|
|
723
|
+
.qa-bg-success-tint { background-color: var(--qa-success-tint); }
|
|
606
724
|
|
|
607
725
|
/* \u2500\u2500 Opacity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
608
726
|
.qa-opacity-0 { opacity: 0; }
|
|
@@ -621,8 +739,10 @@ var QA_CSS = `
|
|
|
621
739
|
.qa-touch-none { touch-action: none; }
|
|
622
740
|
.qa-touch-pan { touch-action: pan-x pan-y; }
|
|
623
741
|
|
|
624
|
-
|
|
625
|
-
|
|
742
|
+
/* Restyled onto :focus-visible (was :focus) so a mouse click no longer
|
|
743
|
+
leaves a persistent ring \u2014 only keyboard/AT focus does. */
|
|
744
|
+
.qa-focus-ring:focus-visible {
|
|
745
|
+
outline: 2px solid var(--qa-accent);
|
|
626
746
|
outline-offset: 2px;
|
|
627
747
|
}
|
|
628
748
|
|
|
@@ -634,13 +754,14 @@ input:disabled,
|
|
|
634
754
|
}
|
|
635
755
|
|
|
636
756
|
/* Hover helpers */
|
|
637
|
-
.qa-hover-bg-black-3:hover { background-color:
|
|
638
|
-
.qa-hover-bg-black-5:hover { background-color:
|
|
639
|
-
.qa-hover-bg-white-15:hover { background-color:
|
|
757
|
+
.qa-hover-bg-black-3:hover { background-color: var(--qa-surface-2); }
|
|
758
|
+
.qa-hover-bg-black-5:hover { background-color: var(--qa-surface-3); }
|
|
759
|
+
.qa-hover-bg-white-15:hover { background-color: var(--qa-surface-2); }
|
|
760
|
+
.qa-hover-bg-2:hover { background-color: var(--qa-surface-2); }
|
|
640
761
|
.qa-hover-opacity-80:hover { opacity: 0.80; }
|
|
641
762
|
.qa-hover-opacity-100:hover { opacity: 1; }
|
|
642
|
-
.qa-hover-text-red:hover { color:
|
|
643
|
-
.qa-hover-text-slate-600:hover { color:
|
|
763
|
+
.qa-hover-text-red:hover { color: var(--qa-danger); }
|
|
764
|
+
.qa-hover-text-slate-600:hover { color: var(--qa-ink-hi); }
|
|
644
765
|
|
|
645
766
|
/* Group-hover (child uses .qa-group-hover-opacity-80 inside a .qa-group parent) */
|
|
646
767
|
.qa-group .qa-group-hover-opacity-80 { opacity: 0.40; }
|
|
@@ -664,13 +785,25 @@ input:disabled,
|
|
|
664
785
|
50% { opacity: 0.5; box-shadow: 0 0 0 8px transparent; }
|
|
665
786
|
}
|
|
666
787
|
|
|
788
|
+
@keyframes qaShimmer {
|
|
789
|
+
0%, 100% { opacity: 0.55; }
|
|
790
|
+
50% { opacity: 1; }
|
|
791
|
+
}
|
|
792
|
+
|
|
667
793
|
.qa-animate-spin {
|
|
668
794
|
animation: qaSpin 1s linear infinite;
|
|
669
795
|
}
|
|
670
796
|
|
|
671
797
|
.qa-animate-pulse-accent {
|
|
672
798
|
animation: qaPulse 2s ease-in-out infinite;
|
|
673
|
-
color: var(--qa-accent
|
|
799
|
+
color: var(--qa-accent);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* Loading placeholder rows (NoteList while notesLoading && !notes.length) */
|
|
803
|
+
.qa-skeleton {
|
|
804
|
+
background-color: var(--qa-surface-2);
|
|
805
|
+
border-radius: var(--qa-radius-sm);
|
|
806
|
+
animation: qaShimmer 1.4s ease-in-out infinite;
|
|
674
807
|
}
|
|
675
808
|
|
|
676
809
|
/* \u2500\u2500 Print \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
@@ -757,6 +890,41 @@ input:disabled,
|
|
|
757
890
|
|
|
758
891
|
/* \u2500\u2500 Extra space-y \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
759
892
|
.qa-space-y-1\\.5 > * + * { margin-top: 0.375rem; }
|
|
893
|
+
|
|
894
|
+
/* \u2500\u2500 Toast (NoticeHost) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
895
|
+
.qa-toast-viewport {
|
|
896
|
+
position: fixed;
|
|
897
|
+
inset-inline: 0;
|
|
898
|
+
bottom: 1rem;
|
|
899
|
+
z-index: var(--qa-z-toast);
|
|
900
|
+
display: flex;
|
|
901
|
+
flex-direction: column;
|
|
902
|
+
align-items: center;
|
|
903
|
+
gap: 0.5rem;
|
|
904
|
+
pointer-events: none;
|
|
905
|
+
}
|
|
906
|
+
.qa-toast {
|
|
907
|
+
pointer-events: auto;
|
|
908
|
+
display: flex;
|
|
909
|
+
align-items: center;
|
|
910
|
+
gap: 0.5rem;
|
|
911
|
+
max-width: min(92vw, 360px);
|
|
912
|
+
padding: 0.625rem 0.75rem;
|
|
913
|
+
background-color: var(--qa-surface-2);
|
|
914
|
+
border: 1px solid var(--qa-border-subtle);
|
|
915
|
+
border-radius: var(--qa-radius-md);
|
|
916
|
+
box-shadow: var(--qa-elev-2), var(--qa-sheen);
|
|
917
|
+
color: var(--qa-ink-hi);
|
|
918
|
+
font-size: 13px;
|
|
919
|
+
opacity: 0;
|
|
920
|
+
transform: translateY(8px);
|
|
921
|
+
transition: opacity var(--qa-dur-2) var(--qa-ease-out),
|
|
922
|
+
transform var(--qa-dur-2) var(--qa-ease-out);
|
|
923
|
+
}
|
|
924
|
+
.qa-toast.qa-toast-in {
|
|
925
|
+
opacity: 1;
|
|
926
|
+
transform: translateY(0);
|
|
927
|
+
}
|
|
760
928
|
`;
|
|
761
929
|
function injectStyles(root) {
|
|
762
930
|
if (typeof CSSStyleSheet !== "undefined" && "adoptedStyleSheets" in Document.prototype) {
|
|
@@ -772,16 +940,295 @@ function injectStyles(root) {
|
|
|
772
940
|
style.textContent = QA_CSS;
|
|
773
941
|
root.appendChild(style);
|
|
774
942
|
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
943
|
+
|
|
944
|
+
// src/lib/contextBuffer.ts
|
|
945
|
+
var RING_CAP = 75;
|
|
946
|
+
var MAX_MESSAGE_CHARS = 600;
|
|
947
|
+
var MAX_HTML_CHARS = 600;
|
|
948
|
+
var ring = [];
|
|
949
|
+
var installed = false;
|
|
950
|
+
var refCount = 0;
|
|
951
|
+
var drainedUpTo = 0;
|
|
952
|
+
var original = {};
|
|
953
|
+
function push(ev) {
|
|
954
|
+
ring.push(ev);
|
|
955
|
+
if (ring.length > RING_CAP) {
|
|
956
|
+
const overflow = ring.length - RING_CAP;
|
|
957
|
+
ring = ring.slice(overflow);
|
|
958
|
+
drainedUpTo = Math.max(0, drainedUpTo - overflow);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
function clip(s, max = MAX_MESSAGE_CHARS) {
|
|
962
|
+
const str = typeof s === "string" ? s : safeStringify(s);
|
|
963
|
+
return str.length > max ? `${str.slice(0, max)}\u2026` : str;
|
|
964
|
+
}
|
|
965
|
+
function safeStringify(v) {
|
|
966
|
+
if (v === null) return "null";
|
|
967
|
+
if (v === void 0) return "undefined";
|
|
968
|
+
if (typeof v === "string") return v;
|
|
969
|
+
if (v instanceof Error) return `${v.name}: ${v.message}`;
|
|
970
|
+
try {
|
|
971
|
+
return JSON.stringify(v) ?? String(v);
|
|
972
|
+
} catch {
|
|
973
|
+
return String(v);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function now() {
|
|
977
|
+
return Date.now();
|
|
978
|
+
}
|
|
979
|
+
function redactUrl(raw) {
|
|
980
|
+
const s = String(raw ?? "");
|
|
981
|
+
try {
|
|
982
|
+
const u = new URL(s, typeof location !== "undefined" ? location.href : "http://localhost");
|
|
983
|
+
const redacted = u.search ? "?\u2026" : "";
|
|
984
|
+
return `${u.origin}${u.pathname}${redacted}`;
|
|
985
|
+
} catch {
|
|
986
|
+
const cut = s.split(/[?#]/)[0];
|
|
987
|
+
return s.length > cut.length ? `${cut}?\u2026` : cut;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function installContextCapture() {
|
|
991
|
+
refCount += 1;
|
|
992
|
+
if (installed) return;
|
|
993
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
994
|
+
installed = true;
|
|
995
|
+
original.consoleError = console.error.bind(console);
|
|
996
|
+
original.consoleWarn = console.warn.bind(console);
|
|
997
|
+
console.error = (...args) => {
|
|
998
|
+
push({ t: now(), kind: "console", level: "error", message: clip(args.map(safeStringify).join(" ")) });
|
|
999
|
+
original.consoleError?.(...args);
|
|
1000
|
+
};
|
|
1001
|
+
console.warn = (...args) => {
|
|
1002
|
+
push({ t: now(), kind: "console", level: "warn", message: clip(args.map(safeStringify).join(" ")) });
|
|
1003
|
+
original.consoleWarn?.(...args);
|
|
1004
|
+
};
|
|
1005
|
+
original.onError = (e) => {
|
|
1006
|
+
const ev = { t: now(), kind: "error", message: clip(e.message) };
|
|
1007
|
+
if (e.error?.stack) ev.stack = clip(e.error.stack);
|
|
1008
|
+
push(ev);
|
|
1009
|
+
};
|
|
1010
|
+
original.onRejection = (e) => {
|
|
1011
|
+
const reason = e.reason;
|
|
1012
|
+
const ev = {
|
|
1013
|
+
t: now(),
|
|
1014
|
+
kind: "error",
|
|
1015
|
+
message: clip(reason instanceof Error ? `${reason.name}: ${reason.message}` : safeStringify(reason))
|
|
1016
|
+
};
|
|
1017
|
+
if (reason instanceof Error && reason.stack) ev.stack = clip(reason.stack);
|
|
1018
|
+
push(ev);
|
|
1019
|
+
};
|
|
1020
|
+
window.addEventListener("error", original.onError);
|
|
1021
|
+
window.addEventListener("unhandledrejection", original.onRejection);
|
|
1022
|
+
if (typeof window.fetch === "function") {
|
|
1023
|
+
original.fetch = window.fetch.bind(window);
|
|
1024
|
+
window.fetch = async (input, init) => {
|
|
1025
|
+
const started = now();
|
|
1026
|
+
const method = (init?.method || (typeof input === "object" && "method" in input ? input.method : "GET") || "GET").toUpperCase();
|
|
1027
|
+
const url = redactUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
1028
|
+
try {
|
|
1029
|
+
const res = await original.fetch(input, init);
|
|
1030
|
+
push({ t: started, kind: "network", method, url, status: res.status, durationMs: now() - started });
|
|
1031
|
+
return res;
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
push({
|
|
1034
|
+
t: started,
|
|
1035
|
+
kind: "network",
|
|
1036
|
+
method,
|
|
1037
|
+
url,
|
|
1038
|
+
status: null,
|
|
1039
|
+
durationMs: now() - started,
|
|
1040
|
+
error: clip(err instanceof Error ? err.message : safeStringify(err))
|
|
1041
|
+
});
|
|
1042
|
+
throw err;
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
1047
|
+
original.xhrOpen = XMLHttpRequest.prototype.open;
|
|
1048
|
+
original.xhrSend = XMLHttpRequest.prototype.send;
|
|
1049
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
1050
|
+
this.__qaMethod = String(method || "GET").toUpperCase();
|
|
1051
|
+
this.__qaUrl = redactUrl(typeof url === "string" ? url : url.href);
|
|
1052
|
+
return original.xhrOpen.call(this, method, url, ...rest);
|
|
1053
|
+
};
|
|
1054
|
+
XMLHttpRequest.prototype.send = function(...args) {
|
|
1055
|
+
this.__qaStart = now();
|
|
1056
|
+
const record = (error) => {
|
|
1057
|
+
const ev = {
|
|
1058
|
+
t: this.__qaStart ?? now(),
|
|
1059
|
+
kind: "network",
|
|
1060
|
+
method: this.__qaMethod ?? "GET",
|
|
1061
|
+
url: this.__qaUrl ?? "",
|
|
1062
|
+
status: error ? null : this.status,
|
|
1063
|
+
durationMs: now() - (this.__qaStart ?? now())
|
|
1064
|
+
};
|
|
1065
|
+
if (error) ev.error = error;
|
|
1066
|
+
push(ev);
|
|
1067
|
+
};
|
|
1068
|
+
this.addEventListener("load", () => record());
|
|
1069
|
+
this.addEventListener("error", () => record("network error"));
|
|
1070
|
+
this.addEventListener("timeout", () => record("timeout"));
|
|
1071
|
+
return original.xhrSend.apply(this, args);
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function uninstallContextCapture() {
|
|
1076
|
+
if (refCount > 0) refCount -= 1;
|
|
1077
|
+
if (!installed || refCount > 0) return;
|
|
1078
|
+
installed = false;
|
|
1079
|
+
if (original.consoleError) console.error = original.consoleError;
|
|
1080
|
+
if (original.consoleWarn) console.warn = original.consoleWarn;
|
|
1081
|
+
if (original.fetch) window.fetch = original.fetch;
|
|
1082
|
+
if (original.xhrOpen) XMLHttpRequest.prototype.open = original.xhrOpen;
|
|
1083
|
+
if (original.xhrSend) XMLHttpRequest.prototype.send = original.xhrSend;
|
|
1084
|
+
if (original.onError) window.removeEventListener("error", original.onError);
|
|
1085
|
+
if (original.onRejection) {
|
|
1086
|
+
window.removeEventListener("unhandledrejection", original.onRejection);
|
|
1087
|
+
}
|
|
1088
|
+
ring = [];
|
|
1089
|
+
drainedUpTo = 0;
|
|
1090
|
+
}
|
|
1091
|
+
function drainSinceLastNote() {
|
|
1092
|
+
if (!installed) return [];
|
|
1093
|
+
const slice = ring.slice(drainedUpTo);
|
|
1094
|
+
drainedUpTo = ring.length;
|
|
1095
|
+
return slice;
|
|
1096
|
+
}
|
|
1097
|
+
function collectEnvSnapshot(route) {
|
|
1098
|
+
const snap = {
|
|
1099
|
+
url: typeof location !== "undefined" ? redactUrl(location.href) : "",
|
|
1100
|
+
route,
|
|
1101
|
+
viewportW: typeof window !== "undefined" ? window.innerWidth : 0,
|
|
1102
|
+
viewportH: typeof window !== "undefined" ? window.innerHeight : 0,
|
|
1103
|
+
dpr: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
1104
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1105
|
+
language: typeof navigator !== "undefined" ? navigator.language : "",
|
|
1106
|
+
online: typeof navigator !== "undefined" ? navigator.onLine !== false : true,
|
|
1107
|
+
timezone: ""
|
|
1108
|
+
};
|
|
1109
|
+
try {
|
|
1110
|
+
snap.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
1111
|
+
} catch {
|
|
1112
|
+
snap.timezone = "";
|
|
1113
|
+
}
|
|
1114
|
+
try {
|
|
1115
|
+
const nav = performance?.getEntriesByType?.("navigation")?.[0];
|
|
1116
|
+
if (nav && Number.isFinite(nav.duration) && nav.duration > 0) {
|
|
1117
|
+
snap.pageLoadMs = Math.round(nav.duration);
|
|
1118
|
+
}
|
|
1119
|
+
} catch {
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const mem = performance.memory;
|
|
1123
|
+
if (mem?.usedJSHeapSize) snap.memoryUsedMB = Math.round(mem.usedJSHeapSize / 1048576);
|
|
1124
|
+
} catch {
|
|
1125
|
+
}
|
|
1126
|
+
return snap;
|
|
1127
|
+
}
|
|
1128
|
+
function luminance(rgb) {
|
|
1129
|
+
const [r, g, b] = rgb.map((c) => {
|
|
1130
|
+
const v = c / 255;
|
|
1131
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
1132
|
+
});
|
|
1133
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1134
|
+
}
|
|
1135
|
+
function parseRgb(color) {
|
|
1136
|
+
const m = (color || "").match(/^rgba?\(([^)]+)\)$/i);
|
|
1137
|
+
if (!m) return null;
|
|
1138
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean).map(parseFloat);
|
|
1139
|
+
if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return null;
|
|
1140
|
+
return [parts[0], parts[1], parts[2]];
|
|
1141
|
+
}
|
|
1142
|
+
var SENSITIVE_FORENSICS_ATTRS = ["value", "checked", "selected"];
|
|
1143
|
+
function sanitizeForForensics(el) {
|
|
1144
|
+
const clone = el.cloneNode(true);
|
|
1145
|
+
const nodes = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
1146
|
+
for (const node of nodes) {
|
|
1147
|
+
for (const attr of SENSITIVE_FORENSICS_ATTRS) {
|
|
1148
|
+
if (node.hasAttribute(attr)) node.removeAttribute(attr);
|
|
1149
|
+
}
|
|
1150
|
+
if (node.tagName === "TEXTAREA") node.textContent = "";
|
|
1151
|
+
}
|
|
1152
|
+
return clone;
|
|
1153
|
+
}
|
|
1154
|
+
function collectTargetForensics(el) {
|
|
1155
|
+
const out = {};
|
|
1156
|
+
if (!el || typeof window === "undefined") return out;
|
|
1157
|
+
try {
|
|
1158
|
+
out.html = clip(sanitizeForForensics(el).outerHTML, MAX_HTML_CHARS);
|
|
1159
|
+
} catch {
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const cs = getComputedStyle(el);
|
|
1163
|
+
out.styles = {
|
|
1164
|
+
display: cs.display,
|
|
1165
|
+
position: cs.position,
|
|
1166
|
+
overflow: cs.overflow,
|
|
1167
|
+
"z-index": cs.zIndex,
|
|
1168
|
+
"font-size": cs.fontSize,
|
|
1169
|
+
color: cs.color,
|
|
1170
|
+
"background-color": cs.backgroundColor
|
|
1171
|
+
};
|
|
1172
|
+
const fg = parseRgb(cs.color);
|
|
1173
|
+
const bg = parseRgb(cs.backgroundColor);
|
|
1174
|
+
let contrastFlag = "unknown";
|
|
1175
|
+
if (fg && bg) {
|
|
1176
|
+
const l1 = luminance(fg);
|
|
1177
|
+
const l2 = luminance(bg);
|
|
1178
|
+
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
1179
|
+
contrastFlag = ratio < 4.5 ? "low" : "ok";
|
|
1180
|
+
}
|
|
1181
|
+
const name = el.getAttribute("aria-label") || el.getAttribute("title") || el.innerText || el.textContent || "";
|
|
1182
|
+
const tabIndexAttr = el.getAttribute("tabindex");
|
|
1183
|
+
const nativelyFocusable = /^(a|button|input|select|textarea)$/i.test(el.tagName) && !el.disabled;
|
|
1184
|
+
out.a11y = {
|
|
1185
|
+
hasAccessibleName: name.trim().length > 0,
|
|
1186
|
+
tabReachable: nativelyFocusable || tabIndexAttr !== null && tabIndexAttr !== "-1",
|
|
1187
|
+
contrastFlag
|
|
1188
|
+
};
|
|
1189
|
+
const role = el.getAttribute("role");
|
|
1190
|
+
if (role) out.a11y.role = role;
|
|
1191
|
+
} catch {
|
|
1192
|
+
}
|
|
1193
|
+
return out;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/lib/journeyMatch.ts
|
|
1197
|
+
function normalizeRoute(route) {
|
|
1198
|
+
const path = String(route ?? "").split(/[?#]/)[0];
|
|
1199
|
+
if (path.length > 1 && path.endsWith("/")) return path.slice(0, -1);
|
|
1200
|
+
return path || "/";
|
|
1201
|
+
}
|
|
1202
|
+
function segments(path) {
|
|
1203
|
+
return normalizeRoute(path).split("/").filter(Boolean);
|
|
1204
|
+
}
|
|
1205
|
+
function matchesWithParams(stepPath, route) {
|
|
1206
|
+
const stepSegs = segments(stepPath);
|
|
1207
|
+
const routeSegs = segments(route);
|
|
1208
|
+
if (stepSegs.length !== routeSegs.length) return false;
|
|
1209
|
+
return stepSegs.every((seg, i) => {
|
|
1210
|
+
const isParam = seg.startsWith(":") || seg.startsWith("[") && seg.endsWith("]");
|
|
1211
|
+
return isParam || seg.toLowerCase() === routeSegs[i].toLowerCase();
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
function matchRouteToSteps(journey, route) {
|
|
1215
|
+
if (!Array.isArray(journey) || !journey.length) return [];
|
|
1216
|
+
const target = normalizeRoute(route);
|
|
1217
|
+
const exact = [];
|
|
1218
|
+
const param = [];
|
|
1219
|
+
for (const lane of journey) {
|
|
1220
|
+
if (!lane || !Array.isArray(lane.steps)) continue;
|
|
1221
|
+
for (const step of lane.steps) {
|
|
1222
|
+
if (!step || typeof step.path !== "string") continue;
|
|
1223
|
+
const ref = { laneId: lane.id, path: step.path };
|
|
1224
|
+
if (normalizeRoute(step.path).toLowerCase() === target.toLowerCase()) {
|
|
1225
|
+
exact.push(ref);
|
|
1226
|
+
} else if (matchesWithParams(step.path, target)) {
|
|
1227
|
+
param.push(ref);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
return [...exact, ...param];
|
|
785
1232
|
}
|
|
786
1233
|
|
|
787
1234
|
// src/lib/storage.ts
|
|
@@ -893,7 +1340,36 @@ var STR = {
|
|
|
893
1340
|
use_this: "Use this",
|
|
894
1341
|
adjust: "Adjust",
|
|
895
1342
|
resize: "Resize",
|
|
896
|
-
confirm_region: "Confirm region"
|
|
1343
|
+
confirm_region: "Confirm region",
|
|
1344
|
+
capture_failed: "Screenshot failed",
|
|
1345
|
+
retry: "Retry",
|
|
1346
|
+
persist_failed: "Storage full \u2014 this note may not survive a reload",
|
|
1347
|
+
note_deleted: "Note deleted",
|
|
1348
|
+
notes_cleared: "All notes cleared",
|
|
1349
|
+
undo: "Undo",
|
|
1350
|
+
export_done: "Export downloaded",
|
|
1351
|
+
export_failed: "Export failed",
|
|
1352
|
+
copied: "Copied",
|
|
1353
|
+
copy_failed: "Copy failed",
|
|
1354
|
+
copy_prompt: "Copy as agent prompt",
|
|
1355
|
+
severity_label: "Severity",
|
|
1356
|
+
sev_bug: "Bug",
|
|
1357
|
+
sev_question: "Question",
|
|
1358
|
+
sev_polish: "Polish",
|
|
1359
|
+
status_open: "Open",
|
|
1360
|
+
status_verified: "Verified",
|
|
1361
|
+
context_attached: "{n} runtime events attached",
|
|
1362
|
+
start_walkthrough: "Start walkthrough",
|
|
1363
|
+
step_of: "Step {n} of {m}",
|
|
1364
|
+
next_step: "Next",
|
|
1365
|
+
prev_step: "Back",
|
|
1366
|
+
mark_pass: "Pass",
|
|
1367
|
+
mark_fail: "Fail",
|
|
1368
|
+
capture_here: "Capture here",
|
|
1369
|
+
exit_walkthrough: "Exit",
|
|
1370
|
+
evidence_n: "{n} attached",
|
|
1371
|
+
no_evidence: "ticked, no capture",
|
|
1372
|
+
expected_label: "Expected"
|
|
897
1373
|
},
|
|
898
1374
|
ar: {
|
|
899
1375
|
tab_notes: "\u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
|
|
@@ -944,7 +1420,36 @@ var STR = {
|
|
|
944
1420
|
use_this: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0627",
|
|
945
1421
|
adjust: "\u062A\u0639\u062F\u064A\u0644",
|
|
946
1422
|
resize: "\u062A\u063A\u064A\u064A\u0631 \u0627\u0644\u062D\u062C\u0645",
|
|
947
|
-
confirm_region: "\u062A\u0623\u0643\u064A\u062F \u0627\u0644\u0645\u0646\u0637\u0642\u0629"
|
|
1423
|
+
confirm_region: "\u062A\u0623\u0643\u064A\u062F \u0627\u0644\u0645\u0646\u0637\u0642\u0629",
|
|
1424
|
+
capture_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629",
|
|
1425
|
+
retry: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629",
|
|
1426
|
+
persist_failed: "\u0645\u0633\u0627\u062D\u0629 \u0627\u0644\u062A\u062E\u0632\u064A\u0646 \u0645\u0645\u062A\u0644\u0626\u0629 \u2014 \u0642\u062F \u0644\u0627 \u062A\u0628\u0642\u0649 \u0647\u0630\u0647 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629 \u0628\u0639\u062F \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u0645\u064A\u0644",
|
|
1427
|
+
note_deleted: "\u062A\u0645 \u062D\u0630\u0641 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629",
|
|
1428
|
+
notes_cleared: "\u062A\u0645 \u0645\u0633\u062D \u062C\u0645\u064A\u0639 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
|
|
1429
|
+
undo: "\u062A\u0631\u0627\u062C\u0639",
|
|
1430
|
+
export_done: "\u062A\u0645 \u062A\u0646\u0632\u064A\u0644 \u0627\u0644\u0645\u0644\u0641",
|
|
1431
|
+
export_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
|
|
1432
|
+
copied: "\u062A\u0645 \u0627\u0644\u0646\u0633\u062E",
|
|
1433
|
+
copy_failed: "\u0641\u0634\u0644 \u0627\u0644\u0646\u0633\u062E",
|
|
1434
|
+
copy_prompt: "\u0646\u0633\u062E \u0643\u0645\u0648\u062C\u0651\u0647 \u0644\u0644\u0648\u0643\u064A\u0644",
|
|
1435
|
+
severity_label: "\u0627\u0644\u0623\u0647\u0645\u064A\u0629",
|
|
1436
|
+
sev_bug: "\u062E\u0644\u0644",
|
|
1437
|
+
sev_question: "\u0633\u0624\u0627\u0644",
|
|
1438
|
+
sev_polish: "\u062A\u062D\u0633\u064A\u0646",
|
|
1439
|
+
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1440
|
+
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1441
|
+
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1442
|
+
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
1443
|
+
step_of: "\u0627\u0644\u062E\u0637\u0648\u0629 {n} \u0645\u0646 {m}",
|
|
1444
|
+
next_step: "\u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1445
|
+
prev_step: "\u0627\u0644\u0633\u0627\u0628\u0642",
|
|
1446
|
+
mark_pass: "\u0646\u062C\u0627\u062D",
|
|
1447
|
+
mark_fail: "\u0641\u0634\u0644",
|
|
1448
|
+
capture_here: "\u0627\u0644\u062A\u0642\u0637 \u0647\u0646\u0627",
|
|
1449
|
+
exit_walkthrough: "\u062E\u0631\u0648\u062C",
|
|
1450
|
+
evidence_n: "{n} \u0645\u0631\u0641\u0642",
|
|
1451
|
+
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1452
|
+
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639"
|
|
948
1453
|
}
|
|
949
1454
|
};
|
|
950
1455
|
function translate(lang, key, vars) {
|
|
@@ -964,10 +1469,14 @@ function pick(value, lang) {
|
|
|
964
1469
|
|
|
965
1470
|
// src/lib/coverage.ts
|
|
966
1471
|
var RISK_COLORS = {
|
|
967
|
-
red: "#
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1472
|
+
red: "#FF6B6B",
|
|
1473
|
+
// --qa-danger
|
|
1474
|
+
amber: "#FBBF24",
|
|
1475
|
+
// --qa-warn
|
|
1476
|
+
green: "#34D399",
|
|
1477
|
+
// --qa-success
|
|
1478
|
+
none: "#5B616B"
|
|
1479
|
+
// --qa-neutral
|
|
971
1480
|
};
|
|
972
1481
|
function computeCoverage(journey, guideChecked) {
|
|
973
1482
|
const red = { total: 0, covered: 0 };
|
|
@@ -1027,33 +1536,103 @@ function computeCoverage(journey, guideChecked) {
|
|
|
1027
1536
|
};
|
|
1028
1537
|
}
|
|
1029
1538
|
|
|
1030
|
-
// src/lib/
|
|
1031
|
-
function
|
|
1539
|
+
// src/lib/noteMarkdown.ts
|
|
1540
|
+
function oneLine(s) {
|
|
1541
|
+
return String(s ?? "").replace(/\r?\n|\r/g, " ").trim();
|
|
1542
|
+
}
|
|
1543
|
+
function formatEvent(ev, t0) {
|
|
1544
|
+
const rel = `${((ev.t - t0) / 1e3).toFixed(1)}s`;
|
|
1545
|
+
if (ev.kind === "network") {
|
|
1546
|
+
const status = ev.status === null ? ev.error ?? "failed" : String(ev.status);
|
|
1547
|
+
return `[${rel}] ${ev.method} ${ev.url} \u2192 ${status} (${ev.durationMs}ms)`;
|
|
1548
|
+
}
|
|
1549
|
+
if (ev.kind === "console") {
|
|
1550
|
+
return `[${rel}] console.${ev.level}: ${oneLine(ev.message)}`;
|
|
1551
|
+
}
|
|
1552
|
+
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
1553
|
+
}
|
|
1554
|
+
function noteToMarkdown(note, opts) {
|
|
1555
|
+
const brand = opts?.brand ?? "Qapture";
|
|
1556
|
+
const idx = opts?.index;
|
|
1032
1557
|
const lines = [];
|
|
1033
|
-
lines.push(
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
if (
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1558
|
+
lines.push(idx != null ? `## Point ${idx}` : `## ${brand} point`);
|
|
1559
|
+
lines.push("");
|
|
1560
|
+
lines.push(`- **Page:** ${oneLine(note.route) || "/"}`);
|
|
1561
|
+
if (note.url) lines.push(`- **Full URL:** ${oneLine(note.url)}`);
|
|
1562
|
+
lines.push(`- **When:** ${oneLine(note.timestamp)}`);
|
|
1563
|
+
if (note.severity) lines.push(`- **Severity:** ${note.severity}`);
|
|
1564
|
+
if (note.status) lines.push(`- **Status:** ${note.status}`);
|
|
1565
|
+
if (note.journeyRef) {
|
|
1566
|
+
lines.push(`- **Journey step:** ${oneLine(note.journeyRef.laneId)} \u2192 ${oneLine(note.journeyRef.path)}`);
|
|
1567
|
+
}
|
|
1568
|
+
const target = note.target;
|
|
1569
|
+
if (target) {
|
|
1570
|
+
lines.push(`- **Target:** ${target.kind}`);
|
|
1571
|
+
if (target.selector) lines.push(`- **Selector:** \`${oneLine(target.selector)}\``);
|
|
1572
|
+
if (target.tagName) lines.push(`- **Tag:** \`<${oneLine(target.tagName)}>\``);
|
|
1573
|
+
if (target.text) lines.push(`- **Text:** ${oneLine(target.text)}`);
|
|
1574
|
+
const r = target.rect;
|
|
1575
|
+
if (r) {
|
|
1576
|
+
lines.push(
|
|
1577
|
+
`- **Position:** top ${Math.round(r.top)}, left ${Math.round(r.left)}, ${Math.round(r.width)}\xD7${Math.round(r.height)}`
|
|
1578
|
+
);
|
|
1579
|
+
}
|
|
1041
1580
|
}
|
|
1042
|
-
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
if (
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
lines.push(
|
|
1581
|
+
if (idx != null && note.screenshot) {
|
|
1582
|
+
lines.push(`- **Screenshot:** screenshots/point-${idx}.png`);
|
|
1583
|
+
}
|
|
1584
|
+
lines.push("");
|
|
1585
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(no description)_");
|
|
1586
|
+
const ctx = note.context;
|
|
1587
|
+
if (ctx) {
|
|
1588
|
+
const env = ctx.env;
|
|
1589
|
+
const events = Array.isArray(ctx.events) ? ctx.events : [];
|
|
1590
|
+
lines.push("");
|
|
1591
|
+
lines.push("<details><summary>Runtime context at capture</summary>");
|
|
1592
|
+
lines.push("");
|
|
1593
|
+
lines.push("```");
|
|
1594
|
+
if (env) {
|
|
1595
|
+
lines.push(`viewport ${env.viewportW}\xD7${env.viewportH} @${env.dpr}x`);
|
|
1596
|
+
if (env.language) lines.push(`language ${env.language}`);
|
|
1597
|
+
if (env.timezone) lines.push(`timezone ${env.timezone}`);
|
|
1598
|
+
lines.push(`online ${env.online}`);
|
|
1599
|
+
if (env.pageLoadMs != null) lines.push(`pageLoad ${env.pageLoadMs}ms`);
|
|
1600
|
+
if (env.memoryUsedMB != null) lines.push(`jsHeap ${env.memoryUsedMB}MB`);
|
|
1601
|
+
if (env.userAgent) lines.push(`userAgent ${env.userAgent}`);
|
|
1602
|
+
}
|
|
1603
|
+
if (events.length) {
|
|
1604
|
+
const t0 = Date.parse(note.timestamp) || (events[events.length - 1]?.t ?? 0);
|
|
1605
|
+
lines.push("");
|
|
1606
|
+
lines.push(`events (${events.length}, most recent last):`);
|
|
1607
|
+
for (const ev of events) lines.push(` ${formatEvent(ev, t0)}`);
|
|
1608
|
+
} else {
|
|
1609
|
+
lines.push("");
|
|
1610
|
+
lines.push("events (none recorded)");
|
|
1611
|
+
}
|
|
1612
|
+
lines.push("```");
|
|
1613
|
+
const f = ctx.forensics;
|
|
1614
|
+
if (f && (f.html || f.styles || f.a11y)) {
|
|
1615
|
+
lines.push("");
|
|
1616
|
+
lines.push("**Element forensics**");
|
|
1617
|
+
lines.push("");
|
|
1618
|
+
lines.push("```");
|
|
1619
|
+
if (f.html) lines.push(`html ${oneLine(f.html)}`);
|
|
1620
|
+
if (f.styles) {
|
|
1621
|
+
for (const [k, v] of Object.entries(f.styles)) lines.push(`${k.padEnd(7)} ${v}`);
|
|
1622
|
+
}
|
|
1623
|
+
if (f.a11y) {
|
|
1624
|
+
if (f.a11y.role) lines.push(`role ${f.a11y.role}`);
|
|
1625
|
+
lines.push(`a11y accessibleName=${f.a11y.hasAccessibleName} tabReachable=${f.a11y.tabReachable}` + (f.a11y.contrastFlag ? ` contrast=${f.a11y.contrastFlag}` : ""));
|
|
1626
|
+
}
|
|
1627
|
+
lines.push("```");
|
|
1628
|
+
}
|
|
1629
|
+
lines.push("");
|
|
1630
|
+
lines.push("</details>");
|
|
1052
1631
|
}
|
|
1053
|
-
if (note.screenshot) lines.push(`- **Screenshot:** screenshots/point-${num}.png`);
|
|
1054
|
-
lines.push("", note.description || "(no description)", "", "---", "");
|
|
1055
1632
|
return lines.join("\n");
|
|
1056
1633
|
}
|
|
1634
|
+
|
|
1635
|
+
// src/lib/exportZip.ts
|
|
1057
1636
|
function safeName(name, stamp) {
|
|
1058
1637
|
const fallback = `qa-notes-${stamp.slice(0, 10)}`;
|
|
1059
1638
|
let base = (name ?? "").trim().replace(/\.zip$/i, "");
|
|
@@ -1133,12 +1712,13 @@ ${list}`);
|
|
|
1133
1712
|
sections.push("## Conventions\n\n(not provided)");
|
|
1134
1713
|
}
|
|
1135
1714
|
const creds = config.credentials ?? [];
|
|
1715
|
+
const redactedCount = creds.filter((c) => !c.seeded).length;
|
|
1136
1716
|
let credBlock;
|
|
1137
1717
|
if (creds.length > 0) {
|
|
1138
1718
|
const credRows = creds.map((c) => [
|
|
1139
1719
|
c.role,
|
|
1140
1720
|
c.login,
|
|
1141
|
-
c.password || "(none)",
|
|
1721
|
+
c.seeded ? c.password || "(none)" : "(redacted \u2014 not marked seeded)",
|
|
1142
1722
|
c.seeded ? "seeded" : "manual",
|
|
1143
1723
|
c.hint?.en ?? "\u2014"
|
|
1144
1724
|
]);
|
|
@@ -1149,12 +1729,13 @@ ${list}`);
|
|
|
1149
1729
|
} else {
|
|
1150
1730
|
credBlock = "(not provided)";
|
|
1151
1731
|
}
|
|
1732
|
+
const redactionNote = redactedCount > 0 ? ` ${redactedCount} credential${redactedCount === 1 ? "" : "s"} above ${redactedCount === 1 ? "is" : "are"} not marked \`seeded: true\`, so its password was withheld from this export \u2014 set \`seeded: true\` in \`credentials\` only for synthetic/throwaway values (e.g. from a seed script), never for a real account.` : "";
|
|
1152
1733
|
sections.push(
|
|
1153
1734
|
`## Login Context
|
|
1154
1735
|
|
|
1155
1736
|
${credBlock}
|
|
1156
1737
|
|
|
1157
|
-
> **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production
|
|
1738
|
+
> **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production.${redactionNote}`
|
|
1158
1739
|
);
|
|
1159
1740
|
const cov = computeCoverage(journey, guideChecked);
|
|
1160
1741
|
const covTableRows = [
|
|
@@ -1231,7 +1812,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked)
|
|
|
1231
1812
|
"---",
|
|
1232
1813
|
""
|
|
1233
1814
|
].join("\n");
|
|
1234
|
-
const
|
|
1815
|
+
const noteBlocks = notes.map(
|
|
1816
|
+
(n, i) => noteToMarkdown(n, { brand: brandLabel, index: i + 1 })
|
|
1817
|
+
);
|
|
1818
|
+
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
1819
|
+
|
|
1820
|
+
---
|
|
1821
|
+
` : "";
|
|
1822
|
+
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
1235
1823
|
zip.file("notes.md", notesMd);
|
|
1236
1824
|
notes.forEach((n, i) => {
|
|
1237
1825
|
if (n.screenshot && shots) {
|
|
@@ -1266,7 +1854,13 @@ function safeLocation() {
|
|
|
1266
1854
|
var QaContext = React.createContext(null);
|
|
1267
1855
|
var LANG_KEY = "lang";
|
|
1268
1856
|
var GUIDE_KEY = "guide";
|
|
1857
|
+
var GUIDE_FAILED_KEY = "guideFailed";
|
|
1269
1858
|
var LOGIN_KEY = "logins";
|
|
1859
|
+
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
1860
|
+
var NOTICE_QUEUE_CAP = 3;
|
|
1861
|
+
var NOTICE_DURATION_INFO = 4e3;
|
|
1862
|
+
var NOTICE_DURATION_ERROR = 6e3;
|
|
1863
|
+
var SOFT_DELETE_MS = 5e3;
|
|
1270
1864
|
function QaProvider({
|
|
1271
1865
|
config,
|
|
1272
1866
|
children
|
|
@@ -1274,6 +1868,7 @@ function QaProvider({
|
|
|
1274
1868
|
const [storage] = React.useState(() => createStorage(config.namespace));
|
|
1275
1869
|
const [idb] = React.useState(() => createIdb(config.namespace));
|
|
1276
1870
|
const [notes, setNotes] = React.useState([]);
|
|
1871
|
+
const [notesLoading, setNotesLoading] = React.useState(true);
|
|
1277
1872
|
const [isOpen, setIsOpen] = React.useState(false);
|
|
1278
1873
|
const [activeTab, setActiveTab] = React.useState("notes");
|
|
1279
1874
|
const [captureActive, setCaptureActive] = React.useState(false);
|
|
@@ -1286,23 +1881,64 @@ function QaProvider({
|
|
|
1286
1881
|
const [guideChecked, setGuideChecked] = React.useState(
|
|
1287
1882
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
1288
1883
|
);
|
|
1884
|
+
const [guideFailed, setGuideFailed] = React.useState(
|
|
1885
|
+
() => new Set(storage.getJSON(GUIDE_FAILED_KEY, []))
|
|
1886
|
+
);
|
|
1289
1887
|
const [loginsUsed, setLoginsUsed] = React.useState(
|
|
1290
1888
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
1291
1889
|
);
|
|
1890
|
+
const [notices, setNotices] = React.useState([]);
|
|
1891
|
+
const noticeTimers = React.useRef(/* @__PURE__ */ new Map());
|
|
1892
|
+
const [testAlong, setTestAlong] = React.useState({
|
|
1893
|
+
active: false,
|
|
1894
|
+
index: 0
|
|
1895
|
+
});
|
|
1896
|
+
const pendingDeletes = React.useRef(
|
|
1897
|
+
/* @__PURE__ */ new Map()
|
|
1898
|
+
);
|
|
1899
|
+
const pendingClear = React.useRef(null);
|
|
1900
|
+
const readPendingDeleteIds = React.useCallback(() => {
|
|
1901
|
+
return new Set(storage.getJSON(PENDING_DELETE_KEY, []));
|
|
1902
|
+
}, [storage]);
|
|
1903
|
+
const addPendingDeleteIds = React.useCallback((ids) => {
|
|
1904
|
+
if (ids.length === 0) return;
|
|
1905
|
+
const current = readPendingDeleteIds();
|
|
1906
|
+
for (const id of ids) current.add(id);
|
|
1907
|
+
storage.setJSON(PENDING_DELETE_KEY, [...current]);
|
|
1908
|
+
}, [storage, readPendingDeleteIds]);
|
|
1909
|
+
const removePendingDeleteIds = React.useCallback((ids) => {
|
|
1910
|
+
if (ids.length === 0) return;
|
|
1911
|
+
const current = readPendingDeleteIds();
|
|
1912
|
+
let changed = false;
|
|
1913
|
+
for (const id of ids) {
|
|
1914
|
+
if (current.delete(id)) changed = true;
|
|
1915
|
+
}
|
|
1916
|
+
if (changed) storage.setJSON(PENDING_DELETE_KEY, [...current]);
|
|
1917
|
+
}, [storage, readPendingDeleteIds]);
|
|
1292
1918
|
React.useEffect(() => {
|
|
1293
1919
|
let alive = true;
|
|
1294
1920
|
idb.getAll().then((rows) => {
|
|
1295
1921
|
if (!alive) return;
|
|
1296
|
-
|
|
1922
|
+
let live = rows;
|
|
1923
|
+
const pendingIds = storage.getJSON(PENDING_DELETE_KEY, []);
|
|
1924
|
+
if (pendingIds.length > 0) {
|
|
1925
|
+
const pendingSet = new Set(pendingIds);
|
|
1926
|
+
live = live.filter((n) => !pendingSet.has(n.id));
|
|
1927
|
+
for (const id of pendingIds) void idb.delete(id);
|
|
1928
|
+
storage.setJSON(PENDING_DELETE_KEY, []);
|
|
1929
|
+
}
|
|
1930
|
+
const sorted = live.slice().sort(
|
|
1297
1931
|
(a, b) => a.timestamp < b.timestamp ? 1 : -1
|
|
1298
1932
|
);
|
|
1299
1933
|
setNotes(sorted);
|
|
1300
1934
|
}).catch(() => {
|
|
1935
|
+
}).finally(() => {
|
|
1936
|
+
if (alive) setNotesLoading(false);
|
|
1301
1937
|
});
|
|
1302
1938
|
return () => {
|
|
1303
1939
|
alive = false;
|
|
1304
1940
|
};
|
|
1305
|
-
}, [idb]);
|
|
1941
|
+
}, [idb, storage]);
|
|
1306
1942
|
const setLang = React.useCallback((l) => {
|
|
1307
1943
|
setLangState(l);
|
|
1308
1944
|
storage.setItem(LANG_KEY, l);
|
|
@@ -1315,27 +1951,160 @@ function QaProvider({
|
|
|
1315
1951
|
(value2) => pick(value2, lang),
|
|
1316
1952
|
[lang]
|
|
1317
1953
|
);
|
|
1954
|
+
const dismissNotice = React.useCallback((id) => {
|
|
1955
|
+
const timer = noticeTimers.current.get(id);
|
|
1956
|
+
if (timer) {
|
|
1957
|
+
clearTimeout(timer);
|
|
1958
|
+
noticeTimers.current.delete(id);
|
|
1959
|
+
}
|
|
1960
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
1961
|
+
}, []);
|
|
1962
|
+
const notify = React.useCallback((message, opts) => {
|
|
1963
|
+
const tone = opts?.tone ?? "info";
|
|
1964
|
+
const id = opts?.id ?? uid();
|
|
1965
|
+
const duration = opts?.duration ?? (tone === "error" ? NOTICE_DURATION_ERROR : NOTICE_DURATION_INFO);
|
|
1966
|
+
const existingTimer = noticeTimers.current.get(id);
|
|
1967
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
1968
|
+
const notice = { id, message, tone, action: opts?.action, duration };
|
|
1969
|
+
setNotices((prev) => {
|
|
1970
|
+
const deduped = prev.filter((n) => n.id !== id);
|
|
1971
|
+
const next = [...deduped, notice];
|
|
1972
|
+
if (next.length <= NOTICE_QUEUE_CAP) return next;
|
|
1973
|
+
const overflow = next.length - NOTICE_QUEUE_CAP;
|
|
1974
|
+
for (const dropped of next.slice(0, overflow)) {
|
|
1975
|
+
const droppedTimer = noticeTimers.current.get(dropped.id);
|
|
1976
|
+
if (droppedTimer) {
|
|
1977
|
+
clearTimeout(droppedTimer);
|
|
1978
|
+
noticeTimers.current.delete(dropped.id);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return next.slice(overflow);
|
|
1982
|
+
});
|
|
1983
|
+
const timer = setTimeout(() => {
|
|
1984
|
+
noticeTimers.current.delete(id);
|
|
1985
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
1986
|
+
}, duration);
|
|
1987
|
+
noticeTimers.current.set(id, timer);
|
|
1988
|
+
return id;
|
|
1989
|
+
}, []);
|
|
1990
|
+
const testAlongSteps = React.useMemo(() => {
|
|
1991
|
+
const out = [];
|
|
1992
|
+
for (const lane of journeyOrEmpty(config.journey)) {
|
|
1993
|
+
const laneRole = pick2(lane.role);
|
|
1994
|
+
const color = lane.color ?? "var(--qa-accent)";
|
|
1995
|
+
for (const step of lane.steps ?? []) {
|
|
1996
|
+
out.push({
|
|
1997
|
+
key: `${lane.id}::${step.path}`,
|
|
1998
|
+
laneId: lane.id,
|
|
1999
|
+
laneRole,
|
|
2000
|
+
color,
|
|
2001
|
+
path: step.path,
|
|
2002
|
+
what: step.what,
|
|
2003
|
+
expect: step.expect,
|
|
2004
|
+
risk: step.risk ?? "green"
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
return out;
|
|
2009
|
+
}, [config.journey, pick2]);
|
|
2010
|
+
const startTestAlong = React.useCallback(() => {
|
|
2011
|
+
setTestAlong({ active: true, index: 0 });
|
|
2012
|
+
setIsOpen(false);
|
|
2013
|
+
}, []);
|
|
2014
|
+
const exitTestAlong = React.useCallback(() => {
|
|
2015
|
+
setTestAlong({ active: false, index: 0 });
|
|
2016
|
+
}, []);
|
|
2017
|
+
const gotoStep = React.useCallback((index) => {
|
|
2018
|
+
setTestAlong((prev) => {
|
|
2019
|
+
if (!prev.active) return prev;
|
|
2020
|
+
const maxIndex = Math.max(0, testAlongSteps.length - 1);
|
|
2021
|
+
const clamped = Math.max(0, Math.min(index, maxIndex));
|
|
2022
|
+
if (clamped === prev.index) return prev;
|
|
2023
|
+
return { ...prev, index: clamped };
|
|
2024
|
+
});
|
|
2025
|
+
}, [testAlongSteps.length]);
|
|
2026
|
+
const gradeStep = React.useCallback((key, grade) => {
|
|
2027
|
+
if (grade === "pass") {
|
|
2028
|
+
setGuideChecked((prev) => {
|
|
2029
|
+
const next = new Set(prev);
|
|
2030
|
+
next.add(key);
|
|
2031
|
+
storage.setJSON(GUIDE_KEY, [...next]);
|
|
2032
|
+
return next;
|
|
2033
|
+
});
|
|
2034
|
+
setGuideFailed((prev) => {
|
|
2035
|
+
const next = new Set(prev);
|
|
2036
|
+
next.delete(key);
|
|
2037
|
+
storage.setJSON(GUIDE_FAILED_KEY, [...next]);
|
|
2038
|
+
return next;
|
|
2039
|
+
});
|
|
2040
|
+
} else {
|
|
2041
|
+
setGuideFailed((prev) => {
|
|
2042
|
+
const next = new Set(prev);
|
|
2043
|
+
next.add(key);
|
|
2044
|
+
storage.setJSON(GUIDE_FAILED_KEY, [...next]);
|
|
2045
|
+
return next;
|
|
2046
|
+
});
|
|
2047
|
+
setGuideChecked((prev) => {
|
|
2048
|
+
const next = new Set(prev);
|
|
2049
|
+
next.delete(key);
|
|
2050
|
+
storage.setJSON(GUIDE_KEY, [...next]);
|
|
2051
|
+
return next;
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
}, [storage]);
|
|
2055
|
+
const evidenceByStep = React.useMemo(() => {
|
|
2056
|
+
const map = /* @__PURE__ */ new Map();
|
|
2057
|
+
for (let i = notes.length - 1; i >= 0; i--) {
|
|
2058
|
+
const note = notes[i];
|
|
2059
|
+
const ref = note.journeyRef;
|
|
2060
|
+
if (!ref) continue;
|
|
2061
|
+
const key = `${ref.laneId}::${ref.path}`;
|
|
2062
|
+
const arr = map.get(key);
|
|
2063
|
+
if (arr) arr.push(note);
|
|
2064
|
+
else map.set(key, [note]);
|
|
2065
|
+
}
|
|
2066
|
+
return map;
|
|
2067
|
+
}, [notes]);
|
|
1318
2068
|
const addNote = React.useCallback(
|
|
1319
|
-
async ({
|
|
1320
|
-
description,
|
|
1321
|
-
screenshot,
|
|
1322
|
-
target
|
|
1323
|
-
}) => {
|
|
2069
|
+
async (input) => {
|
|
1324
2070
|
const loc = safeLocation();
|
|
2071
|
+
const route = loc.pathname + (loc.search ? "?\u2026" : "");
|
|
2072
|
+
let journeyRef;
|
|
2073
|
+
if (testAlong.active) {
|
|
2074
|
+
const step = testAlongSteps[testAlong.index];
|
|
2075
|
+
if (step) journeyRef = { laneId: step.laneId, path: step.path };
|
|
2076
|
+
} else {
|
|
2077
|
+
const hits = matchRouteToSteps(config.journey, route);
|
|
2078
|
+
if (hits.length) journeyRef = hits[0];
|
|
2079
|
+
}
|
|
2080
|
+
let context;
|
|
2081
|
+
if (config.captureContext !== false) {
|
|
2082
|
+
context = {
|
|
2083
|
+
events: drainSinceLastNote(),
|
|
2084
|
+
env: collectEnvSnapshot(route),
|
|
2085
|
+
forensics: input.forensics
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
1325
2088
|
const note = {
|
|
1326
2089
|
id: uid(),
|
|
1327
|
-
url: loc.href,
|
|
1328
|
-
route
|
|
2090
|
+
url: redactUrl(loc.href),
|
|
2091
|
+
route,
|
|
1329
2092
|
timestamp: nowIso(),
|
|
1330
|
-
description: (description || "").trim(),
|
|
1331
|
-
screenshot: screenshot ?? void 0,
|
|
1332
|
-
target: target ?? void 0
|
|
2093
|
+
description: (input.description || "").trim(),
|
|
2094
|
+
screenshot: input.screenshot ?? void 0,
|
|
2095
|
+
target: input.target ?? void 0,
|
|
2096
|
+
severity: input.severity,
|
|
2097
|
+
status: input.status,
|
|
2098
|
+
journeyRef,
|
|
2099
|
+
context
|
|
1333
2100
|
};
|
|
1334
2101
|
setNotes((prev) => [note, ...prev]);
|
|
1335
|
-
await idb.put(note);
|
|
1336
|
-
|
|
2102
|
+
const persisted = await idb.put(note);
|
|
2103
|
+
if (!persisted) {
|
|
2104
|
+
notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
|
|
2105
|
+
}
|
|
1337
2106
|
},
|
|
1338
|
-
[idb]
|
|
2107
|
+
[idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t]
|
|
1339
2108
|
);
|
|
1340
2109
|
const updateNote = React.useCallback(
|
|
1341
2110
|
async (id, patch) => {
|
|
@@ -1350,27 +2119,132 @@ function QaProvider({
|
|
|
1350
2119
|
} else if (patch.screenshot !== void 0) {
|
|
1351
2120
|
next.screenshot = patch.screenshot;
|
|
1352
2121
|
}
|
|
2122
|
+
if (patch.severity !== void 0) next.severity = patch.severity;
|
|
2123
|
+
if (patch.status !== void 0) next.status = patch.status;
|
|
1353
2124
|
updated = next;
|
|
1354
2125
|
return next;
|
|
1355
2126
|
})
|
|
1356
2127
|
);
|
|
1357
2128
|
if (updated) {
|
|
1358
|
-
await idb.put(updated);
|
|
2129
|
+
const persisted = await idb.put(updated);
|
|
2130
|
+
if (!persisted) {
|
|
2131
|
+
notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
|
|
2132
|
+
}
|
|
1359
2133
|
}
|
|
1360
2134
|
},
|
|
1361
|
-
[idb]
|
|
1362
|
-
);
|
|
1363
|
-
const deleteNote = React.useCallback(
|
|
1364
|
-
async (id) => {
|
|
1365
|
-
setNotes((prev) => prev.filter((n) => n.id !== id));
|
|
1366
|
-
await idb.delete(id);
|
|
1367
|
-
},
|
|
1368
|
-
[idb]
|
|
2135
|
+
[idb, notify, t]
|
|
1369
2136
|
);
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
2137
|
+
const deleteNote = React.useCallback(async (id) => {
|
|
2138
|
+
let removedNote = null;
|
|
2139
|
+
let removedAfterId = null;
|
|
2140
|
+
let found = false;
|
|
2141
|
+
setNotes((prev) => {
|
|
2142
|
+
const idx = prev.findIndex((n) => n.id === id);
|
|
2143
|
+
if (idx === -1) return prev;
|
|
2144
|
+
found = true;
|
|
2145
|
+
removedNote = prev[idx];
|
|
2146
|
+
removedAfterId = prev[idx + 1]?.id ?? null;
|
|
2147
|
+
return prev.filter((n) => n.id !== id);
|
|
2148
|
+
});
|
|
2149
|
+
if (!removedNote || !found) return;
|
|
2150
|
+
const noteToRestore = removedNote;
|
|
2151
|
+
const afterIdToRestore = removedAfterId;
|
|
2152
|
+
const existingPending = pendingDeletes.current.get(id);
|
|
2153
|
+
if (existingPending) clearTimeout(existingPending.timer);
|
|
2154
|
+
addPendingDeleteIds([id]);
|
|
2155
|
+
const timer = setTimeout(() => {
|
|
2156
|
+
pendingDeletes.current.delete(id);
|
|
2157
|
+
void idb.delete(id).then(() => removePendingDeleteIds([id]));
|
|
2158
|
+
}, SOFT_DELETE_MS);
|
|
2159
|
+
pendingDeletes.current.set(id, { note: noteToRestore, afterId: afterIdToRestore, timer });
|
|
2160
|
+
notify(t("note_deleted"), {
|
|
2161
|
+
duration: SOFT_DELETE_MS,
|
|
2162
|
+
id: `delete-${id}`,
|
|
2163
|
+
action: {
|
|
2164
|
+
label: t("undo"),
|
|
2165
|
+
onAction: () => {
|
|
2166
|
+
const pending = pendingDeletes.current.get(id);
|
|
2167
|
+
if (!pending) return;
|
|
2168
|
+
clearTimeout(pending.timer);
|
|
2169
|
+
pendingDeletes.current.delete(id);
|
|
2170
|
+
removePendingDeleteIds([id]);
|
|
2171
|
+
setNotes((prev) => {
|
|
2172
|
+
if (prev.some((n) => n.id === id)) return prev;
|
|
2173
|
+
const next = prev.slice();
|
|
2174
|
+
const anchorIndex = pending.afterId != null ? next.findIndex((n) => n.id === pending.afterId) : -1;
|
|
2175
|
+
const insertAt = anchorIndex === -1 ? next.length : anchorIndex;
|
|
2176
|
+
next.splice(insertAt, 0, pending.note);
|
|
2177
|
+
return next;
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
}, [idb, notify, t, addPendingDeleteIds, removePendingDeleteIds]);
|
|
2183
|
+
const clearNotes = React.useCallback(async () => {
|
|
2184
|
+
let snapshot = [];
|
|
2185
|
+
setNotes((prev) => {
|
|
2186
|
+
snapshot = prev;
|
|
2187
|
+
return [];
|
|
2188
|
+
});
|
|
2189
|
+
if (pendingClear.current) clearTimeout(pendingClear.current.timer);
|
|
2190
|
+
for (const [pendingId, pending] of pendingDeletes.current) {
|
|
2191
|
+
clearTimeout(pending.timer);
|
|
2192
|
+
dismissNotice(`delete-${pendingId}`);
|
|
2193
|
+
void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
|
|
2194
|
+
}
|
|
2195
|
+
pendingDeletes.current.clear();
|
|
2196
|
+
const snapshotIds = snapshot.map((n) => n.id);
|
|
2197
|
+
addPendingDeleteIds(snapshotIds);
|
|
2198
|
+
const timer = setTimeout(() => {
|
|
2199
|
+
pendingClear.current = null;
|
|
2200
|
+
void Promise.all(snapshot.map((n) => idb.delete(n.id))).then(
|
|
2201
|
+
() => removePendingDeleteIds(snapshotIds)
|
|
2202
|
+
);
|
|
2203
|
+
}, SOFT_DELETE_MS);
|
|
2204
|
+
pendingClear.current = { notes: snapshot, timer };
|
|
2205
|
+
notify(t("notes_cleared"), {
|
|
2206
|
+
duration: SOFT_DELETE_MS,
|
|
2207
|
+
id: "clear-all",
|
|
2208
|
+
action: {
|
|
2209
|
+
label: t("undo"),
|
|
2210
|
+
onAction: () => {
|
|
2211
|
+
const pending = pendingClear.current;
|
|
2212
|
+
if (!pending) return;
|
|
2213
|
+
clearTimeout(pending.timer);
|
|
2214
|
+
pendingClear.current = null;
|
|
2215
|
+
removePendingDeleteIds(pending.notes.map((n) => n.id));
|
|
2216
|
+
setNotes(pending.notes);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
});
|
|
2220
|
+
}, [idb, notify, dismissNotice, t, addPendingDeleteIds, removePendingDeleteIds]);
|
|
2221
|
+
const flushPendingDeletes = React.useCallback(() => {
|
|
2222
|
+
for (const [pendingId, pending] of pendingDeletes.current) {
|
|
2223
|
+
clearTimeout(pending.timer);
|
|
2224
|
+
void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
|
|
2225
|
+
}
|
|
2226
|
+
pendingDeletes.current.clear();
|
|
2227
|
+
if (pendingClear.current) {
|
|
2228
|
+
const { notes: clearedNotes } = pendingClear.current;
|
|
2229
|
+
clearTimeout(pendingClear.current.timer);
|
|
2230
|
+
pendingClear.current = null;
|
|
2231
|
+
const clearedIds = clearedNotes.map((n) => n.id);
|
|
2232
|
+
void Promise.all(clearedNotes.map((n) => idb.delete(n.id))).then(
|
|
2233
|
+
() => removePendingDeleteIds(clearedIds)
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
}, [idb, removePendingDeleteIds]);
|
|
2237
|
+
React.useEffect(() => {
|
|
2238
|
+
if (typeof window === "undefined") return void 0;
|
|
2239
|
+
const onBeforeUnload = () => flushPendingDeletes();
|
|
2240
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
2241
|
+
return () => {
|
|
2242
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
2243
|
+
flushPendingDeletes();
|
|
2244
|
+
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
2245
|
+
noticeTimers.current.clear();
|
|
2246
|
+
};
|
|
2247
|
+
}, [flushPendingDeletes]);
|
|
1374
2248
|
const startCapture = React.useCallback(() => {
|
|
1375
2249
|
setIsOpen(false);
|
|
1376
2250
|
setCaptureActive(true);
|
|
@@ -1415,17 +2289,19 @@ function QaProvider({
|
|
|
1415
2289
|
// Data
|
|
1416
2290
|
notes,
|
|
1417
2291
|
guideChecked,
|
|
2292
|
+
guideFailed,
|
|
1418
2293
|
loginsUsed,
|
|
1419
2294
|
// UI state
|
|
1420
2295
|
isOpen,
|
|
1421
2296
|
activeTab,
|
|
1422
2297
|
captureActive,
|
|
1423
2298
|
isExporting,
|
|
2299
|
+
notesLoading,
|
|
1424
2300
|
// i18n
|
|
1425
2301
|
lang,
|
|
1426
2302
|
dir: lang === "ar" ? "rtl" : "ltr",
|
|
1427
2303
|
// Config passthrough
|
|
1428
|
-
|
|
2304
|
+
namespace: config.namespace,
|
|
1429
2305
|
brand: config.brand,
|
|
1430
2306
|
loginField: config.loginField,
|
|
1431
2307
|
credentials: config.credentials,
|
|
@@ -1434,6 +2310,10 @@ function QaProvider({
|
|
|
1434
2310
|
// i18n helpers
|
|
1435
2311
|
t,
|
|
1436
2312
|
pick: pick2,
|
|
2313
|
+
// Notices
|
|
2314
|
+
notices,
|
|
2315
|
+
notify,
|
|
2316
|
+
dismissNotice,
|
|
1437
2317
|
// Actions
|
|
1438
2318
|
setIsOpen,
|
|
1439
2319
|
setActiveTab,
|
|
@@ -1441,15 +2321,26 @@ function QaProvider({
|
|
|
1441
2321
|
addNote,
|
|
1442
2322
|
updateNote,
|
|
1443
2323
|
deleteNote,
|
|
1444
|
-
|
|
2324
|
+
clearNotes,
|
|
1445
2325
|
startCapture,
|
|
1446
2326
|
endCapture,
|
|
1447
2327
|
toggleGuide,
|
|
1448
2328
|
toggleLogin,
|
|
2329
|
+
// Test-along
|
|
2330
|
+
testAlong,
|
|
2331
|
+
testAlongSteps,
|
|
2332
|
+
startTestAlong,
|
|
2333
|
+
exitTestAlong,
|
|
2334
|
+
gotoStep,
|
|
2335
|
+
gradeStep,
|
|
2336
|
+
evidenceByStep,
|
|
1449
2337
|
exportZip: exportZipFn
|
|
1450
2338
|
};
|
|
1451
2339
|
return /* @__PURE__ */ jsxRuntime.jsx(QaContext.Provider, { value, children });
|
|
1452
2340
|
}
|
|
2341
|
+
function journeyOrEmpty(journey) {
|
|
2342
|
+
return Array.isArray(journey) ? journey : [];
|
|
2343
|
+
}
|
|
1453
2344
|
function useQa() {
|
|
1454
2345
|
const ctx = React.useContext(QaContext);
|
|
1455
2346
|
if (!ctx) throw new Error("useQa must be used inside <QaProvider>");
|
|
@@ -1459,6 +2350,37 @@ var ICONS = {
|
|
|
1459
2350
|
Check: [
|
|
1460
2351
|
["path", { d: "M20 6 9 17l-5-5" }]
|
|
1461
2352
|
],
|
|
2353
|
+
Bug: [
|
|
2354
|
+
["path", { d: "m8 2 1.88 1.88" }],
|
|
2355
|
+
["path", { d: "M14.12 3.88 16 2" }],
|
|
2356
|
+
["path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }],
|
|
2357
|
+
["path", { d: "M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" }],
|
|
2358
|
+
["path", { d: "M12 20v-9" }],
|
|
2359
|
+
["path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }],
|
|
2360
|
+
["path", { d: "M6 13H2" }],
|
|
2361
|
+
["path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }],
|
|
2362
|
+
["path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }],
|
|
2363
|
+
["path", { d: "M22 13h-4" }],
|
|
2364
|
+
["path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" }]
|
|
2365
|
+
],
|
|
2366
|
+
AlertTriangle: [
|
|
2367
|
+
["path", { d: "m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" }],
|
|
2368
|
+
["path", { d: "M12 9v4" }],
|
|
2369
|
+
["path", { d: "M12 17h.01" }]
|
|
2370
|
+
],
|
|
2371
|
+
RotateCcw: [
|
|
2372
|
+
["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }],
|
|
2373
|
+
["path", { d: "M3 3v5h5" }]
|
|
2374
|
+
],
|
|
2375
|
+
ChevronLeft: [
|
|
2376
|
+
["path", { d: "m15 18-6-6 6-6" }]
|
|
2377
|
+
],
|
|
2378
|
+
ChevronRight: [
|
|
2379
|
+
["path", { d: "m9 18 6-6-6-6" }]
|
|
2380
|
+
],
|
|
2381
|
+
Play: [
|
|
2382
|
+
["polygon", { points: "6 3 20 12 6 21 6 3" }]
|
|
2383
|
+
],
|
|
1462
2384
|
X: [
|
|
1463
2385
|
["path", { d: "M18 6 6 18" }],
|
|
1464
2386
|
["path", { d: "m6 6 12 12" }]
|
|
@@ -1632,27 +2554,41 @@ var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
|
|
|
1632
2554
|
var FAB_SIZE_PX = 56;
|
|
1633
2555
|
var EDGE_MARGIN = 12;
|
|
1634
2556
|
var DRAG_THRESHOLD = 8;
|
|
1635
|
-
var
|
|
2557
|
+
var LEGACY_FAB_POS_KEY = "qapture:fabpos";
|
|
2558
|
+
function fabPosKey(namespace) {
|
|
2559
|
+
return `${namespace}:fabpos`;
|
|
2560
|
+
}
|
|
1636
2561
|
function isFabPos(v) {
|
|
1637
2562
|
if (!v || typeof v !== "object") return false;
|
|
1638
2563
|
const o = v;
|
|
1639
2564
|
return typeof o.left === "number" && Number.isFinite(o.left) && typeof o.bottom === "number" && Number.isFinite(o.bottom);
|
|
1640
2565
|
}
|
|
1641
|
-
function loadFabPos() {
|
|
2566
|
+
function loadFabPos(namespace) {
|
|
1642
2567
|
if (typeof window === "undefined") return null;
|
|
2568
|
+
const key = fabPosKey(namespace);
|
|
1643
2569
|
try {
|
|
1644
|
-
const raw = window.localStorage.getItem(
|
|
1645
|
-
if (
|
|
1646
|
-
|
|
1647
|
-
|
|
2570
|
+
const raw = window.localStorage.getItem(key);
|
|
2571
|
+
if (raw) {
|
|
2572
|
+
const parsed = JSON.parse(raw);
|
|
2573
|
+
if (isFabPos(parsed)) return parsed;
|
|
2574
|
+
}
|
|
2575
|
+
const legacyRaw = window.localStorage.getItem(LEGACY_FAB_POS_KEY);
|
|
2576
|
+
if (!legacyRaw) return null;
|
|
2577
|
+
const legacyParsed = JSON.parse(legacyRaw);
|
|
2578
|
+
if (!isFabPos(legacyParsed)) return null;
|
|
2579
|
+
try {
|
|
2580
|
+
window.localStorage.setItem(key, JSON.stringify(legacyParsed));
|
|
2581
|
+
} catch {
|
|
2582
|
+
}
|
|
2583
|
+
return legacyParsed;
|
|
1648
2584
|
} catch {
|
|
1649
2585
|
return null;
|
|
1650
2586
|
}
|
|
1651
2587
|
}
|
|
1652
|
-
function saveFabPos(pos) {
|
|
2588
|
+
function saveFabPos(namespace, pos) {
|
|
1653
2589
|
if (typeof window === "undefined") return;
|
|
1654
2590
|
try {
|
|
1655
|
-
window.localStorage.setItem(
|
|
2591
|
+
window.localStorage.setItem(fabPosKey(namespace), JSON.stringify(pos));
|
|
1656
2592
|
} catch {
|
|
1657
2593
|
}
|
|
1658
2594
|
}
|
|
@@ -1669,9 +2605,9 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
1669
2605
|
};
|
|
1670
2606
|
}
|
|
1671
2607
|
function QaFab() {
|
|
1672
|
-
const { isOpen, setIsOpen, notes, captureActive,
|
|
2608
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace } = useQa();
|
|
1673
2609
|
const coarse = useCoarsePointer();
|
|
1674
|
-
const [pos, setPos] = React.useState(() => loadFabPos());
|
|
2610
|
+
const [pos, setPos] = React.useState(() => loadFabPos(namespace));
|
|
1675
2611
|
const dragRef = React.useRef(null);
|
|
1676
2612
|
const didDragRef = React.useRef(false);
|
|
1677
2613
|
const [, setViewportTick] = React.useState(0);
|
|
@@ -1735,7 +2671,7 @@ function QaFab() {
|
|
|
1735
2671
|
const dy = e.clientY - d.startY;
|
|
1736
2672
|
const next = clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height);
|
|
1737
2673
|
setPos(next);
|
|
1738
|
-
saveFabPos(next);
|
|
2674
|
+
saveFabPos(namespace, next);
|
|
1739
2675
|
didDragRef.current = true;
|
|
1740
2676
|
}
|
|
1741
2677
|
};
|
|
@@ -1755,9 +2691,7 @@ function QaFab() {
|
|
|
1755
2691
|
bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
|
|
1756
2692
|
width: "3.5rem",
|
|
1757
2693
|
height: "3.5rem",
|
|
1758
|
-
|
|
1759
|
-
boxShadow: "0 20px 25px -5px rgba(0,0,0,0.1), 0 10px 10px -5px rgba(0,0,0,0.04), 0 0 0 2px rgba(255,255,255,0.7)",
|
|
1760
|
-
zIndex: 9990
|
|
2694
|
+
zIndex: "var(--qa-z-fab)"
|
|
1761
2695
|
};
|
|
1762
2696
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1763
2697
|
"button",
|
|
@@ -1772,7 +2706,7 @@ function QaFab() {
|
|
|
1772
2706
|
onPointerCancel: coarse ? onPointerCancel : void 0,
|
|
1773
2707
|
"aria-label": "Qapture \u2014 testing notes",
|
|
1774
2708
|
title: "Qapture",
|
|
1775
|
-
className: `qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-
|
|
2709
|
+
className: `qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-bg-accent qa-elev-2 qa-print-hidden qa-fab-btn${coarse ? " qa-touch-none" : ""}`,
|
|
1776
2710
|
style: fabStyle,
|
|
1777
2711
|
children: [
|
|
1778
2712
|
!isOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1787,17 +2721,14 @@ function QaFab() {
|
|
|
1787
2721
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1788
2722
|
"span",
|
|
1789
2723
|
{
|
|
1790
|
-
className: "qa-absolute qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-xs qa-font-bold",
|
|
2724
|
+
className: "qa-absolute qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-xs qa-font-bold qa-bg-1 qa-text-hi qa-border qa-border-subtle qa-elev-1",
|
|
1791
2725
|
"aria-label": `${notes.length} notes`,
|
|
1792
2726
|
style: {
|
|
1793
2727
|
top: "-4px",
|
|
1794
2728
|
right: "-4px",
|
|
1795
2729
|
minWidth: "1.5rem",
|
|
1796
2730
|
height: "1.5rem",
|
|
1797
|
-
padding: "0 4px"
|
|
1798
|
-
background: "#fff",
|
|
1799
|
-
color: theme.primary,
|
|
1800
|
-
boxShadow: "0 1px 3px rgba(0,0,0,0.2)"
|
|
2731
|
+
padding: "0 4px"
|
|
1801
2732
|
},
|
|
1802
2733
|
children: notes.length
|
|
1803
2734
|
}
|
|
@@ -1806,13 +2737,47 @@ function QaFab() {
|
|
|
1806
2737
|
}
|
|
1807
2738
|
);
|
|
1808
2739
|
}
|
|
2740
|
+
var SEVERITIES = [
|
|
2741
|
+
{ value: "bug", labelKey: "sev_bug", icon: "Bug" },
|
|
2742
|
+
{ value: "question", labelKey: "sev_question" },
|
|
2743
|
+
{ value: "polish", labelKey: "sev_polish" }
|
|
2744
|
+
];
|
|
2745
|
+
function SeverityChipRow({
|
|
2746
|
+
value,
|
|
2747
|
+
onChange,
|
|
2748
|
+
t
|
|
2749
|
+
}) {
|
|
2750
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2751
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mb-1 qa-text-11 qa-text-lo", children: t("severity_label") }),
|
|
2752
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1.5", role: "radiogroup", "aria-label": t("severity_label"), children: SEVERITIES.map((s) => {
|
|
2753
|
+
const active = value === s.value;
|
|
2754
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2755
|
+
"button",
|
|
2756
|
+
{
|
|
2757
|
+
type: "button",
|
|
2758
|
+
role: "radio",
|
|
2759
|
+
"aria-checked": active,
|
|
2760
|
+
onClick: () => onChange(s.value),
|
|
2761
|
+
className: `qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-2 qa-py-1 qa-text-11 qa-font-medium qa-transition ${active ? "qa-bg-accent" : "qa-bg-3 qa-text-mid qa-hover-bg-2"}`,
|
|
2762
|
+
style: { border: "none", cursor: "pointer" },
|
|
2763
|
+
children: [
|
|
2764
|
+
s.icon && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: s.icon, size: 12 }),
|
|
2765
|
+
t(s.labelKey)
|
|
2766
|
+
]
|
|
2767
|
+
},
|
|
2768
|
+
s.value
|
|
2769
|
+
);
|
|
2770
|
+
}) })
|
|
2771
|
+
] });
|
|
2772
|
+
}
|
|
1809
2773
|
function NoteEditor() {
|
|
1810
|
-
const { addNote, startCapture, t
|
|
2774
|
+
const { addNote, startCapture, t } = useQa();
|
|
1811
2775
|
const [open, setOpen] = React.useState(false);
|
|
1812
2776
|
const [description, setDescription] = React.useState("");
|
|
1813
2777
|
const [screenshot, setScreenshot] = React.useState(null);
|
|
1814
2778
|
const [previewUrl, setPreviewUrl] = React.useState(null);
|
|
1815
2779
|
const [dragOver, setDragOver] = React.useState(false);
|
|
2780
|
+
const [severity, setSeverity] = React.useState("bug");
|
|
1816
2781
|
const fileRef = React.useRef(null);
|
|
1817
2782
|
const previewUrlRef = React.useRef(null);
|
|
1818
2783
|
React.useEffect(() => {
|
|
@@ -1863,11 +2828,18 @@ function NoteEditor() {
|
|
|
1863
2828
|
if (f?.type.startsWith("image/")) setImage(f);
|
|
1864
2829
|
e.target.value = "";
|
|
1865
2830
|
};
|
|
2831
|
+
const resetForm = () => {
|
|
2832
|
+
setOpen(false);
|
|
2833
|
+
clearImage();
|
|
2834
|
+
setDescription("");
|
|
2835
|
+
setSeverity("bug");
|
|
2836
|
+
};
|
|
1866
2837
|
const save = async () => {
|
|
1867
2838
|
if (!description.trim()) return;
|
|
1868
|
-
await addNote({ description, screenshot: screenshot ?? void 0 });
|
|
2839
|
+
await addNote({ description, screenshot: screenshot ?? void 0, severity });
|
|
1869
2840
|
setDescription("");
|
|
1870
2841
|
clearImage();
|
|
2842
|
+
setSeverity("bug");
|
|
1871
2843
|
setOpen(false);
|
|
1872
2844
|
};
|
|
1873
2845
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
|
|
@@ -1875,12 +2847,8 @@ function NoteEditor() {
|
|
|
1875
2847
|
"button",
|
|
1876
2848
|
{
|
|
1877
2849
|
onClick: startCapture,
|
|
1878
|
-
className: "qa-flex qa-w-full qa-items-center qa-justify-center qa-gap-2 qa-rounded-xl qa-px-4 qa-py-3 qa-text-sm qa-font-semibold qa-
|
|
1879
|
-
style: {
|
|
1880
|
-
backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
|
|
1881
|
-
border: "none",
|
|
1882
|
-
cursor: "pointer"
|
|
1883
|
-
},
|
|
2850
|
+
className: "qa-tap qa-flex qa-w-full qa-items-center qa-justify-center qa-gap-2 qa-rounded-xl qa-bg-accent qa-px-4 qa-py-3 qa-text-sm qa-font-semibold qa-shadow-sm qa-transition qa-hover-brightness-105",
|
|
2851
|
+
style: { border: "none", cursor: "pointer" },
|
|
1884
2852
|
children: [
|
|
1885
2853
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 16 }),
|
|
1886
2854
|
t("capture_cta")
|
|
@@ -1891,13 +2859,8 @@ function NoteEditor() {
|
|
|
1891
2859
|
"button",
|
|
1892
2860
|
{
|
|
1893
2861
|
onClick: () => setOpen(true),
|
|
1894
|
-
className: "qa-flex qa-w-full qa-items-center qa-justify-center qa-gap-1 qa-rounded-lg qa-border qa-border-dashed qa-py-1.5 qa-text-xs qa-
|
|
1895
|
-
style: {
|
|
1896
|
-
borderColor: `${theme.primary}33`,
|
|
1897
|
-
color: theme.primary,
|
|
1898
|
-
background: "transparent",
|
|
1899
|
-
cursor: "pointer"
|
|
1900
|
-
},
|
|
2862
|
+
className: "qa-tap qa-flex qa-w-full qa-items-center qa-justify-center qa-gap-1 qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-py-1.5 qa-text-xs qa-text-accent",
|
|
2863
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
1901
2864
|
children: [
|
|
1902
2865
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 14 }),
|
|
1903
2866
|
t("quick_note")
|
|
@@ -1907,8 +2870,7 @@ function NoteEditor() {
|
|
|
1907
2870
|
"div",
|
|
1908
2871
|
{
|
|
1909
2872
|
onPaste,
|
|
1910
|
-
className: "qa-space-y-2 qa-rounded-xl qa-border qa-p-2.5",
|
|
1911
|
-
style: { borderColor: `${theme.primary}1a`, background: theme.cream },
|
|
2873
|
+
className: "qa-space-y-2 qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-2.5",
|
|
1912
2874
|
children: [
|
|
1913
2875
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1914
2876
|
"textarea",
|
|
@@ -1918,10 +2880,10 @@ function NoteEditor() {
|
|
|
1918
2880
|
onChange: (e) => setDescription(e.target.value),
|
|
1919
2881
|
rows: 3,
|
|
1920
2882
|
placeholder: t("desc_placeholder"),
|
|
1921
|
-
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
1922
|
-
style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
|
|
2883
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-1 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
1923
2884
|
}
|
|
1924
2885
|
),
|
|
2886
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeverityChipRow, { value: severity, onChange: setSeverity, t }),
|
|
1925
2887
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1926
2888
|
"div",
|
|
1927
2889
|
{
|
|
@@ -1931,11 +2893,7 @@ function NoteEditor() {
|
|
|
1931
2893
|
},
|
|
1932
2894
|
onDragLeave: () => setDragOver(false),
|
|
1933
2895
|
onDrop,
|
|
1934
|
-
className:
|
|
1935
|
-
style: {
|
|
1936
|
-
borderColor: dragOver ? theme.accent : `${theme.primary}33`,
|
|
1937
|
-
background: dragOver ? `${theme.accent}12` : "#fff"
|
|
1938
|
-
},
|
|
2896
|
+
className: `qa-rounded-lg qa-border qa-border-dashed qa-px-2 qa-py-2 qa-text-center qa-text-xs ${dragOver ? "qa-border-accent qa-bg-accent-tint" : "qa-border-subtle qa-bg-1"}`,
|
|
1939
2897
|
children: [
|
|
1940
2898
|
previewUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-relative qa-inline-block", children: [
|
|
1941
2899
|
/* @__PURE__ */ jsxRuntime.jsx("img", { src: previewUrl, alt: "preview", style: { maxHeight: "7rem", borderRadius: "0.25rem" } }),
|
|
@@ -1943,11 +2901,10 @@ function NoteEditor() {
|
|
|
1943
2901
|
"button",
|
|
1944
2902
|
{
|
|
1945
2903
|
onClick: clearImage,
|
|
1946
|
-
className: "qa-absolute qa-rounded-full qa-
|
|
2904
|
+
className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
|
|
1947
2905
|
style: {
|
|
1948
2906
|
top: "-8px",
|
|
1949
2907
|
insetInlineEnd: "-8px",
|
|
1950
|
-
background: theme.primary,
|
|
1951
2908
|
border: "none",
|
|
1952
2909
|
cursor: "pointer"
|
|
1953
2910
|
},
|
|
@@ -1958,8 +2915,8 @@ function NoteEditor() {
|
|
|
1958
2915
|
"button",
|
|
1959
2916
|
{
|
|
1960
2917
|
onClick: () => fileRef.current?.click(),
|
|
1961
|
-
className: "qa-inline-flex qa-items-center qa-gap-1 qa-
|
|
1962
|
-
style: {
|
|
2918
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
|
|
2919
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
1963
2920
|
children: [
|
|
1964
2921
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ImagePlus", size: 16 }),
|
|
1965
2922
|
t("image_hint")
|
|
@@ -1983,28 +2940,19 @@ function NoteEditor() {
|
|
|
1983
2940
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1984
2941
|
"button",
|
|
1985
2942
|
{
|
|
1986
|
-
onClick: save,
|
|
2943
|
+
onClick: () => void save(),
|
|
1987
2944
|
disabled: !description.trim(),
|
|
1988
|
-
className: "qa-flex-1 qa-rounded-lg qa-px-3 qa-py-1.5 qa-text-sm qa-font-semibold
|
|
1989
|
-
style: {
|
|
2945
|
+
className: "qa-tap qa-flex-1 qa-rounded-lg qa-bg-accent qa-px-3 qa-py-1.5 qa-text-sm qa-font-semibold",
|
|
2946
|
+
style: { border: "none", cursor: "pointer" },
|
|
1990
2947
|
children: t("add_point")
|
|
1991
2948
|
}
|
|
1992
2949
|
),
|
|
1993
2950
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1994
2951
|
"button",
|
|
1995
2952
|
{
|
|
1996
|
-
onClick:
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
setDescription("");
|
|
2000
|
-
},
|
|
2001
|
-
className: "qa-rounded-lg qa-border qa-px-3 qa-text-sm qa-tap",
|
|
2002
|
-
style: {
|
|
2003
|
-
borderColor: `${theme.primary}33`,
|
|
2004
|
-
color: theme.primary,
|
|
2005
|
-
background: "transparent",
|
|
2006
|
-
cursor: "pointer"
|
|
2007
|
-
},
|
|
2953
|
+
onClick: resetForm,
|
|
2954
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
|
|
2955
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
2008
2956
|
children: t("cancel")
|
|
2009
2957
|
}
|
|
2010
2958
|
)
|
|
@@ -2017,16 +2965,11 @@ function NoteEditor() {
|
|
|
2017
2965
|
|
|
2018
2966
|
// src/lib/highlight.ts
|
|
2019
2967
|
var SETTLE_TIMEOUT_MS = 400;
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
return val || fallback;
|
|
2024
|
-
}
|
|
2025
|
-
function paint(rect, colors) {
|
|
2968
|
+
var ACCENT = "#4D9CFF";
|
|
2969
|
+
var DANGER = "#FF6B6B";
|
|
2970
|
+
function paint(rect) {
|
|
2026
2971
|
if (typeof document === "undefined") return;
|
|
2027
2972
|
if (!rect || rect.width < 1 || rect.height < 1) return;
|
|
2028
|
-
const accent = colors?.accent ?? readCssVar("--qa-accent", "#7c3aed");
|
|
2029
|
-
const primary = colors?.primary ?? readCssVar("--qa-primary", "#4f46e5");
|
|
2030
2973
|
const box = document.createElement("div");
|
|
2031
2974
|
box.setAttribute("data-qa-overlay", "true");
|
|
2032
2975
|
Object.assign(box.style, {
|
|
@@ -2038,9 +2981,9 @@ function paint(rect, colors) {
|
|
|
2038
2981
|
zIndex: "10098",
|
|
2039
2982
|
pointerEvents: "none",
|
|
2040
2983
|
borderRadius: "3px",
|
|
2041
|
-
outline: `3px solid ${
|
|
2042
|
-
background: `${
|
|
2043
|
-
boxShadow: `0 0 0 4px ${
|
|
2984
|
+
outline: `3px solid ${ACCENT}`,
|
|
2985
|
+
background: `${ACCENT}22`,
|
|
2986
|
+
boxShadow: `0 0 0 4px ${DANGER}55`,
|
|
2044
2987
|
transition: "opacity 0.45s ease",
|
|
2045
2988
|
opacity: "1"
|
|
2046
2989
|
});
|
|
@@ -2052,9 +2995,9 @@ function paint(rect, colors) {
|
|
|
2052
2995
|
if (box.parentNode) box.remove();
|
|
2053
2996
|
}, 1500);
|
|
2054
2997
|
}
|
|
2055
|
-
function settleThenPaint(el
|
|
2056
|
-
const
|
|
2057
|
-
const start =
|
|
2998
|
+
function settleThenPaint(el) {
|
|
2999
|
+
const now2 = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
3000
|
+
const start = now2();
|
|
2058
3001
|
let last = null;
|
|
2059
3002
|
let stableFrames = 0;
|
|
2060
3003
|
const tick = () => {
|
|
@@ -2062,15 +3005,15 @@ function settleThenPaint(el, colors) {
|
|
|
2062
3005
|
const unchanged = !!last && r.top === last.top && r.left === last.left && r.width === last.width && r.height === last.height;
|
|
2063
3006
|
stableFrames = unchanged ? stableFrames + 1 : 0;
|
|
2064
3007
|
last = r;
|
|
2065
|
-
if (stableFrames >= 2 ||
|
|
2066
|
-
paint({ top: r.top, left: r.left, width: r.width, height: r.height }
|
|
3008
|
+
if (stableFrames >= 2 || now2() - start >= SETTLE_TIMEOUT_MS) {
|
|
3009
|
+
paint({ top: r.top, left: r.left, width: r.width, height: r.height });
|
|
2067
3010
|
return;
|
|
2068
3011
|
}
|
|
2069
3012
|
requestAnimationFrame(tick);
|
|
2070
3013
|
};
|
|
2071
3014
|
requestAnimationFrame(tick);
|
|
2072
3015
|
}
|
|
2073
|
-
function flashLocate(target
|
|
3016
|
+
function flashLocate(target) {
|
|
2074
3017
|
if (typeof document === "undefined" || !target) return;
|
|
2075
3018
|
let el = null;
|
|
2076
3019
|
if (target.selector) {
|
|
@@ -2082,7 +3025,7 @@ function flashLocate(target, colors) {
|
|
|
2082
3025
|
}
|
|
2083
3026
|
if (el) {
|
|
2084
3027
|
el.scrollIntoView({ block: "center", inline: "center" });
|
|
2085
|
-
settleThenPaint(el
|
|
3028
|
+
settleThenPaint(el);
|
|
2086
3029
|
} else if (target.rect) {
|
|
2087
3030
|
let rect = target.rect;
|
|
2088
3031
|
const snap = target.scroll;
|
|
@@ -2093,106 +3036,92 @@ function flashLocate(target, colors) {
|
|
|
2093
3036
|
rect = { ...rect, left: rect.left - dx, top: rect.top - dy };
|
|
2094
3037
|
}
|
|
2095
3038
|
}
|
|
2096
|
-
paint(rect
|
|
3039
|
+
paint(rect);
|
|
2097
3040
|
}
|
|
2098
3041
|
}
|
|
2099
3042
|
function LocationReveal({ target }) {
|
|
2100
|
-
const { t
|
|
3043
|
+
const { t } = useQa();
|
|
2101
3044
|
const [open, setOpen] = React.useState(false);
|
|
2102
3045
|
if (!target) return null;
|
|
2103
3046
|
const r = target.rect;
|
|
2104
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2105
|
-
"div",
|
|
2106
|
-
|
|
2107
|
-
className: "qa-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
name: "ChevronDown",
|
|
2125
|
-
size: 14,
|
|
2126
|
-
style: {
|
|
2127
|
-
transition: "transform 150ms",
|
|
2128
|
-
transform: open ? "rotate(180deg)" : "rotate(0deg)"
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
)
|
|
2132
|
-
]
|
|
2133
|
-
}
|
|
2134
|
-
)
|
|
2135
|
-
] }),
|
|
2136
|
-
open && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2137
|
-
"div",
|
|
2138
|
-
{
|
|
2139
|
-
className: "qa-space-y-1 qa-px-2 qa-pb-2 qa-text-11 qa-dir-ltr",
|
|
2140
|
-
style: { color: theme.ink },
|
|
2141
|
-
children: [
|
|
2142
|
-
target.selector && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-1", children: [
|
|
2143
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "selector" }),
|
|
2144
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2145
|
-
"code",
|
|
2146
|
-
{
|
|
2147
|
-
className: "qa-min-w-0 qa-flex-1 qa-truncate qa-rounded qa-bg-white qa-px-1",
|
|
2148
|
-
title: target.selector,
|
|
2149
|
-
children: target.selector
|
|
2150
|
-
}
|
|
2151
|
-
)
|
|
2152
|
-
] }),
|
|
2153
|
-
target.tagName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2154
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "tag " }),
|
|
2155
|
-
/* @__PURE__ */ jsxRuntime.jsxs("code", { className: "qa-rounded qa-bg-white qa-px-1", children: [
|
|
2156
|
-
"<",
|
|
2157
|
-
target.tagName,
|
|
2158
|
-
">"
|
|
2159
|
-
] })
|
|
2160
|
-
] }),
|
|
2161
|
-
target.text && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-truncate", children: [
|
|
2162
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "text " }),
|
|
2163
|
-
'"',
|
|
2164
|
-
target.text,
|
|
2165
|
-
'"'
|
|
2166
|
-
] }),
|
|
2167
|
-
r && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2168
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "pos " }),
|
|
2169
|
-
Math.round(r.left),
|
|
2170
|
-
", ",
|
|
2171
|
-
Math.round(r.top),
|
|
2172
|
-
" \xB7 ",
|
|
2173
|
-
Math.round(r.width),
|
|
2174
|
-
"\xD7",
|
|
2175
|
-
Math.round(r.height)
|
|
2176
|
-
] }),
|
|
2177
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2178
|
-
"button",
|
|
2179
|
-
{
|
|
2180
|
-
onClick: () => flashLocate(target, { primary: theme.primary, accent: theme.accent }),
|
|
2181
|
-
className: "qa-mt-1 qa-inline-flex qa-items-center qa-gap-1 qa-rounded-md qa-px-2 qa-py-1 qa-font-medium qa-text-white qa-tap",
|
|
2182
|
-
style: { background: theme.accent, border: "none", cursor: "pointer" },
|
|
2183
|
-
children: [
|
|
2184
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 12 }),
|
|
2185
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPinned", size: 12 }),
|
|
2186
|
-
t("loc_locate")
|
|
2187
|
-
]
|
|
3047
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-lg qa-border qa-border-subtle qa-bg-2", children: [
|
|
3048
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1.5 qa-px-2 qa-py-1.5 qa-text-11", children: [
|
|
3049
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 14, className: "qa-text-success" }),
|
|
3050
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-font-medium qa-text-hi", children: t("loc_captured") }),
|
|
3051
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3052
|
+
"button",
|
|
3053
|
+
{
|
|
3054
|
+
onClick: () => setOpen((o) => !o),
|
|
3055
|
+
className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-font-medium qa-tap qa-text-accent",
|
|
3056
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3057
|
+
children: [
|
|
3058
|
+
open ? t("loc_hide") : t("loc_show"),
|
|
3059
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3060
|
+
Icon,
|
|
3061
|
+
{
|
|
3062
|
+
name: "ChevronDown",
|
|
3063
|
+
size: 14,
|
|
3064
|
+
style: {
|
|
3065
|
+
transition: "transform 150ms",
|
|
3066
|
+
transform: open ? "rotate(180deg)" : "rotate(0deg)"
|
|
2188
3067
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
3068
|
+
}
|
|
3069
|
+
)
|
|
3070
|
+
]
|
|
3071
|
+
}
|
|
3072
|
+
)
|
|
3073
|
+
] }),
|
|
3074
|
+
open && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-1 qa-px-2 qa-pb-2 qa-text-11 qa-dir-ltr qa-text-hi", children: [
|
|
3075
|
+
target.selector && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-1", children: [
|
|
3076
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "selector" }),
|
|
3077
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3078
|
+
"code",
|
|
3079
|
+
{
|
|
3080
|
+
className: "qa-min-w-0 qa-flex-1 qa-truncate qa-rounded qa-bg-3 qa-px-1",
|
|
3081
|
+
title: target.selector,
|
|
3082
|
+
children: target.selector
|
|
2191
3083
|
}
|
|
2192
3084
|
)
|
|
2193
|
-
]
|
|
2194
|
-
|
|
2195
|
-
|
|
3085
|
+
] }),
|
|
3086
|
+
target.tagName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3087
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "tag " }),
|
|
3088
|
+
/* @__PURE__ */ jsxRuntime.jsxs("code", { className: "qa-rounded qa-bg-3 qa-px-1", children: [
|
|
3089
|
+
"<",
|
|
3090
|
+
target.tagName,
|
|
3091
|
+
">"
|
|
3092
|
+
] })
|
|
3093
|
+
] }),
|
|
3094
|
+
target.text && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-truncate", children: [
|
|
3095
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "text " }),
|
|
3096
|
+
'"',
|
|
3097
|
+
target.text,
|
|
3098
|
+
'"'
|
|
3099
|
+
] }),
|
|
3100
|
+
r && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3101
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "pos " }),
|
|
3102
|
+
Math.round(r.left),
|
|
3103
|
+
", ",
|
|
3104
|
+
Math.round(r.top),
|
|
3105
|
+
" \xB7 ",
|
|
3106
|
+
Math.round(r.width),
|
|
3107
|
+
"\xD7",
|
|
3108
|
+
Math.round(r.height)
|
|
3109
|
+
] }),
|
|
3110
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3111
|
+
"button",
|
|
3112
|
+
{
|
|
3113
|
+
onClick: () => flashLocate(target),
|
|
3114
|
+
className: "qa-mt-1 qa-inline-flex qa-items-center qa-gap-1 qa-rounded-md qa-px-2 qa-py-1 qa-font-medium qa-tap qa-bg-accent",
|
|
3115
|
+
style: { border: "none", cursor: "pointer" },
|
|
3116
|
+
children: [
|
|
3117
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 12 }),
|
|
3118
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPinned", size: 12 }),
|
|
3119
|
+
t("loc_locate")
|
|
3120
|
+
]
|
|
3121
|
+
}
|
|
3122
|
+
)
|
|
3123
|
+
] })
|
|
3124
|
+
] });
|
|
2196
3125
|
}
|
|
2197
3126
|
function useObjectUrl(blob) {
|
|
2198
3127
|
const [url, setUrl] = React.useState(null);
|
|
@@ -2209,11 +3138,10 @@ function useObjectUrl(blob) {
|
|
|
2209
3138
|
}
|
|
2210
3139
|
function KindBadge({
|
|
2211
3140
|
target,
|
|
2212
|
-
t
|
|
2213
|
-
theme
|
|
3141
|
+
t
|
|
2214
3142
|
}) {
|
|
2215
3143
|
if (!target) {
|
|
2216
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-
|
|
3144
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-lo", children: [
|
|
2217
3145
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "FileText", size: 12 }),
|
|
2218
3146
|
t("kind_note")
|
|
2219
3147
|
] });
|
|
@@ -2222,8 +3150,7 @@ function KindBadge({
|
|
|
2222
3150
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2223
3151
|
"span",
|
|
2224
3152
|
{
|
|
2225
|
-
className:
|
|
2226
|
-
style: { background: region ? theme.accentDark : theme.primary },
|
|
3153
|
+
className: `qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10 qa-font-medium ${region ? "qa-bg-accent-tint qa-text-accent" : "qa-bg-3 qa-text-hi"}`,
|
|
2227
3154
|
children: [
|
|
2228
3155
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: region ? "Square" : "MousePointerClick", size: 10 }),
|
|
2229
3156
|
region ? t("kind_region") : t("kind_element")
|
|
@@ -2231,13 +3158,59 @@ function KindBadge({
|
|
|
2231
3158
|
}
|
|
2232
3159
|
);
|
|
2233
3160
|
}
|
|
3161
|
+
var SEVERITY_CLASS = {
|
|
3162
|
+
bug: "qa-bg-danger-tint qa-text-danger",
|
|
3163
|
+
question: "qa-bg-warn-tint qa-text-warn",
|
|
3164
|
+
polish: "qa-bg-accent-tint qa-text-accent"
|
|
3165
|
+
};
|
|
3166
|
+
var SEVERITY_LABEL_KEY = {
|
|
3167
|
+
bug: "sev_bug",
|
|
3168
|
+
question: "sev_question",
|
|
3169
|
+
polish: "sev_polish"
|
|
3170
|
+
};
|
|
3171
|
+
function SeverityChip({ severity, t }) {
|
|
3172
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3173
|
+
"span",
|
|
3174
|
+
{
|
|
3175
|
+
className: `qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10 qa-font-medium ${SEVERITY_CLASS[severity]}`,
|
|
3176
|
+
children: [
|
|
3177
|
+
severity === "bug" && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Bug", size: 10 }),
|
|
3178
|
+
t(SEVERITY_LABEL_KEY[severity])
|
|
3179
|
+
]
|
|
3180
|
+
}
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
function StatusPill({
|
|
3184
|
+
status,
|
|
3185
|
+
onToggle,
|
|
3186
|
+
t
|
|
3187
|
+
}) {
|
|
3188
|
+
const verified = status === "verified";
|
|
3189
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3190
|
+
"button",
|
|
3191
|
+
{
|
|
3192
|
+
type: "button",
|
|
3193
|
+
onClick: onToggle,
|
|
3194
|
+
"aria-label": t(verified ? "status_verified" : "status_open"),
|
|
3195
|
+
className: `qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10 qa-font-medium qa-transition ${verified ? "qa-bg-success-tint qa-text-success" : "qa-bg-3 qa-text-mid qa-hover-bg-2"}`,
|
|
3196
|
+
style: { border: "none", cursor: "pointer" },
|
|
3197
|
+
children: [
|
|
3198
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: verified ? "CheckCircle2" : "Circle", size: 10 }),
|
|
3199
|
+
t(verified ? "status_verified" : "status_open")
|
|
3200
|
+
]
|
|
3201
|
+
}
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
2234
3204
|
function NoteItem({ note, index }) {
|
|
2235
|
-
const { deleteNote, updateNote,
|
|
3205
|
+
const { deleteNote, updateNote, notify, t } = useQa();
|
|
2236
3206
|
const [editing, setEditing] = React.useState(false);
|
|
2237
3207
|
const [desc, setDesc] = React.useState(note.description);
|
|
2238
3208
|
const [img, setImg] = React.useState(note.screenshot ?? null);
|
|
2239
3209
|
const fileRef = React.useRef(null);
|
|
2240
3210
|
const thumbUrl = useObjectUrl(editing ? img ?? void 0 : note.screenshot);
|
|
3211
|
+
const severity = note.severity ?? "bug";
|
|
3212
|
+
const status = note.status ?? "open";
|
|
3213
|
+
const contextEventCount = note.context?.events.length ?? 0;
|
|
2241
3214
|
const startEdit = () => {
|
|
2242
3215
|
setDesc(note.description);
|
|
2243
3216
|
setImg(note.screenshot ?? null);
|
|
@@ -2266,188 +3239,178 @@ function NoteItem({ note, index }) {
|
|
|
2266
3239
|
updateNote(note.id, patch);
|
|
2267
3240
|
setEditing(false);
|
|
2268
3241
|
};
|
|
2269
|
-
|
|
2270
|
-
"
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
)
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
3242
|
+
const toggleStatus = () => {
|
|
3243
|
+
updateNote(note.id, { status: status === "open" ? "verified" : "open" });
|
|
3244
|
+
};
|
|
3245
|
+
const copyPrompt = async () => {
|
|
3246
|
+
try {
|
|
3247
|
+
if (!navigator.clipboard?.writeText) throw new Error("clipboard unavailable");
|
|
3248
|
+
await navigator.clipboard.writeText(noteToMarkdown(note));
|
|
3249
|
+
notify(t("copied"));
|
|
3250
|
+
} catch {
|
|
3251
|
+
notify(t("copy_failed"), { tone: "error" });
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-1 qa-elev-1 qa-p-3 qa-text-sm qa-text-hi", children: [
|
|
3255
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-1 qa-flex qa-flex-wrap qa-items-center qa-gap-1.5", children: [
|
|
3256
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-flex qa-h-5 qa-w-5 qa-items-center qa-justify-center qa-rounded-full qa-text-11 qa-font-bold qa-bg-accent", children: index }),
|
|
3257
|
+
/* @__PURE__ */ jsxRuntime.jsx(KindBadge, { target: note.target, t }),
|
|
3258
|
+
/* @__PURE__ */ jsxRuntime.jsx(SeverityChip, { severity, t }),
|
|
3259
|
+
/* @__PURE__ */ jsxRuntime.jsx(StatusPill, { status, onToggle: toggleStatus, t }),
|
|
3260
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-ms-auto qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3261
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3262
|
+
"button",
|
|
3263
|
+
{
|
|
3264
|
+
onClick: () => void copyPrompt(),
|
|
3265
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
|
|
3266
|
+
title: t("copy_prompt"),
|
|
3267
|
+
"aria-label": t("copy_prompt"),
|
|
3268
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3269
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Copy", size: 14 })
|
|
3270
|
+
}
|
|
3271
|
+
),
|
|
3272
|
+
!editing && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3273
|
+
"button",
|
|
3274
|
+
{
|
|
3275
|
+
onClick: startEdit,
|
|
3276
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
|
|
3277
|
+
title: t("edit"),
|
|
3278
|
+
"aria-label": t("edit"),
|
|
3279
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3280
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 14 })
|
|
3281
|
+
}
|
|
3282
|
+
),
|
|
3283
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3284
|
+
"button",
|
|
3285
|
+
{
|
|
3286
|
+
onClick: () => deleteNote(note.id),
|
|
3287
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-red",
|
|
3288
|
+
"aria-label": "delete",
|
|
3289
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3290
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash2", size: 16 })
|
|
3291
|
+
}
|
|
3292
|
+
)
|
|
3293
|
+
] })
|
|
3294
|
+
] }),
|
|
3295
|
+
editing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", onPaste, children: [
|
|
3296
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3297
|
+
"textarea",
|
|
3298
|
+
{
|
|
3299
|
+
autoFocus: true,
|
|
3300
|
+
value: desc,
|
|
3301
|
+
onChange: (e) => setDesc(e.target.value),
|
|
3302
|
+
rows: 3,
|
|
3303
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-2 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
3304
|
+
}
|
|
3305
|
+
),
|
|
3306
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-lg qa-border qa-border-dashed qa-border-subtle qa-p-2 qa-text-center qa-text-xs", children: [
|
|
3307
|
+
thumbUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-relative qa-inline-block", children: [
|
|
2310
3308
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2311
|
-
"
|
|
2312
|
-
{
|
|
2313
|
-
autoFocus: true,
|
|
2314
|
-
value: desc,
|
|
2315
|
-
onChange: (e) => setDesc(e.target.value),
|
|
2316
|
-
rows: 3,
|
|
2317
|
-
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring",
|
|
2318
|
-
style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
|
|
2319
|
-
}
|
|
2320
|
-
),
|
|
2321
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2322
|
-
"div",
|
|
3309
|
+
"img",
|
|
2323
3310
|
{
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
thumbUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-relative qa-inline-block", children: [
|
|
2328
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2329
|
-
"img",
|
|
2330
|
-
{
|
|
2331
|
-
src: thumbUrl,
|
|
2332
|
-
alt: "screenshot",
|
|
2333
|
-
style: { maxHeight: "7rem", borderRadius: "0.25rem" }
|
|
2334
|
-
}
|
|
2335
|
-
),
|
|
2336
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2337
|
-
"button",
|
|
2338
|
-
{
|
|
2339
|
-
onClick: () => setImg(null),
|
|
2340
|
-
className: "qa-absolute qa-rounded-full qa-p-1 qa-text-white qa-tap-icon",
|
|
2341
|
-
title: t("remove_image"),
|
|
2342
|
-
style: {
|
|
2343
|
-
top: "-8px",
|
|
2344
|
-
insetInlineEnd: "-8px",
|
|
2345
|
-
background: theme.primary,
|
|
2346
|
-
border: "none",
|
|
2347
|
-
cursor: "pointer"
|
|
2348
|
-
},
|
|
2349
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 12 })
|
|
2350
|
-
}
|
|
2351
|
-
)
|
|
2352
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2353
|
-
"button",
|
|
2354
|
-
{
|
|
2355
|
-
onClick: () => fileRef.current?.click(),
|
|
2356
|
-
className: "qa-inline-flex qa-items-center qa-gap-1",
|
|
2357
|
-
style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
|
|
2358
|
-
children: [
|
|
2359
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ImagePlus", size: 16 }),
|
|
2360
|
-
t("image_hint")
|
|
2361
|
-
]
|
|
2362
|
-
}
|
|
2363
|
-
),
|
|
2364
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2365
|
-
"input",
|
|
2366
|
-
{
|
|
2367
|
-
ref: fileRef,
|
|
2368
|
-
type: "file",
|
|
2369
|
-
accept: "image/*",
|
|
2370
|
-
onChange: onFile,
|
|
2371
|
-
className: "qa-hidden"
|
|
2372
|
-
}
|
|
2373
|
-
)
|
|
2374
|
-
]
|
|
3311
|
+
src: thumbUrl,
|
|
3312
|
+
alt: "screenshot",
|
|
3313
|
+
style: { maxHeight: "7rem", borderRadius: "0.25rem" }
|
|
2375
3314
|
}
|
|
2376
3315
|
),
|
|
2377
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-2", children: [
|
|
2378
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2379
|
-
"button",
|
|
2380
|
-
{
|
|
2381
|
-
onClick: save,
|
|
2382
|
-
disabled: !desc.trim(),
|
|
2383
|
-
className: "qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1 qa-rounded-lg qa-px-3 qa-py-1.5 qa-text-sm qa-font-semibold qa-text-white qa-tap",
|
|
2384
|
-
style: { background: theme.accent, border: "none", cursor: "pointer" },
|
|
2385
|
-
children: [
|
|
2386
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
|
|
2387
|
-
t("save")
|
|
2388
|
-
]
|
|
2389
|
-
}
|
|
2390
|
-
),
|
|
2391
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2392
|
-
"button",
|
|
2393
|
-
{
|
|
2394
|
-
onClick: () => setEditing(false),
|
|
2395
|
-
className: "qa-rounded-lg qa-border qa-px-3 qa-text-sm qa-tap",
|
|
2396
|
-
style: {
|
|
2397
|
-
borderColor: `${theme.primary}33`,
|
|
2398
|
-
color: theme.primary,
|
|
2399
|
-
background: "transparent",
|
|
2400
|
-
cursor: "pointer"
|
|
2401
|
-
},
|
|
2402
|
-
children: t("cancel")
|
|
2403
|
-
}
|
|
2404
|
-
)
|
|
2405
|
-
] })
|
|
2406
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2407
3316
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2408
|
-
"
|
|
2409
|
-
{
|
|
2410
|
-
className: "qa-whitespace-pre-wrap qa-break-words",
|
|
2411
|
-
style: { color: theme.ink },
|
|
2412
|
-
children: note.description
|
|
2413
|
-
}
|
|
2414
|
-
),
|
|
2415
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-space-y-1.5 qa-text-11 qa-text-slate-500", children: [
|
|
2416
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
|
|
2417
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
|
|
2418
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
|
|
2419
|
-
] }),
|
|
2420
|
-
note.target && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: note.target })
|
|
2421
|
-
] }),
|
|
2422
|
-
thumbUrl && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2423
|
-
"img",
|
|
3317
|
+
"button",
|
|
2424
3318
|
{
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
style: {
|
|
3319
|
+
onClick: () => setImg(null),
|
|
3320
|
+
className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
|
|
3321
|
+
title: t("remove_image"),
|
|
3322
|
+
style: {
|
|
3323
|
+
top: "-8px",
|
|
3324
|
+
insetInlineEnd: "-8px",
|
|
3325
|
+
border: "none",
|
|
3326
|
+
cursor: "pointer"
|
|
3327
|
+
},
|
|
3328
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 12 })
|
|
2429
3329
|
}
|
|
2430
3330
|
)
|
|
2431
|
-
] })
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3331
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3332
|
+
"button",
|
|
3333
|
+
{
|
|
3334
|
+
onClick: () => fileRef.current?.click(),
|
|
3335
|
+
className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
|
|
3336
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3337
|
+
children: [
|
|
3338
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ImagePlus", size: 16 }),
|
|
3339
|
+
t("image_hint")
|
|
3340
|
+
]
|
|
3341
|
+
}
|
|
3342
|
+
),
|
|
3343
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3344
|
+
"input",
|
|
3345
|
+
{
|
|
3346
|
+
ref: fileRef,
|
|
3347
|
+
type: "file",
|
|
3348
|
+
accept: "image/*",
|
|
3349
|
+
onChange: onFile,
|
|
3350
|
+
className: "qa-hidden"
|
|
3351
|
+
}
|
|
3352
|
+
)
|
|
3353
|
+
] }),
|
|
3354
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-2", children: [
|
|
3355
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3356
|
+
"button",
|
|
3357
|
+
{
|
|
3358
|
+
onClick: save,
|
|
3359
|
+
disabled: !desc.trim(),
|
|
3360
|
+
className: "qa-tap qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1 qa-rounded-lg qa-bg-accent qa-px-3 qa-py-1.5 qa-text-sm qa-font-semibold",
|
|
3361
|
+
style: { border: "none", cursor: "pointer" },
|
|
3362
|
+
children: [
|
|
3363
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
|
|
3364
|
+
t("save")
|
|
3365
|
+
]
|
|
3366
|
+
}
|
|
3367
|
+
),
|
|
3368
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3369
|
+
"button",
|
|
3370
|
+
{
|
|
3371
|
+
onClick: () => setEditing(false),
|
|
3372
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
|
|
3373
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3374
|
+
children: t("cancel")
|
|
3375
|
+
}
|
|
3376
|
+
)
|
|
3377
|
+
] })
|
|
3378
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3379
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-whitespace-pre-wrap qa-break-words qa-text-hi", children: note.description }),
|
|
3380
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-space-y-1.5 qa-text-11 qa-text-lo", children: [
|
|
3381
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
|
|
3382
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
|
|
3383
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
|
|
3384
|
+
] }),
|
|
3385
|
+
note.target && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: note.target }),
|
|
3386
|
+
contextEventCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { children: t("context_attached", { n: contextEventCount }) })
|
|
3387
|
+
] }),
|
|
3388
|
+
thumbUrl && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3389
|
+
"img",
|
|
3390
|
+
{
|
|
3391
|
+
src: thumbUrl,
|
|
3392
|
+
alt: "screenshot",
|
|
3393
|
+
className: "qa-mt-2 qa-w-full qa-rounded-lg qa-border qa-border-subtle"
|
|
3394
|
+
}
|
|
3395
|
+
)
|
|
3396
|
+
] })
|
|
3397
|
+
] });
|
|
2435
3398
|
}
|
|
2436
3399
|
function NoteList() {
|
|
2437
|
-
const { notes,
|
|
3400
|
+
const { notes, notesLoading, t } = useQa();
|
|
3401
|
+
if (notesLoading && !notes.length) {
|
|
3402
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "qa-space-y-2", "aria-hidden": "true", children: [
|
|
3403
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
|
|
3404
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
|
|
3405
|
+
/* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } })
|
|
3406
|
+
] });
|
|
3407
|
+
}
|
|
2438
3408
|
if (!notes.length) {
|
|
2439
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2440
|
-
"
|
|
2441
|
-
{
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
children: [
|
|
2445
|
-
t("no_points"),
|
|
2446
|
-
/* @__PURE__ */ jsxRuntime.jsx("br", {}),
|
|
2447
|
-
t("no_points_hint", { cta: t("capture_cta") })
|
|
2448
|
-
]
|
|
2449
|
-
}
|
|
2450
|
-
);
|
|
3409
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-dashed qa-border-subtle qa-py-8 qa-text-center qa-text-sm qa-text-lo", children: [
|
|
3410
|
+
t("no_points"),
|
|
3411
|
+
/* @__PURE__ */ jsxRuntime.jsx("br", {}),
|
|
3412
|
+
t("no_points_hint", { cta: t("capture_cta") })
|
|
3413
|
+
] });
|
|
2451
3414
|
}
|
|
2452
3415
|
return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-2", children: notes.map((n, i) => /* @__PURE__ */ jsxRuntime.jsx(NoteItem, { note: n, index: notes.length - i }, n.id)) });
|
|
2453
3416
|
}
|
|
@@ -2479,17 +3442,27 @@ function EyeIcon({ open, size = 12, className }) {
|
|
|
2479
3442
|
);
|
|
2480
3443
|
}
|
|
2481
3444
|
var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2482
|
-
function CopyField({
|
|
3445
|
+
function CopyField({
|
|
3446
|
+
value,
|
|
3447
|
+
maskable = false,
|
|
3448
|
+
notify,
|
|
3449
|
+
t
|
|
3450
|
+
}) {
|
|
2483
3451
|
const [done, setDone] = React.useState(false);
|
|
2484
3452
|
const [revealed, setRevealed] = React.useState(true);
|
|
2485
3453
|
const copy = async () => {
|
|
2486
3454
|
if (value === "\u2014") return;
|
|
2487
|
-
if (typeof navigator === "undefined" || !navigator.clipboard)
|
|
3455
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
3456
|
+
notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
2488
3459
|
try {
|
|
2489
3460
|
await navigator.clipboard.writeText(value);
|
|
2490
3461
|
setDone(true);
|
|
2491
3462
|
setTimeout(() => setDone(false), 1100);
|
|
3463
|
+
notify(t("copied"), { tone: "success", id: "credentials-copy" });
|
|
2492
3464
|
} catch {
|
|
3465
|
+
notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
|
|
2493
3466
|
}
|
|
2494
3467
|
};
|
|
2495
3468
|
const hidden = maskable && !revealed && value !== "\u2014";
|
|
@@ -2498,14 +3471,14 @@ function CopyField({ value, ink, maskable = false }) {
|
|
|
2498
3471
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2499
3472
|
"button",
|
|
2500
3473
|
{
|
|
2501
|
-
onClick: copy,
|
|
3474
|
+
onClick: () => void copy(),
|
|
2502
3475
|
disabled: value === "\u2014",
|
|
2503
3476
|
dir: "ltr",
|
|
2504
3477
|
className: "qa-group qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-px-1.5 qa-py-0.5 qa-font-mono qa-text-xs qa-hover-bg-black-5",
|
|
2505
3478
|
style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
|
|
2506
3479
|
children: [
|
|
2507
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", {
|
|
2508
|
-
value !== "\u2014" && (done ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 12, className: "qa-text-
|
|
3480
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-hi", children: displayValue }),
|
|
3481
|
+
value !== "\u2014" && (done ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 12, className: "qa-text-success" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Copy", size: 12, className: "qa-opacity-40 qa-group-hover-opacity-80" }))
|
|
2509
3482
|
]
|
|
2510
3483
|
}
|
|
2511
3484
|
),
|
|
@@ -2524,7 +3497,7 @@ function CopyField({ value, ink, maskable = false }) {
|
|
|
2524
3497
|
] });
|
|
2525
3498
|
}
|
|
2526
3499
|
function CredentialsSection() {
|
|
2527
|
-
const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials,
|
|
3500
|
+
const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, notify } = useQa();
|
|
2528
3501
|
const usedCount = credentials.filter((c) => loginsUsed.has(c.role)).length;
|
|
2529
3502
|
const field = pick2(loginField);
|
|
2530
3503
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2.5", children: [
|
|
@@ -2533,8 +3506,8 @@ function CredentialsSection() {
|
|
|
2533
3506
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2534
3507
|
"span",
|
|
2535
3508
|
{
|
|
2536
|
-
className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium
|
|
2537
|
-
style: { background:
|
|
3509
|
+
className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium",
|
|
3510
|
+
style: { background: "var(--qa-success)", color: "var(--qa-on-accent)" },
|
|
2538
3511
|
children: t("used_count", { n: usedCount, m: credentials.length })
|
|
2539
3512
|
}
|
|
2540
3513
|
)
|
|
@@ -2545,24 +3518,20 @@ function CredentialsSection() {
|
|
|
2545
3518
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2546
3519
|
"div",
|
|
2547
3520
|
{
|
|
2548
|
-
className:
|
|
2549
|
-
style: {
|
|
2550
|
-
borderColor: used ? theme.sage : `${theme.primary}14`,
|
|
2551
|
-
background: used ? `${theme.sage}12` : "#fff"
|
|
2552
|
-
},
|
|
3521
|
+
className: `qa-rounded-xl qa-border qa-p-2.5 qa-elev-1 qa-transition ${used ? "qa-bg-success-tint" : "qa-bg-1"}`,
|
|
3522
|
+
style: { borderColor: used ? "var(--qa-success)" : "var(--qa-border-subtle)" },
|
|
2553
3523
|
children: [
|
|
2554
3524
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
2555
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CircleUser", size: 16, className: "qa-shrink-0"
|
|
2556
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-semibold",
|
|
3525
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CircleUser", size: 16, className: "qa-shrink-0 qa-text-accent" }),
|
|
3526
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-semibold qa-text-hi", children: label }),
|
|
2557
3527
|
c.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-slate-400", children: pick2(c.hint) }),
|
|
2558
3528
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2559
3529
|
"button",
|
|
2560
3530
|
{
|
|
2561
3531
|
onClick: () => toggleLogin(c.role),
|
|
2562
3532
|
disabled: !c.seeded,
|
|
2563
|
-
className:
|
|
3533
|
+
className: `qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-text-xs ${used ? "qa-text-success" : "qa-text-lo"}`,
|
|
2564
3534
|
style: {
|
|
2565
|
-
color: used ? theme.sage : "#94a3b8",
|
|
2566
3535
|
background: "transparent",
|
|
2567
3536
|
border: "none",
|
|
2568
3537
|
cursor: c.seeded ? "pointer" : "default"
|
|
@@ -2575,9 +3544,9 @@ function CredentialsSection() {
|
|
|
2575
3544
|
)
|
|
2576
3545
|
] }),
|
|
2577
3546
|
c.seeded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-flex qa-flex-wrap qa-items-center qa-gap-x-3 qa-gap-y-1 qa-ps-6", children: [
|
|
2578
|
-
/* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.login,
|
|
3547
|
+
/* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.login, notify, t }),
|
|
2579
3548
|
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
|
|
2580
|
-
/* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password,
|
|
3549
|
+
/* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password, notify, t, maskable: true })
|
|
2581
3550
|
] })
|
|
2582
3551
|
]
|
|
2583
3552
|
},
|
|
@@ -2587,193 +3556,192 @@ function CredentialsSection() {
|
|
|
2587
3556
|
] });
|
|
2588
3557
|
}
|
|
2589
3558
|
var keyOf = (id, path) => `${id}::${path}`;
|
|
3559
|
+
var DEFAULT_LANE_COLOR = "#4D9CFF";
|
|
2590
3560
|
function Lane({
|
|
2591
3561
|
group,
|
|
2592
3562
|
checked,
|
|
2593
3563
|
toggle,
|
|
2594
3564
|
pick: pick2
|
|
2595
3565
|
}) {
|
|
2596
|
-
const {
|
|
2597
|
-
const { id, color =
|
|
3566
|
+
const { lang, t, guideFailed, evidenceByStep } = useQa();
|
|
3567
|
+
const { id, color = DEFAULT_LANE_COLOR, steps } = group;
|
|
2598
3568
|
const done = steps.filter((s) => checked.has(keyOf(id, s.path))).length;
|
|
2599
3569
|
const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
|
|
2600
3570
|
const uncoveredRedCount = steps.filter(
|
|
2601
3571
|
(s) => s.risk === "red" && !checked.has(keyOf(id, s.path))
|
|
2602
3572
|
).length;
|
|
2603
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2604
|
-
"div",
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3573
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-3 qa-elev-1", children: [
|
|
3574
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-2 qa-flex qa-items-center qa-gap-2", children: [
|
|
3575
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3576
|
+
"span",
|
|
3577
|
+
{
|
|
3578
|
+
className: "qa-h-2.5 qa-w-2.5 qa-rounded-full",
|
|
3579
|
+
style: { background: color }
|
|
3580
|
+
}
|
|
3581
|
+
),
|
|
3582
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-bold qa-text-hi", children: pick2(group.role) }),
|
|
3583
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-ms-auto qa-text-11 qa-font-medium qa-text-slate-400", children: [
|
|
3584
|
+
done,
|
|
3585
|
+
"/",
|
|
3586
|
+
steps.length
|
|
3587
|
+
] }),
|
|
3588
|
+
uncoveredRedCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3589
|
+
"span",
|
|
3590
|
+
{
|
|
3591
|
+
className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-text-10 qa-font-medium",
|
|
3592
|
+
title: lang === "ar" ? `${uncoveredRedCount} \u0645\u0646\u0637\u0642\u0629 \u062D\u0645\u0631\u0627\u0621 \u063A\u064A\u0631 \u0645\u063A\u0637\u0627\u0629` : `${uncoveredRedCount} uncovered red zone(s)`,
|
|
3593
|
+
children: lang === "ar" ? `\u0623\u062D\u0645\u0631: ${uncoveredRedCount}` : `red: ${uncoveredRedCount}`
|
|
3594
|
+
}
|
|
3595
|
+
)
|
|
3596
|
+
] }),
|
|
3597
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3598
|
+
"div",
|
|
3599
|
+
{
|
|
3600
|
+
className: "qa-mb-3 qa-h-1.5 qa-overflow-hidden qa-rounded-full",
|
|
3601
|
+
style: { background: `${color}22` },
|
|
3602
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2634
3603
|
"div",
|
|
2635
3604
|
{
|
|
2636
|
-
className: "qa-
|
|
2637
|
-
style: {
|
|
2638
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2639
|
-
"div",
|
|
2640
|
-
{
|
|
2641
|
-
className: "qa-h-full qa-rounded-full qa-transition-all",
|
|
2642
|
-
style: { width: `${pct}%`, background: color }
|
|
2643
|
-
}
|
|
2644
|
-
)
|
|
3605
|
+
className: "qa-h-full qa-rounded-full qa-transition-all",
|
|
3606
|
+
style: { width: `${pct}%`, background: color }
|
|
2645
3607
|
}
|
|
2646
|
-
)
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
3608
|
+
)
|
|
3609
|
+
}
|
|
3610
|
+
),
|
|
3611
|
+
/* @__PURE__ */ jsxRuntime.jsxs("ol", { className: "qa-relative qa-ms-1.5", children: [
|
|
3612
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3613
|
+
"span",
|
|
3614
|
+
{
|
|
3615
|
+
className: "qa-absolute qa-top-1 qa-bottom-0 qa-w-px",
|
|
3616
|
+
style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
|
|
3617
|
+
}
|
|
3618
|
+
),
|
|
3619
|
+
steps.map((s, i) => {
|
|
3620
|
+
const k = keyOf(id, s.path);
|
|
3621
|
+
const on = checked.has(k);
|
|
3622
|
+
const failed = guideFailed.has(k);
|
|
3623
|
+
const evidence = evidenceByStep.get(k);
|
|
3624
|
+
const evidenceCount = evidence ? evidence.length : 0;
|
|
3625
|
+
const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
|
|
3626
|
+
const dotTitle = !s.risk ? lang === "ar" ? "\u0644\u0645 \u064A\u062A\u0645 \u062A\u0642\u064A\u064A\u0645 \u0627\u0644\u0645\u062E\u0627\u0637\u0631 \u0628\u0639\u062F" : "not graded yet" : s.riskWhy ?? s.risk;
|
|
3627
|
+
return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-relative qa-mb-2 qa-last-mb-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3628
|
+
"button",
|
|
3629
|
+
{
|
|
3630
|
+
onClick: () => toggle(k),
|
|
3631
|
+
className: `qa-flex qa-w-full qa-items-start qa-gap-2.5 qa-rounded-lg qa-p-1 qa-text-start qa-hover-bg-black-3${failed ? " qa-bg-danger-tint" : ""}`,
|
|
3632
|
+
style: { background: failed ? void 0 : "transparent", border: "none", cursor: "pointer" },
|
|
3633
|
+
children: [
|
|
3634
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3635
|
+
"span",
|
|
3636
|
+
{
|
|
3637
|
+
className: "qa-relative qa-z-1 qa-mt-0.5 qa-flex qa-h-4 qa-w-4 qa-shrink-0 qa-items-center qa-justify-center qa-rounded-full qa-border-2 qa-transition",
|
|
3638
|
+
style: {
|
|
3639
|
+
borderColor: failed ? "var(--qa-danger)" : color,
|
|
3640
|
+
background: on ? color : failed ? "var(--qa-danger-tint)" : "var(--qa-surface-1)",
|
|
3641
|
+
zIndex: 1
|
|
3642
|
+
},
|
|
3643
|
+
children: [
|
|
3644
|
+
on && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" }),
|
|
3645
|
+
!on && failed && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "AlertTriangle", size: 9, strokeWidth: 2.5, className: "qa-text-danger" })
|
|
3646
|
+
]
|
|
3647
|
+
}
|
|
3648
|
+
),
|
|
3649
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-min-w-0", children: [
|
|
3650
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1", children: [
|
|
2667
3651
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2668
|
-
"
|
|
3652
|
+
"code",
|
|
2669
3653
|
{
|
|
2670
|
-
className: "qa-
|
|
3654
|
+
className: "qa-rounded qa-px-1 qa-text-11 qa-font-semibold qa-dir-ltr qa-text-hi",
|
|
2671
3655
|
style: {
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3656
|
+
background: failed ? "var(--qa-danger-tint)" : `${color}14`,
|
|
3657
|
+
textDecoration: on ? "line-through" : "none",
|
|
3658
|
+
opacity: on ? 0.55 : 1
|
|
2675
3659
|
},
|
|
2676
|
-
children:
|
|
3660
|
+
children: s.path
|
|
2677
3661
|
}
|
|
2678
3662
|
),
|
|
2679
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
{
|
|
2711
|
-
className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
|
|
2712
|
-
style: { opacity: on ? 0.5 : 1 },
|
|
2713
|
-
children: pick2(s.what)
|
|
2714
|
-
}
|
|
2715
|
-
)
|
|
2716
|
-
] })
|
|
2717
|
-
]
|
|
2718
|
-
}
|
|
2719
|
-
) }, `${k}-${i}`);
|
|
2720
|
-
})
|
|
2721
|
-
] })
|
|
2722
|
-
]
|
|
2723
|
-
}
|
|
2724
|
-
);
|
|
3663
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3664
|
+
"span",
|
|
3665
|
+
{
|
|
3666
|
+
className: "qa-inline-block qa-rounded-full qa-shrink-0",
|
|
3667
|
+
style: {
|
|
3668
|
+
width: "6px",
|
|
3669
|
+
height: "6px",
|
|
3670
|
+
background: riskColor,
|
|
3671
|
+
flexShrink: 0
|
|
3672
|
+
},
|
|
3673
|
+
title: dotTitle
|
|
3674
|
+
}
|
|
3675
|
+
)
|
|
3676
|
+
] }),
|
|
3677
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3678
|
+
"span",
|
|
3679
|
+
{
|
|
3680
|
+
className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
|
|
3681
|
+
style: { opacity: on ? 0.5 : 1 },
|
|
3682
|
+
children: pick2(s.what)
|
|
3683
|
+
}
|
|
3684
|
+
),
|
|
3685
|
+
evidenceCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-mt-1 qa-inline-flex qa-items-center qa-rounded qa-bg-accent-tint qa-text-accent qa-px-1 qa-text-10 qa-font-medium", children: t("evidence_n", { n: evidenceCount }) }),
|
|
3686
|
+
evidenceCount === 0 && on && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-mt-1 qa-inline-flex qa-items-center qa-rounded qa-bg-warn-tint qa-text-warn qa-px-1 qa-text-10 qa-font-medium", children: t("no_evidence") })
|
|
3687
|
+
] })
|
|
3688
|
+
]
|
|
3689
|
+
}
|
|
3690
|
+
) }, `${k}-${i}`);
|
|
3691
|
+
})
|
|
3692
|
+
] })
|
|
3693
|
+
] });
|
|
2725
3694
|
}
|
|
2726
3695
|
function GuideSection() {
|
|
2727
|
-
const { guideChecked, toggleGuide, t, journey, pick: pick2,
|
|
3696
|
+
const { guideChecked, toggleGuide, t, journey, pick: pick2, lang, startTestAlong } = useQa();
|
|
2728
3697
|
const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
|
|
2729
3698
|
const done = all.filter((k) => guideChecked.has(k)).length;
|
|
2730
3699
|
const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
|
|
2731
3700
|
const coverage = computeCoverage(journey, guideChecked);
|
|
2732
3701
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-3", children: [
|
|
2733
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2734
|
-
"div",
|
|
2735
|
-
|
|
2736
|
-
className: "qa-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
coverage.red.total
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
),
|
|
3702
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
|
|
3703
|
+
coverage.red.total > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
|
|
3704
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-font-bold", children: lang === "ar" ? "\u0623\u062D\u0645\u0631" : "RED" }),
|
|
3705
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr qa-font-semibold qa-text-hi", children: [
|
|
3706
|
+
coverage.red.covered,
|
|
3707
|
+
"/",
|
|
3708
|
+
coverage.red.total,
|
|
3709
|
+
" ",
|
|
3710
|
+
lang === "ar" ? "\u0645\u063A\u0637\u0649" : "covered"
|
|
3711
|
+
] })
|
|
3712
|
+
] }),
|
|
3713
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-text-sm qa-font-semibold qa-text-hi", children: [
|
|
3714
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t("journey_title") }),
|
|
3715
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr", children: [
|
|
3716
|
+
done,
|
|
3717
|
+
"/",
|
|
3718
|
+
all.length,
|
|
3719
|
+
" \xB7 ",
|
|
3720
|
+
pct,
|
|
3721
|
+
"%"
|
|
3722
|
+
] })
|
|
3723
|
+
] }),
|
|
3724
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-h-2 qa-overflow-hidden qa-rounded-full qa-bg-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3725
|
+
"div",
|
|
3726
|
+
{
|
|
3727
|
+
className: "qa-h-full qa-rounded-full qa-bg-accent qa-transition-all",
|
|
3728
|
+
style: { width: `${pct}%` }
|
|
3729
|
+
}
|
|
3730
|
+
) }),
|
|
3731
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-flex qa-justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3732
|
+
"button",
|
|
3733
|
+
{
|
|
3734
|
+
type: "button",
|
|
3735
|
+
onClick: startTestAlong,
|
|
3736
|
+
disabled: all.length === 0,
|
|
3737
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-full qa-bg-accent qa-border-0 qa-cursor-pointer qa-px-3 qa-py-1 qa-text-xs qa-font-semibold qa-focus-ring",
|
|
3738
|
+
children: [
|
|
3739
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Play", size: 13 }),
|
|
3740
|
+
t("start_walkthrough")
|
|
3741
|
+
]
|
|
3742
|
+
}
|
|
3743
|
+
) })
|
|
3744
|
+
] }),
|
|
2777
3745
|
journey.map((g) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2778
3746
|
Lane,
|
|
2779
3747
|
{
|
|
@@ -2831,13 +3799,13 @@ function QaPanel() {
|
|
|
2831
3799
|
notes,
|
|
2832
3800
|
exportZip,
|
|
2833
3801
|
isExporting,
|
|
2834
|
-
|
|
3802
|
+
clearNotes,
|
|
3803
|
+
startCapture,
|
|
2835
3804
|
t,
|
|
2836
3805
|
lang,
|
|
2837
3806
|
setLang,
|
|
2838
3807
|
dir,
|
|
2839
3808
|
brand,
|
|
2840
|
-
theme,
|
|
2841
3809
|
journey,
|
|
2842
3810
|
guideChecked
|
|
2843
3811
|
} = useQa();
|
|
@@ -2943,6 +3911,14 @@ function QaPanel() {
|
|
|
2943
3911
|
React.useEffect(() => {
|
|
2944
3912
|
if (!isOpen) setKeyboardLift(0);
|
|
2945
3913
|
}, [isOpen]);
|
|
3914
|
+
React.useEffect(() => {
|
|
3915
|
+
if (!naming) return void 0;
|
|
3916
|
+
const onKeyDown = (e) => {
|
|
3917
|
+
if (e.key === "Escape") setNaming(false);
|
|
3918
|
+
};
|
|
3919
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3920
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
3921
|
+
}, [naming]);
|
|
2946
3922
|
const keyboardLiftActive = coarse && !isIpadLandscape;
|
|
2947
3923
|
const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
|
|
2948
3924
|
if (phase === "hidden") return null;
|
|
@@ -2964,7 +3940,7 @@ function QaPanel() {
|
|
|
2964
3940
|
"data-qa-overlay": "true",
|
|
2965
3941
|
dir,
|
|
2966
3942
|
onTransitionEnd: handleTransitionEnd,
|
|
2967
|
-
className: `qa-fixed qa-flex qa-flex-col qa-overflow-hidden qa-rounded-2xl qa-border qa-
|
|
3943
|
+
className: `qa-fixed qa-flex qa-flex-col qa-overflow-hidden qa-rounded-2xl qa-border qa-border-subtle qa-elev-3 qa-print-hidden qa-w-panel qa-max-h-74vh qa-bg-1 qa-panel-anim${showIn ? " qa-panel-in" : ""}`,
|
|
2968
3944
|
style: {
|
|
2969
3945
|
// Floating popover position (default). Fully overridden below when
|
|
2970
3946
|
// docked as an iPad-landscape side-sheet.
|
|
@@ -2978,10 +3954,7 @@ function QaPanel() {
|
|
|
2978
3954
|
// full height — neutralize it only in the docked sheet variant.
|
|
2979
3955
|
maxHeight: isIpadLandscape ? "none" : void 0,
|
|
2980
3956
|
borderRadius: isIpadLandscape ? 0 : void 0,
|
|
2981
|
-
|
|
2982
|
-
borderColor: `${theme.primary}22`,
|
|
2983
|
-
fontFamily: lang === "ar" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif",
|
|
2984
|
-
zIndex: 9990,
|
|
3957
|
+
zIndex: "var(--qa-z-panel)",
|
|
2985
3958
|
// Keyboard-avoidance lift (coarse/touch only — see effect above).
|
|
2986
3959
|
// undefined ⇒ !keyboardLiftActive, so desktop and the iPad-landscape
|
|
2987
3960
|
// side-sheet render this property exactly as before (the class's own
|
|
@@ -2989,156 +3962,141 @@ function QaPanel() {
|
|
|
2989
3962
|
transition: keyboardLiftActive ? PANEL_TRANSITION_WITH_LIFT : void 0
|
|
2990
3963
|
},
|
|
2991
3964
|
children: [
|
|
2992
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2993
|
-
"
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
"
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
className: "qa-ms-auto qa-flex qa-items-center qa-overflow-hidden qa-rounded-lg qa-text-11 qa-font-semibold",
|
|
3012
|
-
dir: "ltr",
|
|
3013
|
-
style: { background: "rgba(255,255,255,0.15)" },
|
|
3014
|
-
children: ["en", "ar"].map((l) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3015
|
-
"button",
|
|
3016
|
-
{
|
|
3017
|
-
onClick: () => setLang(l),
|
|
3018
|
-
className: "qa-px-2 qa-py-1 qa-transition qa-tap",
|
|
3019
|
-
style: {
|
|
3020
|
-
background: lang === l ? "#ffffff" : "transparent",
|
|
3021
|
-
color: lang === l ? theme.primary : "#fff",
|
|
3022
|
-
border: "none",
|
|
3023
|
-
cursor: "pointer"
|
|
3024
|
-
},
|
|
3025
|
-
children: l === "en" ? "EN" : "\u0639"
|
|
3026
|
-
},
|
|
3027
|
-
l
|
|
3028
|
-
))
|
|
3029
|
-
}
|
|
3030
|
-
),
|
|
3031
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3965
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-px-4 qa-py-3 qa-bg-1", children: [
|
|
3966
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5 qa-dir-ltr", dir: "ltr", children: [
|
|
3967
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3968
|
+
"span",
|
|
3969
|
+
{
|
|
3970
|
+
"aria-hidden": "true",
|
|
3971
|
+
className: "qa-shrink-0",
|
|
3972
|
+
style: { width: 6, height: 6, background: "var(--qa-accent)" }
|
|
3973
|
+
}
|
|
3974
|
+
),
|
|
3975
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-hi", style: { fontSize: 13, fontWeight: 600 }, children: brand.label })
|
|
3976
|
+
] }),
|
|
3977
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-rounded-full qa-bg-3 qa-text-mid qa-px-2 qa-text-xs qa-font-medium", children: notes.length }),
|
|
3978
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3979
|
+
"div",
|
|
3980
|
+
{
|
|
3981
|
+
className: "qa-ms-auto qa-flex qa-items-center qa-overflow-hidden qa-rounded-lg qa-text-11 qa-font-semibold qa-bg-2",
|
|
3982
|
+
dir: "ltr",
|
|
3983
|
+
children: ["en", "ar"].map((l) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3032
3984
|
"button",
|
|
3033
3985
|
{
|
|
3034
|
-
onClick:
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3044
|
-
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
)
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px", style: { background: `${theme.primary}14` } }),
|
|
3066
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3067
|
-
"div",
|
|
3068
|
-
{
|
|
3069
|
-
className: "qa-flex-1 qa-space-y-3 qa-overflow-y-auto qa-p-3",
|
|
3070
|
-
style: { background: `${theme.cream}80` },
|
|
3071
|
-
children: [
|
|
3072
|
-
activeTab === "notes" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3073
|
-
/* @__PURE__ */ jsxRuntime.jsx(NoteEditor, {}),
|
|
3074
|
-
/* @__PURE__ */ jsxRuntime.jsx(NoteList, {}),
|
|
3075
|
-
notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-pt-1 qa-text-center", children: confirmClear ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-xs qa-text-slate-500", children: [
|
|
3076
|
-
t("delete_all_q", { n: notes.length }),
|
|
3077
|
-
" ",
|
|
3078
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3079
|
-
"button",
|
|
3080
|
-
{
|
|
3081
|
-
onClick: () => {
|
|
3082
|
-
void clearAll();
|
|
3083
|
-
setConfirmClear(false);
|
|
3084
|
-
},
|
|
3085
|
-
className: "qa-font-semibold qa-text-red-600 qa-tap",
|
|
3086
|
-
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3087
|
-
children: t("yes")
|
|
3088
|
-
}
|
|
3089
|
-
),
|
|
3090
|
-
" / ",
|
|
3091
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3092
|
-
"button",
|
|
3093
|
-
{
|
|
3094
|
-
onClick: () => setConfirmClear(false),
|
|
3095
|
-
className: "qa-tap",
|
|
3096
|
-
style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
|
|
3097
|
-
children: t("no")
|
|
3098
|
-
}
|
|
3099
|
-
)
|
|
3100
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3101
|
-
"button",
|
|
3986
|
+
onClick: () => setLang(l),
|
|
3987
|
+
className: `qa-px-2 qa-py-1 qa-transition qa-tap ${lang === l ? "qa-bg-accent" : "qa-bg-transparent qa-text-mid"}`,
|
|
3988
|
+
style: { border: "none", cursor: "pointer" },
|
|
3989
|
+
children: l === "en" ? "EN" : "\u0639"
|
|
3990
|
+
},
|
|
3991
|
+
l
|
|
3992
|
+
))
|
|
3993
|
+
}
|
|
3994
|
+
),
|
|
3995
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3996
|
+
"button",
|
|
3997
|
+
{
|
|
3998
|
+
onClick: startCapture,
|
|
3999
|
+
title: t("capture_cta"),
|
|
4000
|
+
"aria-label": t("capture_cta"),
|
|
4001
|
+
className: "qa-tap-icon qa-rounded-lg qa-border qa-border-subtle qa-bg-transparent qa-text-hi qa-hover-bg-2 qa-transition",
|
|
4002
|
+
style: { cursor: "pointer" },
|
|
4003
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 16 })
|
|
4004
|
+
}
|
|
4005
|
+
),
|
|
4006
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4007
|
+
"button",
|
|
4008
|
+
{
|
|
4009
|
+
onClick: openNaming,
|
|
4010
|
+
disabled: !notes.length || isExporting,
|
|
4011
|
+
title: t("export"),
|
|
4012
|
+
className: "qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-lg qa-border qa-border-subtle qa-bg-transparent qa-px-2.5 qa-py-1.5 qa-text-xs qa-font-medium qa-text-hi qa-hover-bg-2 qa-transition qa-tap",
|
|
4013
|
+
style: { cursor: "pointer" },
|
|
4014
|
+
children: [
|
|
4015
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4016
|
+
Icon,
|
|
3102
4017
|
{
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
children: [
|
|
3107
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash", size: 12 }),
|
|
3108
|
-
t("clear_all")
|
|
3109
|
-
]
|
|
4018
|
+
name: isExporting ? "Loader2" : "Download",
|
|
4019
|
+
size: 14,
|
|
4020
|
+
className: isExporting ? "qa-animate-spin" : void 0
|
|
3110
4021
|
}
|
|
3111
|
-
)
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
4022
|
+
),
|
|
4023
|
+
t("export")
|
|
4024
|
+
]
|
|
4025
|
+
}
|
|
4026
|
+
)
|
|
4027
|
+
] }),
|
|
4028
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4029
|
+
TabsBar,
|
|
4030
|
+
{
|
|
4031
|
+
activeTab,
|
|
4032
|
+
setActiveTab,
|
|
4033
|
+
t,
|
|
4034
|
+
lang
|
|
3116
4035
|
}
|
|
3117
4036
|
),
|
|
4037
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3" }),
|
|
4038
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex-1 qa-space-y-3 qa-overflow-y-auto qa-p-3 qa-bg-0", children: [
|
|
4039
|
+
activeTab === "notes" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4040
|
+
/* @__PURE__ */ jsxRuntime.jsx(NoteEditor, {}),
|
|
4041
|
+
/* @__PURE__ */ jsxRuntime.jsx(NoteList, {}),
|
|
4042
|
+
notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-pt-1 qa-text-center", children: confirmClear ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-xs qa-text-mid", children: [
|
|
4043
|
+
t("delete_all_q", { n: notes.length }),
|
|
4044
|
+
" ",
|
|
4045
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4046
|
+
"button",
|
|
4047
|
+
{
|
|
4048
|
+
onClick: () => {
|
|
4049
|
+
void clearNotes();
|
|
4050
|
+
setConfirmClear(false);
|
|
4051
|
+
},
|
|
4052
|
+
className: "qa-font-semibold qa-text-danger qa-tap",
|
|
4053
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4054
|
+
children: t("yes")
|
|
4055
|
+
}
|
|
4056
|
+
),
|
|
4057
|
+
" / ",
|
|
4058
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4059
|
+
"button",
|
|
4060
|
+
{
|
|
4061
|
+
onClick: () => setConfirmClear(false),
|
|
4062
|
+
className: "qa-text-accent qa-tap",
|
|
4063
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4064
|
+
children: t("no")
|
|
4065
|
+
}
|
|
4066
|
+
)
|
|
4067
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4068
|
+
"button",
|
|
4069
|
+
{
|
|
4070
|
+
onClick: () => setConfirmClear(true),
|
|
4071
|
+
className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-xs qa-text-lo qa-hover-text-red",
|
|
4072
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4073
|
+
children: [
|
|
4074
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash", size: 12 }),
|
|
4075
|
+
t("clear_all")
|
|
4076
|
+
]
|
|
4077
|
+
}
|
|
4078
|
+
) })
|
|
4079
|
+
] }),
|
|
4080
|
+
activeTab === "logins" && /* @__PURE__ */ jsxRuntime.jsx(CredentialsSection, {}),
|
|
4081
|
+
activeTab === "guide" && /* @__PURE__ */ jsxRuntime.jsx(GuideSection, {})
|
|
4082
|
+
] }),
|
|
3118
4083
|
naming && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3119
4084
|
"div",
|
|
3120
4085
|
{
|
|
3121
4086
|
className: "qa-absolute qa-inset-0 qa-z-50 qa-flex qa-items-center qa-justify-center qa-p-5",
|
|
3122
|
-
style: { background: "
|
|
4087
|
+
style: { background: "var(--qa-scrim-dialog)" },
|
|
4088
|
+
onClick: () => setNaming(false),
|
|
3123
4089
|
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3124
4090
|
"div",
|
|
3125
4091
|
{
|
|
3126
|
-
className: "qa-w-full qa-rounded-xl qa-border qa-bg-
|
|
3127
|
-
|
|
4092
|
+
className: "qa-w-full qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-4 qa-elev-3",
|
|
4093
|
+
onClick: (e) => e.stopPropagation(),
|
|
3128
4094
|
children: [
|
|
3129
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3130
|
-
"p",
|
|
3131
|
-
{
|
|
3132
|
-
className: "qa-mb-2 qa-text-sm qa-font-semibold",
|
|
3133
|
-
style: { color: theme.ink },
|
|
3134
|
-
children: t("export_name_title")
|
|
3135
|
-
}
|
|
3136
|
-
),
|
|
4095
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mb-2 qa-text-sm qa-font-semibold qa-text-hi", children: t("export_name_title") }),
|
|
3137
4096
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3138
4097
|
"div",
|
|
3139
4098
|
{
|
|
3140
|
-
className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-dir-ltr",
|
|
3141
|
-
style: { borderColor: `${theme.primary}33` },
|
|
4099
|
+
className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-border-subtle qa-dir-ltr",
|
|
3142
4100
|
children: [
|
|
3143
4101
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3144
4102
|
"input",
|
|
@@ -3148,32 +4106,24 @@ function QaPanel() {
|
|
|
3148
4106
|
onChange: (e) => setFilename(e.target.value),
|
|
3149
4107
|
onKeyDown: (e) => {
|
|
3150
4108
|
if (e.key === "Enter") doExport();
|
|
3151
|
-
if (e.key === "Escape") setNaming(false);
|
|
3152
4109
|
},
|
|
3153
4110
|
placeholder: t("export_name_placeholder"),
|
|
3154
4111
|
className: "qa-min-w-0 qa-flex-1 qa-rounded-lg qa-px-2 qa-py-1.5 qa-text-sm qa-border-0",
|
|
3155
4112
|
style: { outline: "none", background: "transparent", color: "inherit" }
|
|
3156
4113
|
}
|
|
3157
4114
|
),
|
|
3158
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-px-2 qa-text-xs qa-text-
|
|
4115
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-px-2 qa-text-xs qa-text-lo", children: ".zip" })
|
|
3159
4116
|
]
|
|
3160
4117
|
}
|
|
3161
4118
|
),
|
|
3162
|
-
namingCoverage && namingCoverage.uncoveredReds.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3163
|
-
"p",
|
|
3164
|
-
{
|
|
3165
|
-
className: "qa-mt-2 qa-text-11",
|
|
3166
|
-
style: { color: "#F59E0B" },
|
|
3167
|
-
children: lang === "ar" ? `\u26A0 ${namingCoverage.uncoveredReds.length} \u0645\u0646\u0637\u0642\u0629/\u0645\u0646\u0627\u0637\u0642 \u062D\u0645\u0631\u0627\u0621 \u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646\u0647\u0627 \u2014 \u062A\u0635\u062F\u064A\u0631 \u0639\u0644\u0649 \u0623\u064A \u062D\u0627\u0644\u061F` : `\u26A0 ${namingCoverage.uncoveredReds.length} red zone(s) not yet verified \u2014 export anyway?`
|
|
3168
|
-
}
|
|
3169
|
-
),
|
|
4119
|
+
namingCoverage && namingCoverage.uncoveredReds.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mt-2 qa-text-11 qa-text-warn", children: lang === "ar" ? `\u26A0 ${namingCoverage.uncoveredReds.length} \u0645\u0646\u0637\u0642\u0629/\u0645\u0646\u0627\u0637\u0642 \u062D\u0645\u0631\u0627\u0621 \u0644\u0645 \u064A\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642 \u0645\u0646\u0647\u0627 \u2014 \u062A\u0635\u062F\u064A\u0631 \u0639\u0644\u0649 \u0623\u064A \u062D\u0627\u0644\u061F` : `\u26A0 ${namingCoverage.uncoveredReds.length} red zone(s) not yet verified \u2014 export anyway?` }),
|
|
3170
4120
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-3 qa-flex qa-gap-2", children: [
|
|
3171
4121
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3172
4122
|
"button",
|
|
3173
4123
|
{
|
|
3174
4124
|
onClick: doExport,
|
|
3175
|
-
className: "qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-
|
|
3176
|
-
style: {
|
|
4125
|
+
className: "qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-bg-accent qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-tap",
|
|
4126
|
+
style: { border: "none", cursor: "pointer" },
|
|
3177
4127
|
children: [
|
|
3178
4128
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
|
|
3179
4129
|
t("export")
|
|
@@ -3184,13 +4134,8 @@ function QaPanel() {
|
|
|
3184
4134
|
"button",
|
|
3185
4135
|
{
|
|
3186
4136
|
onClick: () => setNaming(false),
|
|
3187
|
-
className: "qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-border qa-px-3 qa-py-2 qa-text-sm qa-tap",
|
|
3188
|
-
style: {
|
|
3189
|
-
borderColor: `${theme.primary}33`,
|
|
3190
|
-
color: theme.primary,
|
|
3191
|
-
background: "transparent",
|
|
3192
|
-
cursor: "pointer"
|
|
3193
|
-
},
|
|
4137
|
+
className: "qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-hi qa-tap",
|
|
4138
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3194
4139
|
children: [
|
|
3195
4140
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 16 }),
|
|
3196
4141
|
t("cancel")
|
|
@@ -3211,7 +4156,6 @@ function TabsBar({
|
|
|
3211
4156
|
activeTab,
|
|
3212
4157
|
setActiveTab,
|
|
3213
4158
|
t,
|
|
3214
|
-
theme,
|
|
3215
4159
|
lang
|
|
3216
4160
|
}) {
|
|
3217
4161
|
const tabRefs = React.useRef([]);
|
|
@@ -3235,7 +4179,7 @@ function TabsBar({
|
|
|
3235
4179
|
ro.observe(container);
|
|
3236
4180
|
return () => ro.disconnect();
|
|
3237
4181
|
}, [reposition]);
|
|
3238
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative", children: [
|
|
4182
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative qa-bg-1", children: [
|
|
3239
4183
|
TABS.map((tab, i) => {
|
|
3240
4184
|
const on = activeTab === tab.key;
|
|
3241
4185
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -3245,13 +4189,8 @@ function TabsBar({
|
|
|
3245
4189
|
tabRefs.current[i] = el;
|
|
3246
4190
|
},
|
|
3247
4191
|
onClick: () => setActiveTab(tab.key),
|
|
3248
|
-
className:
|
|
3249
|
-
style: {
|
|
3250
|
-
color: on ? theme.primary : "#94a3b8",
|
|
3251
|
-
background: "transparent",
|
|
3252
|
-
border: "none",
|
|
3253
|
-
cursor: "pointer"
|
|
3254
|
-
},
|
|
4192
|
+
className: `qa-relative qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-py-2 qa-text-sm qa-font-medium qa-transition qa-tap ${on ? "qa-text-accent" : "qa-text-mid"}`,
|
|
4193
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3255
4194
|
children: [
|
|
3256
4195
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: tab.icon, size: 16 }),
|
|
3257
4196
|
t(tab.labelKey)
|
|
@@ -3265,20 +4204,265 @@ function TabsBar({
|
|
|
3265
4204
|
{
|
|
3266
4205
|
ref: barRef,
|
|
3267
4206
|
className: "qa-tab-indicator",
|
|
3268
|
-
style: { background:
|
|
4207
|
+
style: { background: "var(--qa-accent)" },
|
|
3269
4208
|
"aria-hidden": "true"
|
|
3270
4209
|
}
|
|
3271
4210
|
)
|
|
3272
4211
|
] });
|
|
3273
4212
|
}
|
|
4213
|
+
function toneIcon(tone) {
|
|
4214
|
+
switch (tone) {
|
|
4215
|
+
case "success":
|
|
4216
|
+
return { name: "Check", colorClass: "qa-text-success" };
|
|
4217
|
+
case "error":
|
|
4218
|
+
return { name: "AlertTriangle", colorClass: "qa-text-danger" };
|
|
4219
|
+
default:
|
|
4220
|
+
return { name: "X", colorClass: "qa-text-accent" };
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
function Toast({ notice }) {
|
|
4224
|
+
const { dismissNotice } = useQa();
|
|
4225
|
+
const icon = toneIcon(notice.tone);
|
|
4226
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4227
|
+
"div",
|
|
4228
|
+
{
|
|
4229
|
+
className: "qa-toast qa-toast-in qa-bg-2 qa-border qa-border-subtle qa-elev-2 qa-text-hi qa-flex qa-items-center qa-gap-2 qa-rounded-md qa-px-3 qa-py-2",
|
|
4230
|
+
style: { fontSize: 13 },
|
|
4231
|
+
children: [
|
|
4232
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon.name, size: 16, className: `qa-shrink-0 ${icon.colorClass}` }),
|
|
4233
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-min-w-0 qa-flex-1", children: notice.message }),
|
|
4234
|
+
notice.action && /* @__PURE__ */ jsxRuntime.jsx(
|
|
4235
|
+
"button",
|
|
4236
|
+
{
|
|
4237
|
+
type: "button",
|
|
4238
|
+
onClick: () => {
|
|
4239
|
+
notice.action?.onAction();
|
|
4240
|
+
dismissNotice(notice.id);
|
|
4241
|
+
},
|
|
4242
|
+
className: "qa-tap qa-text-accent qa-focus-ring qa-shrink-0 qa-rounded qa-px-2 qa-py-1 qa-font-semibold",
|
|
4243
|
+
style: { background: "transparent", border: "none", cursor: "pointer", fontSize: 13, pointerEvents: "auto" },
|
|
4244
|
+
children: notice.action.label
|
|
4245
|
+
}
|
|
4246
|
+
)
|
|
4247
|
+
]
|
|
4248
|
+
}
|
|
4249
|
+
);
|
|
4250
|
+
}
|
|
4251
|
+
function NoticeHost() {
|
|
4252
|
+
const { notices, dir } = useQa();
|
|
4253
|
+
const politeNotices = notices.filter((n) => n.tone !== "error");
|
|
4254
|
+
const errorNotices = notices.filter((n) => n.tone === "error");
|
|
4255
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4256
|
+
"div",
|
|
4257
|
+
{
|
|
4258
|
+
"data-qa-overlay": "true",
|
|
4259
|
+
dir,
|
|
4260
|
+
className: "qa-toast-viewport qa-print-hidden qa-fixed",
|
|
4261
|
+
style: { zIndex: "var(--qa-z-toast)", pointerEvents: "none" },
|
|
4262
|
+
children: [
|
|
4263
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "aria-live": "polite", className: "qa-flex qa-flex-col qa-items-center qa-gap-2", children: politeNotices.map((n) => /* @__PURE__ */ jsxRuntime.jsx(Toast, { notice: n }, n.id)) }),
|
|
4264
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { "aria-live": "assertive", className: "qa-flex qa-flex-col qa-items-center qa-gap-2", children: errorNotices.map((n) => /* @__PURE__ */ jsxRuntime.jsx(Toast, { notice: n }, n.id)) })
|
|
4265
|
+
]
|
|
4266
|
+
}
|
|
4267
|
+
);
|
|
4268
|
+
}
|
|
4269
|
+
function TestAlongHud() {
|
|
4270
|
+
const {
|
|
4271
|
+
dir,
|
|
4272
|
+
t,
|
|
4273
|
+
pick: pick2,
|
|
4274
|
+
testAlong,
|
|
4275
|
+
testAlongSteps,
|
|
4276
|
+
gotoStep,
|
|
4277
|
+
gradeStep,
|
|
4278
|
+
exitTestAlong,
|
|
4279
|
+
startCapture,
|
|
4280
|
+
evidenceByStep
|
|
4281
|
+
} = useQa();
|
|
4282
|
+
const index = testAlong.index;
|
|
4283
|
+
const steps = testAlongSteps;
|
|
4284
|
+
const currentStep = steps[index];
|
|
4285
|
+
const backIcon = dir === "rtl" ? "ChevronRight" : "ChevronLeft";
|
|
4286
|
+
const nextIcon = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
|
|
4287
|
+
React.useEffect(() => {
|
|
4288
|
+
const step = steps[index];
|
|
4289
|
+
if (!step) return;
|
|
4290
|
+
const notesForStep = evidenceByStep.get(step.key);
|
|
4291
|
+
if (!notesForStep || notesForStep.length === 0) return;
|
|
4292
|
+
const latest = notesForStep[notesForStep.length - 1];
|
|
4293
|
+
if (latest.target) flashLocate(latest.target);
|
|
4294
|
+
}, [testAlong.index]);
|
|
4295
|
+
if (!testAlong.active) return null;
|
|
4296
|
+
const atFirst = index <= 0;
|
|
4297
|
+
const atLast = index >= steps.length - 1;
|
|
4298
|
+
const riskColor = RISK_COLORS[currentStep?.risk ?? "green"];
|
|
4299
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4300
|
+
"div",
|
|
4301
|
+
{
|
|
4302
|
+
"data-qa-overlay": "true",
|
|
4303
|
+
dir,
|
|
4304
|
+
className: "qa-fixed qa-print-hidden",
|
|
4305
|
+
style: {
|
|
4306
|
+
left: "env(safe-area-inset-left)",
|
|
4307
|
+
right: "env(safe-area-inset-right)",
|
|
4308
|
+
bottom: "env(safe-area-inset-bottom)",
|
|
4309
|
+
zIndex: "var(--qa-z-panel)",
|
|
4310
|
+
padding: "0.75rem",
|
|
4311
|
+
pointerEvents: "none"
|
|
4312
|
+
},
|
|
4313
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4314
|
+
"div",
|
|
4315
|
+
{
|
|
4316
|
+
role: "region",
|
|
4317
|
+
"aria-label": t("journey_title"),
|
|
4318
|
+
className: "qa-flex qa-flex-col qa-gap-2 qa-bg-1 qa-border qa-border-subtle qa-elev-3 qa-rounded-xl qa-p-3",
|
|
4319
|
+
style: { maxWidth: "32rem", marginInline: "auto", pointerEvents: "auto" },
|
|
4320
|
+
children: [
|
|
4321
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
4322
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4323
|
+
"span",
|
|
4324
|
+
{
|
|
4325
|
+
"aria-hidden": "true",
|
|
4326
|
+
className: "qa-shrink-0 qa-rounded-full",
|
|
4327
|
+
style: { width: 8, height: 8, backgroundColor: riskColor }
|
|
4328
|
+
}
|
|
4329
|
+
),
|
|
4330
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-mid", children: t("step_of", { n: index + 1, m: steps.length }) }),
|
|
4331
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4332
|
+
"button",
|
|
4333
|
+
{
|
|
4334
|
+
type: "button",
|
|
4335
|
+
onClick: exitTestAlong,
|
|
4336
|
+
className: "qa-tap qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-px-2 qa-py-1 qa-text-11 qa-font-medium qa-text-lo qa-hover-bg-2 qa-focus-ring qa-transition",
|
|
4337
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4338
|
+
children: [
|
|
4339
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 14 }),
|
|
4340
|
+
t("exit_walkthrough")
|
|
4341
|
+
]
|
|
4342
|
+
}
|
|
4343
|
+
)
|
|
4344
|
+
] }),
|
|
4345
|
+
currentStep && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-min-w-0", children: [
|
|
4346
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-text-sm qa-font-medium qa-text-hi qa-break-words", children: pick2(currentStep.what) }),
|
|
4347
|
+
currentStep.expect && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-11 qa-text-mid qa-mt-1 qa-break-words", children: [
|
|
4348
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-font-semibold", children: [
|
|
4349
|
+
t("expected_label"),
|
|
4350
|
+
": "
|
|
4351
|
+
] }),
|
|
4352
|
+
pick2(currentStep.expect)
|
|
4353
|
+
] })
|
|
4354
|
+
] }),
|
|
4355
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-items-center qa-gap-2", children: [
|
|
4356
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4357
|
+
"button",
|
|
4358
|
+
{
|
|
4359
|
+
type: "button",
|
|
4360
|
+
onClick: () => gotoStep(index - 1),
|
|
4361
|
+
disabled: atFirst,
|
|
4362
|
+
className: "qa-tap qa-shrink-0 qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-xs qa-font-medium qa-text-hi qa-hover-bg-2 qa-focus-ring qa-transition",
|
|
4363
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4364
|
+
children: [
|
|
4365
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: backIcon, size: 14 }),
|
|
4366
|
+
t("prev_step")
|
|
4367
|
+
]
|
|
4368
|
+
}
|
|
4369
|
+
),
|
|
4370
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4371
|
+
"button",
|
|
4372
|
+
{
|
|
4373
|
+
type: "button",
|
|
4374
|
+
onClick: () => currentStep && gradeStep(currentStep.key, "fail"),
|
|
4375
|
+
disabled: !currentStep,
|
|
4376
|
+
className: "qa-tap qa-flex-1 qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-bg-danger-tint qa-text-danger qa-text-xs qa-font-semibold qa-focus-ring qa-transition",
|
|
4377
|
+
style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
|
|
4378
|
+
children: [
|
|
4379
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "AlertTriangle", size: 14 }),
|
|
4380
|
+
t("mark_fail")
|
|
4381
|
+
]
|
|
4382
|
+
}
|
|
4383
|
+
),
|
|
4384
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4385
|
+
"button",
|
|
4386
|
+
{
|
|
4387
|
+
type: "button",
|
|
4388
|
+
onClick: () => currentStep && gradeStep(currentStep.key, "pass"),
|
|
4389
|
+
disabled: !currentStep,
|
|
4390
|
+
className: "qa-tap qa-flex-1 qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-bg-success-tint qa-text-success qa-text-xs qa-font-semibold qa-focus-ring qa-transition",
|
|
4391
|
+
style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
|
|
4392
|
+
children: [
|
|
4393
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 14 }),
|
|
4394
|
+
t("mark_pass")
|
|
4395
|
+
]
|
|
4396
|
+
}
|
|
4397
|
+
),
|
|
4398
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4399
|
+
"button",
|
|
4400
|
+
{
|
|
4401
|
+
type: "button",
|
|
4402
|
+
onClick: () => startCapture(),
|
|
4403
|
+
className: "qa-tap qa-flex-1 qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-bg-accent qa-text-xs qa-font-semibold qa-focus-ring qa-transition",
|
|
4404
|
+
style: { border: "none", cursor: "pointer", minWidth: "6rem" },
|
|
4405
|
+
children: [
|
|
4406
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 14 }),
|
|
4407
|
+
t("capture_here")
|
|
4408
|
+
]
|
|
4409
|
+
}
|
|
4410
|
+
),
|
|
4411
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4412
|
+
"button",
|
|
4413
|
+
{
|
|
4414
|
+
type: "button",
|
|
4415
|
+
onClick: () => gotoStep(index + 1),
|
|
4416
|
+
disabled: atLast,
|
|
4417
|
+
className: "qa-tap qa-shrink-0 qa-inline-flex qa-items-center qa-gap-1 qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-xs qa-font-medium qa-text-hi qa-hover-bg-2 qa-focus-ring qa-transition",
|
|
4418
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4419
|
+
children: [
|
|
4420
|
+
t("next_step"),
|
|
4421
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: nextIcon, size: 14 })
|
|
4422
|
+
]
|
|
4423
|
+
}
|
|
4424
|
+
)
|
|
4425
|
+
] })
|
|
4426
|
+
]
|
|
4427
|
+
}
|
|
4428
|
+
)
|
|
4429
|
+
}
|
|
4430
|
+
);
|
|
4431
|
+
}
|
|
3274
4432
|
|
|
3275
4433
|
// src/lib/capture.ts
|
|
3276
4434
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
4435
|
+
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
3277
4436
|
function withTimeout(promise, ms) {
|
|
4437
|
+
let timer;
|
|
3278
4438
|
return Promise.race([
|
|
3279
4439
|
promise,
|
|
3280
|
-
new Promise((resolve) =>
|
|
3281
|
-
|
|
4440
|
+
new Promise((resolve) => {
|
|
4441
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
4442
|
+
})
|
|
4443
|
+
]).finally(() => {
|
|
4444
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4445
|
+
});
|
|
4446
|
+
}
|
|
4447
|
+
function isTransparent(color) {
|
|
4448
|
+
const c = (color || "").trim().toLowerCase();
|
|
4449
|
+
if (!c || c === "transparent") return true;
|
|
4450
|
+
const m = c.match(/^rgba?\(([^)]+)\)$/);
|
|
4451
|
+
if (!m) return false;
|
|
4452
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean);
|
|
4453
|
+
return parts.length >= 4 && parseFloat(parts[3]) === 0;
|
|
4454
|
+
}
|
|
4455
|
+
function resolvePageBackground() {
|
|
4456
|
+
if (typeof getComputedStyle !== "function") return FALLBACK_PAGE_BACKGROUND;
|
|
4457
|
+
for (const el of [document.body, document.documentElement]) {
|
|
4458
|
+
if (!el) continue;
|
|
4459
|
+
try {
|
|
4460
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
4461
|
+
if (!isTransparent(bg)) return bg;
|
|
4462
|
+
} catch {
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
return FALLBACK_PAGE_BACKGROUND;
|
|
3282
4466
|
}
|
|
3283
4467
|
function toBlob(canvas) {
|
|
3284
4468
|
return new Promise((resolve) => {
|
|
@@ -3294,8 +4478,10 @@ function toBlob(canvas) {
|
|
|
3294
4478
|
});
|
|
3295
4479
|
}
|
|
3296
4480
|
async function captureRegion(rect, scroll) {
|
|
3297
|
-
if (typeof document === "undefined" || typeof window === "undefined")
|
|
3298
|
-
|
|
4481
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
4482
|
+
return { status: "empty" };
|
|
4483
|
+
}
|
|
4484
|
+
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3299
4485
|
const sx = scroll?.x ?? window.scrollX;
|
|
3300
4486
|
const sy = scroll?.y ?? window.scrollY;
|
|
3301
4487
|
try {
|
|
@@ -3310,7 +4496,8 @@ async function captureRegion(rect, scroll) {
|
|
|
3310
4496
|
scale,
|
|
3311
4497
|
useCORS: true,
|
|
3312
4498
|
allowTaint: true,
|
|
3313
|
-
|
|
4499
|
+
// The page's own background, never null — see resolvePageBackground().
|
|
4500
|
+
backgroundColor: resolvePageBackground(),
|
|
3314
4501
|
logging: false,
|
|
3315
4502
|
scrollX: sx,
|
|
3316
4503
|
scrollY: sy,
|
|
@@ -3322,11 +4509,12 @@ async function captureRegion(rect, scroll) {
|
|
|
3322
4509
|
}),
|
|
3323
4510
|
HTML2CANVAS_TIMEOUT_MS
|
|
3324
4511
|
);
|
|
3325
|
-
if (!canvas) return
|
|
3326
|
-
|
|
4512
|
+
if (!canvas) return { status: "failed" };
|
|
4513
|
+
const blob = await toBlob(canvas);
|
|
4514
|
+
return blob ? { status: "ok", blob } : { status: "failed" };
|
|
3327
4515
|
} catch (err) {
|
|
3328
4516
|
console.warn("[QA] region capture failed:", err);
|
|
3329
|
-
return
|
|
4517
|
+
return { status: "failed" };
|
|
3330
4518
|
}
|
|
3331
4519
|
}
|
|
3332
4520
|
|
|
@@ -3430,6 +4618,26 @@ function unlockPageScroll() {
|
|
|
3430
4618
|
var DRAG_THRESHOLD2 = 6;
|
|
3431
4619
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
3432
4620
|
var MIN_REGION_SIZE = 8;
|
|
4621
|
+
var SEVERITIES2 = ["bug", "question", "polish"];
|
|
4622
|
+
var SEVERITY_ICON = {
|
|
4623
|
+
bug: "Bug",
|
|
4624
|
+
question: "AlertTriangle",
|
|
4625
|
+
polish: "Pencil"
|
|
4626
|
+
};
|
|
4627
|
+
var SEVERITY_LABEL_KEY2 = {
|
|
4628
|
+
bug: "sev_bug",
|
|
4629
|
+
question: "sev_question",
|
|
4630
|
+
polish: "sev_polish"
|
|
4631
|
+
};
|
|
4632
|
+
function clampRegionRect(rect) {
|
|
4633
|
+
const vw = typeof window !== "undefined" ? window.innerWidth : rect.left + rect.width;
|
|
4634
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : rect.top + rect.height;
|
|
4635
|
+
const width = Math.min(Math.max(MIN_REGION_SIZE, rect.width), Math.max(MIN_REGION_SIZE, vw));
|
|
4636
|
+
const height = Math.min(Math.max(MIN_REGION_SIZE, rect.height), Math.max(MIN_REGION_SIZE, vh));
|
|
4637
|
+
const left = Math.min(Math.max(0, rect.left), Math.max(0, vw - width));
|
|
4638
|
+
const top = Math.min(Math.max(0, rect.top), Math.max(0, vh - height));
|
|
4639
|
+
return { top, left, width, height };
|
|
4640
|
+
}
|
|
3433
4641
|
var REGION_HANDLES = [
|
|
3434
4642
|
{ edge: "nw", top: "0%", left: "0%", cursor: "nwse-resize" },
|
|
3435
4643
|
{ edge: "n", top: "0%", left: "50%", cursor: "ns-resize" },
|
|
@@ -3441,7 +4649,7 @@ var REGION_HANDLES = [
|
|
|
3441
4649
|
{ edge: "se", top: "100%", left: "100%", cursor: "nwse-resize" }
|
|
3442
4650
|
];
|
|
3443
4651
|
function CaptureMode() {
|
|
3444
|
-
const { addNote, endCapture, t, dir
|
|
4652
|
+
const { addNote, endCapture, t, dir } = useQa();
|
|
3445
4653
|
const coarse = useCoarsePointer();
|
|
3446
4654
|
const layerRef = React.useRef(null);
|
|
3447
4655
|
const overlayRootRef = React.useRef(null);
|
|
@@ -3455,7 +4663,10 @@ function CaptureMode() {
|
|
|
3455
4663
|
const [shot, setShot] = React.useState(null);
|
|
3456
4664
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
3457
4665
|
const [capturing, setCapturing] = React.useState(false);
|
|
4666
|
+
const [captureError, setCaptureError] = React.useState(false);
|
|
3458
4667
|
const [description, setDescription] = React.useState("");
|
|
4668
|
+
const [severity, setSeverity] = React.useState("bug");
|
|
4669
|
+
const [targetForensics, setTargetForensics] = React.useState(void 0);
|
|
3459
4670
|
const taRef = React.useRef(null);
|
|
3460
4671
|
const activePointerId = React.useRef(null);
|
|
3461
4672
|
const pointerKind = React.useRef("mouse");
|
|
@@ -3476,31 +4687,39 @@ function CaptureMode() {
|
|
|
3476
4687
|
if (!el || el.closest?.("[data-qa-overlay]")) return null;
|
|
3477
4688
|
return el;
|
|
3478
4689
|
}, []);
|
|
3479
|
-
const
|
|
3480
|
-
setSelection(sel);
|
|
3481
|
-
setCandidate(null);
|
|
3482
|
-
setHover(null);
|
|
3483
|
-
setRegionMode(false);
|
|
3484
|
-
setPhase("annotating");
|
|
3485
|
-
setCardIn(false);
|
|
4690
|
+
const runCapture = React.useCallback(async (rect) => {
|
|
3486
4691
|
setCapturing(true);
|
|
4692
|
+
setCaptureError(false);
|
|
3487
4693
|
lockPageScroll();
|
|
3488
4694
|
try {
|
|
3489
|
-
const
|
|
4695
|
+
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
4696
|
+
const blob = outcome.status === "ok" ? outcome.blob : null;
|
|
4697
|
+
const url = blob ? URL.createObjectURL(blob) : null;
|
|
3490
4698
|
if (!mountedRef.current) {
|
|
3491
|
-
if (
|
|
4699
|
+
if (url) URL.revokeObjectURL(url);
|
|
3492
4700
|
return;
|
|
3493
4701
|
}
|
|
4702
|
+
setCaptureError(outcome.status === "failed");
|
|
3494
4703
|
setShot(blob);
|
|
3495
4704
|
setShotUrl((old) => {
|
|
3496
4705
|
if (old) URL.revokeObjectURL(old);
|
|
3497
|
-
return
|
|
4706
|
+
return url;
|
|
3498
4707
|
});
|
|
3499
4708
|
} finally {
|
|
3500
4709
|
unlockPageScroll();
|
|
3501
4710
|
if (mountedRef.current) setCapturing(false);
|
|
3502
4711
|
}
|
|
3503
4712
|
}, []);
|
|
4713
|
+
const beginAnnotation = React.useCallback(async (sel) => {
|
|
4714
|
+
setSelection(sel);
|
|
4715
|
+
setCandidate(null);
|
|
4716
|
+
setHover(null);
|
|
4717
|
+
setRegionMode(false);
|
|
4718
|
+
setSeverity("bug");
|
|
4719
|
+
setPhase("annotating");
|
|
4720
|
+
setCardIn(false);
|
|
4721
|
+
await runCapture(sel.rect);
|
|
4722
|
+
}, [runCapture]);
|
|
3504
4723
|
React.useEffect(() => {
|
|
3505
4724
|
if (phase !== "annotating") {
|
|
3506
4725
|
setCardIn(false);
|
|
@@ -3559,42 +4778,50 @@ function CaptureMode() {
|
|
|
3559
4778
|
activePointerId.current = null;
|
|
3560
4779
|
const d = dragRef.current;
|
|
3561
4780
|
dragRef.current = null;
|
|
4781
|
+
setDrag(null);
|
|
3562
4782
|
const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD2 : TOUCH_DRAG_THRESHOLD;
|
|
3563
4783
|
const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
|
|
3564
4784
|
scrollSnap.current = { x: window.scrollX, y: window.scrollY };
|
|
4785
|
+
let regionRect = null;
|
|
3565
4786
|
if (moved && d) {
|
|
3566
|
-
const
|
|
4787
|
+
const rawRect = {
|
|
3567
4788
|
left: Math.min(d.x0, e.clientX),
|
|
3568
4789
|
top: Math.min(d.y0, e.clientY),
|
|
3569
4790
|
width: Math.abs(e.clientX - d.x0),
|
|
3570
4791
|
height: Math.abs(e.clientY - d.y0)
|
|
3571
4792
|
};
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
if (coarse) {
|
|
3575
|
-
setCandidate(sel);
|
|
3576
|
-
setPhase("confirming");
|
|
3577
|
-
} else {
|
|
3578
|
-
void beginAnnotation(sel);
|
|
4793
|
+
if (rawRect.width >= MIN_REGION_SIZE || rawRect.height >= MIN_REGION_SIZE) {
|
|
4794
|
+
regionRect = clampRegionRect(rawRect);
|
|
3579
4795
|
}
|
|
3580
|
-
}
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
const sel = {
|
|
3585
|
-
kind: "element",
|
|
3586
|
-
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
|
3587
|
-
selector: getStableSelector(el),
|
|
3588
|
-
text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
|
|
3589
|
-
tagName: el.tagName.toLowerCase()
|
|
3590
|
-
};
|
|
4796
|
+
}
|
|
4797
|
+
if (regionRect) {
|
|
4798
|
+
const sel2 = { kind: "region", rect: regionRect };
|
|
4799
|
+
setTargetForensics(void 0);
|
|
3591
4800
|
if (coarse) {
|
|
3592
|
-
setCandidate(
|
|
3593
|
-
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
4801
|
+
setCandidate(sel2);
|
|
3594
4802
|
setPhase("confirming");
|
|
3595
4803
|
} else {
|
|
3596
|
-
void beginAnnotation(
|
|
4804
|
+
void beginAnnotation(sel2);
|
|
3597
4805
|
}
|
|
4806
|
+
return;
|
|
4807
|
+
}
|
|
4808
|
+
const el = elementUnder(e.clientX, e.clientY);
|
|
4809
|
+
if (!el) return;
|
|
4810
|
+
const r = el.getBoundingClientRect();
|
|
4811
|
+
const sel = {
|
|
4812
|
+
kind: "element",
|
|
4813
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
|
4814
|
+
selector: getStableSelector(el),
|
|
4815
|
+
text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
|
|
4816
|
+
tagName: el.tagName.toLowerCase()
|
|
4817
|
+
};
|
|
4818
|
+
setTargetForensics(collectTargetForensics(el));
|
|
4819
|
+
if (coarse) {
|
|
4820
|
+
setCandidate(sel);
|
|
4821
|
+
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
4822
|
+
setPhase("confirming");
|
|
4823
|
+
} else {
|
|
4824
|
+
void beginAnnotation(sel);
|
|
3598
4825
|
}
|
|
3599
4826
|
};
|
|
3600
4827
|
const onPointerCancel = (e) => {
|
|
@@ -3629,23 +4856,29 @@ function CaptureMode() {
|
|
|
3629
4856
|
const dx = e.clientX - hd.startX;
|
|
3630
4857
|
const dy = e.clientY - hd.startY;
|
|
3631
4858
|
const { startRect, edge } = hd;
|
|
3632
|
-
let
|
|
4859
|
+
let top = startRect.top;
|
|
4860
|
+
let left = startRect.left;
|
|
4861
|
+
let right = startRect.left + startRect.width;
|
|
4862
|
+
let bottom = startRect.top + startRect.height;
|
|
3633
4863
|
if (edge === "move") {
|
|
3634
4864
|
left = startRect.left + dx;
|
|
3635
4865
|
top = startRect.top + dy;
|
|
4866
|
+
right = left + startRect.width;
|
|
4867
|
+
bottom = top + startRect.height;
|
|
3636
4868
|
} else {
|
|
3637
|
-
if (edge.includes("e"))
|
|
3638
|
-
if (edge.includes("w"))
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
}
|
|
3642
|
-
if (edge.includes("s")) height = Math.max(MIN_REGION_SIZE, startRect.height + dy);
|
|
3643
|
-
if (edge.includes("n")) {
|
|
3644
|
-
height = Math.max(MIN_REGION_SIZE, startRect.height - dy);
|
|
3645
|
-
top = startRect.top + (startRect.height - height);
|
|
3646
|
-
}
|
|
4869
|
+
if (edge.includes("e")) right += dx;
|
|
4870
|
+
if (edge.includes("w")) left += dx;
|
|
4871
|
+
if (edge.includes("s")) bottom += dy;
|
|
4872
|
+
if (edge.includes("n")) top += dy;
|
|
3647
4873
|
}
|
|
3648
|
-
|
|
4874
|
+
const rawRect = {
|
|
4875
|
+
left: Math.min(left, right),
|
|
4876
|
+
top: Math.min(top, bottom),
|
|
4877
|
+
width: Math.abs(right - left),
|
|
4878
|
+
height: Math.abs(bottom - top)
|
|
4879
|
+
};
|
|
4880
|
+
const rect = clampRegionRect(rawRect);
|
|
4881
|
+
setCandidate((prev) => prev ? { ...prev, rect } : prev);
|
|
3649
4882
|
}, []);
|
|
3650
4883
|
const onHandlePointerUp = React.useCallback((e) => {
|
|
3651
4884
|
const hd = handleDragRef.current;
|
|
@@ -3682,7 +4915,8 @@ function CaptureMode() {
|
|
|
3682
4915
|
}
|
|
3683
4916
|
const first = focusable[0];
|
|
3684
4917
|
const last = focusable[focusable.length - 1];
|
|
3685
|
-
const
|
|
4918
|
+
const rootNode = root.getRootNode();
|
|
4919
|
+
const active = rootNode.activeElement;
|
|
3686
4920
|
const activeInside = !!active && root.contains(active);
|
|
3687
4921
|
if (e.shiftKey) {
|
|
3688
4922
|
if (!activeInside || active === first) {
|
|
@@ -3723,7 +4957,13 @@ function CaptureMode() {
|
|
|
3723
4957
|
},
|
|
3724
4958
|
scroll: { ...scrollSnap.current }
|
|
3725
4959
|
};
|
|
3726
|
-
await addNote({
|
|
4960
|
+
await addNote({
|
|
4961
|
+
description,
|
|
4962
|
+
screenshot: shot ?? void 0,
|
|
4963
|
+
target,
|
|
4964
|
+
severity,
|
|
4965
|
+
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
4966
|
+
});
|
|
3727
4967
|
endCapture();
|
|
3728
4968
|
};
|
|
3729
4969
|
const popStyleFor = React.useCallback((r) => {
|
|
@@ -3745,7 +4985,7 @@ function CaptureMode() {
|
|
|
3745
4985
|
const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
|
|
3746
4986
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
3747
4987
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
3748
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", ref: overlayRootRef, children: [
|
|
4988
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
3749
4989
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3750
4990
|
"div",
|
|
3751
4991
|
{
|
|
@@ -3758,15 +4998,14 @@ function CaptureMode() {
|
|
|
3758
4998
|
style: {
|
|
3759
4999
|
cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
|
|
3760
5000
|
touchAction: coarse ? "none" : "auto",
|
|
3761
|
-
background: "
|
|
5001
|
+
background: "var(--qa-scrim-capture)"
|
|
3762
5002
|
}
|
|
3763
5003
|
}
|
|
3764
5004
|
),
|
|
3765
5005
|
phase === "selecting" && !coarse && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3766
5006
|
"div",
|
|
3767
5007
|
{
|
|
3768
|
-
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-px-4 qa-py-2 qa-text-sm qa-text-
|
|
3769
|
-
style: { background: theme.primary },
|
|
5008
|
+
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-border qa-border-subtle qa-bg-2 qa-px-4 qa-py-2 qa-text-sm qa-text-hi qa-elev-2",
|
|
3770
5009
|
children: [
|
|
3771
5010
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3772
5011
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
@@ -3781,8 +5020,8 @@ function CaptureMode() {
|
|
|
3781
5020
|
"button",
|
|
3782
5021
|
{
|
|
3783
5022
|
onClick: () => endCapture(),
|
|
3784
|
-
className: "qa-tap-icon qa-ms-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-xs qa-hover-bg-white-15",
|
|
3785
|
-
style: { background: "transparent",
|
|
5023
|
+
className: "qa-tap-icon qa-ms-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-xs qa-text-hi qa-hover-bg-white-15",
|
|
5024
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3786
5025
|
children: "Esc"
|
|
3787
5026
|
}
|
|
3788
5027
|
)
|
|
@@ -3792,8 +5031,7 @@ function CaptureMode() {
|
|
|
3792
5031
|
phase === "selecting" && coarse && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3793
5032
|
"div",
|
|
3794
5033
|
{
|
|
3795
|
-
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-px-4 qa-py-2 qa-text-sm qa-text-
|
|
3796
|
-
style: { background: theme.primary },
|
|
5034
|
+
className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-border qa-border-subtle qa-bg-2 qa-px-4 qa-py-2 qa-text-sm qa-text-hi qa-elev-2",
|
|
3797
5035
|
children: [
|
|
3798
5036
|
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3799
5037
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
@@ -3822,8 +5060,8 @@ function CaptureMode() {
|
|
|
3822
5060
|
"button",
|
|
3823
5061
|
{
|
|
3824
5062
|
onClick: () => endCapture(),
|
|
3825
|
-
className: "qa-tap-icon qa-ms-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-xs qa-hover-bg-white-15",
|
|
3826
|
-
style: { background: "transparent",
|
|
5063
|
+
className: "qa-tap-icon qa-ms-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-xs qa-text-hi qa-hover-bg-white-15",
|
|
5064
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3827
5065
|
children: "Esc"
|
|
3828
5066
|
}
|
|
3829
5067
|
)
|
|
@@ -3840,10 +5078,10 @@ function CaptureMode() {
|
|
|
3840
5078
|
width: activeRect.width,
|
|
3841
5079
|
height: activeRect.height,
|
|
3842
5080
|
pointerEvents: confirmingRegion ? "auto" : "none",
|
|
3843
|
-
outline: `2px ${isRegion ? "dashed" : "solid"}
|
|
5081
|
+
outline: `2px ${isRegion ? "dashed" : "solid"} var(--qa-accent)`,
|
|
3844
5082
|
outlineOffset: "1px",
|
|
3845
|
-
background:
|
|
3846
|
-
boxShadow: phase === "annotating" ? "0 0 0 9999px
|
|
5083
|
+
background: "var(--qa-accent-tint)",
|
|
5084
|
+
boxShadow: phase === "annotating" ? "0 0 0 9999px var(--qa-scrim-spot)" : "none"
|
|
3847
5085
|
},
|
|
3848
5086
|
children: [
|
|
3849
5087
|
(phase === "selecting" || phase === "confirming") && hover?.selector && !drag && /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -3854,7 +5092,7 @@ function CaptureMode() {
|
|
|
3854
5092
|
top: "-1.5rem",
|
|
3855
5093
|
left: 0,
|
|
3856
5094
|
maxWidth: "260px",
|
|
3857
|
-
background:
|
|
5095
|
+
background: "var(--qa-surface-3)"
|
|
3858
5096
|
},
|
|
3859
5097
|
children: hover.selector
|
|
3860
5098
|
}
|
|
@@ -3866,7 +5104,7 @@ function CaptureMode() {
|
|
|
3866
5104
|
style: {
|
|
3867
5105
|
bottom: "-1.5rem",
|
|
3868
5106
|
right: 0,
|
|
3869
|
-
background:
|
|
5107
|
+
background: "var(--qa-accent-active)"
|
|
3870
5108
|
},
|
|
3871
5109
|
children: [
|
|
3872
5110
|
Math.round(drag.rect.width),
|
|
@@ -3903,7 +5141,7 @@ function CaptureMode() {
|
|
|
3903
5141
|
transform: "translate(-50%, -50%)",
|
|
3904
5142
|
touchAction: "none",
|
|
3905
5143
|
cursor,
|
|
3906
|
-
background:
|
|
5144
|
+
background: "var(--qa-accent-tint)"
|
|
3907
5145
|
},
|
|
3908
5146
|
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3909
5147
|
"span",
|
|
@@ -3912,7 +5150,7 @@ function CaptureMode() {
|
|
|
3912
5150
|
style: {
|
|
3913
5151
|
width: 16,
|
|
3914
5152
|
height: 16,
|
|
3915
|
-
background:
|
|
5153
|
+
background: "var(--qa-accent)",
|
|
3916
5154
|
border: "2px solid #fff",
|
|
3917
5155
|
boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
|
|
3918
5156
|
pointerEvents: "none"
|
|
@@ -3933,19 +5171,19 @@ function CaptureMode() {
|
|
|
3933
5171
|
dir,
|
|
3934
5172
|
role: "group",
|
|
3935
5173
|
"aria-label": candidate.kind === "region" ? t("confirm_region") : t("use_this"),
|
|
3936
|
-
className: "qa-fixed qa-z-10096 qa-flex qa-items-center qa-gap-2 qa-rounded-full qa-border qa-px-3 qa-py-2 qa-
|
|
5174
|
+
className: "qa-fixed qa-z-10096 qa-flex qa-items-center qa-gap-2 qa-rounded-full qa-border qa-px-3 qa-py-2 qa-elev-2",
|
|
3937
5175
|
style: {
|
|
3938
5176
|
...confirmPopStyle,
|
|
3939
|
-
background:
|
|
3940
|
-
borderColor:
|
|
5177
|
+
background: "var(--qa-surface-1)",
|
|
5178
|
+
borderColor: "var(--qa-border-subtle)"
|
|
3941
5179
|
},
|
|
3942
5180
|
children: [
|
|
3943
5181
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3944
5182
|
"button",
|
|
3945
5183
|
{
|
|
3946
5184
|
onClick: () => void beginAnnotation(candidate),
|
|
3947
|
-
className: "qa-tap qa-flex qa-items-center qa-gap-1.5 qa-rounded-full qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-
|
|
3948
|
-
style: {
|
|
5185
|
+
className: "qa-tap qa-flex qa-items-center qa-gap-1.5 qa-rounded-full qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-bg-accent",
|
|
5186
|
+
style: { border: "none", cursor: "pointer" },
|
|
3949
5187
|
children: [
|
|
3950
5188
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
|
|
3951
5189
|
t("use_this")
|
|
@@ -3959,14 +5197,10 @@ function CaptureMode() {
|
|
|
3959
5197
|
setCandidate(null);
|
|
3960
5198
|
setHover(null);
|
|
3961
5199
|
setPhase("selecting");
|
|
5200
|
+
setTargetForensics(void 0);
|
|
3962
5201
|
},
|
|
3963
|
-
className: "qa-tap qa-rounded-full qa-border qa-px-3 qa-py-2 qa-text-sm",
|
|
3964
|
-
style: {
|
|
3965
|
-
borderColor: `${theme.primary}33`,
|
|
3966
|
-
color: theme.primary,
|
|
3967
|
-
background: "transparent",
|
|
3968
|
-
cursor: "pointer"
|
|
3969
|
-
},
|
|
5202
|
+
className: "qa-tap qa-rounded-full qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
5203
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3970
5204
|
children: t("adjust")
|
|
3971
5205
|
}
|
|
3972
5206
|
)
|
|
@@ -3978,70 +5212,102 @@ function CaptureMode() {
|
|
|
3978
5212
|
{
|
|
3979
5213
|
"data-qa-overlay": "true",
|
|
3980
5214
|
dir,
|
|
3981
|
-
className: `qa-fixed qa-z-10096 qa-w-320 qa-overflow-hidden qa-rounded-xl qa-border qa-
|
|
5215
|
+
className: `qa-fixed qa-z-10096 qa-w-320 qa-overflow-hidden qa-rounded-xl qa-border qa-border-subtle qa-elev-3 qa-card-anim${cardIn ? " qa-card-in" : ""}`,
|
|
3982
5216
|
style: {
|
|
3983
5217
|
...popStyle,
|
|
3984
|
-
background:
|
|
3985
|
-
borderColor: `${theme.primary}22`,
|
|
3986
|
-
fontFamily: dir === "rtl" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif"
|
|
5218
|
+
background: "var(--qa-surface-1)"
|
|
3987
5219
|
},
|
|
3988
5220
|
children: [
|
|
3989
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
),
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
style: { background: "transparent", border: "none", cursor: "pointer", color: "#fff" },
|
|
4009
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 16 })
|
|
4010
|
-
}
|
|
4011
|
-
)
|
|
4012
|
-
]
|
|
4013
|
-
}
|
|
4014
|
-
),
|
|
5221
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-px-3 qa-py-2 qa-bg-2 qa-border-b qa-border-subtle qa-text-hi", children: [
|
|
5222
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5223
|
+
Icon,
|
|
5224
|
+
{
|
|
5225
|
+
name: selection.kind === "region" ? "Square" : "MousePointerClick",
|
|
5226
|
+
size: 16
|
|
5227
|
+
}
|
|
5228
|
+
),
|
|
5229
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-font-semibold", children: selection.kind === "region" ? t("sel_region") : t("sel_element") }),
|
|
5230
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
5231
|
+
"button",
|
|
5232
|
+
{
|
|
5233
|
+
onClick: () => endCapture(),
|
|
5234
|
+
className: "qa-tap-icon qa-ms-auto qa-opacity-80 qa-hover-opacity-100",
|
|
5235
|
+
style: { background: "transparent", border: "none", cursor: "pointer", color: "var(--qa-ink-hi)" },
|
|
5236
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 16 })
|
|
5237
|
+
}
|
|
5238
|
+
)
|
|
5239
|
+
] }),
|
|
4015
5240
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
|
|
4016
5241
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4017
5242
|
"div",
|
|
4018
5243
|
{
|
|
4019
5244
|
className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
|
|
4020
5245
|
style: {
|
|
4021
|
-
borderColor:
|
|
4022
|
-
background:
|
|
5246
|
+
borderColor: "var(--qa-border-subtle)",
|
|
5247
|
+
background: "var(--qa-surface-0)"
|
|
4023
5248
|
},
|
|
4024
|
-
children: capturing ? /* @__PURE__ */ jsxRuntime.jsxs(
|
|
4025
|
-
"
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
style: { color: theme.primary },
|
|
4029
|
-
children: [
|
|
4030
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
|
|
4031
|
-
t("capturing")
|
|
4032
|
-
]
|
|
4033
|
-
}
|
|
4034
|
-
) : shotUrl ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
5249
|
+
children: capturing ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
|
|
5250
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
|
|
5251
|
+
t("capturing")
|
|
5252
|
+
] }) : shotUrl ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
4035
5253
|
"img",
|
|
4036
5254
|
{
|
|
4037
5255
|
src: shotUrl,
|
|
4038
5256
|
alt: "capture",
|
|
4039
5257
|
className: "qa-max-h-32 qa-rounded-md"
|
|
4040
5258
|
}
|
|
5259
|
+
) : captureError ? (
|
|
5260
|
+
// The render broke rather than being skipped — offer a retry
|
|
5261
|
+
// against the same selection instead of a dead-end message.
|
|
5262
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
|
|
5263
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
|
|
5264
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
5265
|
+
"button",
|
|
5266
|
+
{
|
|
5267
|
+
type: "button",
|
|
5268
|
+
onClick: () => selection && void runCapture(selection.rect),
|
|
5269
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
|
|
5270
|
+
children: [
|
|
5271
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
5272
|
+
t("retry")
|
|
5273
|
+
]
|
|
5274
|
+
}
|
|
5275
|
+
)
|
|
5276
|
+
] })
|
|
4041
5277
|
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
|
|
4042
5278
|
}
|
|
4043
5279
|
),
|
|
4044
5280
|
/* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
5281
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
5282
|
+
"div",
|
|
5283
|
+
{
|
|
5284
|
+
role: "group",
|
|
5285
|
+
"aria-label": t("severity_label"),
|
|
5286
|
+
className: "qa-flex qa-items-center qa-flex-wrap qa-gap-1.5",
|
|
5287
|
+
children: [
|
|
5288
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid qa-me-1", children: t("severity_label") }),
|
|
5289
|
+
SEVERITIES2.map((sev) => {
|
|
5290
|
+
const active = severity === sev;
|
|
5291
|
+
const toneClass = sev === "bug" ? "qa-bg-danger-tint qa-text-danger" : sev === "question" ? "qa-bg-warn-tint qa-text-warn" : "qa-bg-accent-tint qa-text-accent";
|
|
5292
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
5293
|
+
"button",
|
|
5294
|
+
{
|
|
5295
|
+
type: "button",
|
|
5296
|
+
onClick: () => setSeverity(sev),
|
|
5297
|
+
"aria-pressed": active,
|
|
5298
|
+
className: `qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-11 qa-focus-ring ${active ? toneClass : "qa-bg-2 qa-text-mid"}`,
|
|
5299
|
+
style: { cursor: "pointer" },
|
|
5300
|
+
children: [
|
|
5301
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: SEVERITY_ICON[sev], size: 12 }),
|
|
5302
|
+
t(SEVERITY_LABEL_KEY2[sev])
|
|
5303
|
+
]
|
|
5304
|
+
},
|
|
5305
|
+
sev
|
|
5306
|
+
);
|
|
5307
|
+
})
|
|
5308
|
+
]
|
|
5309
|
+
}
|
|
5310
|
+
),
|
|
4045
5311
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4046
5312
|
"textarea",
|
|
4047
5313
|
{
|
|
@@ -4053,8 +5319,7 @@ function CaptureMode() {
|
|
|
4053
5319
|
},
|
|
4054
5320
|
rows: 3,
|
|
4055
5321
|
placeholder: t("annotate_placeholder"),
|
|
4056
|
-
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
4057
|
-
style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
|
|
5322
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
4058
5323
|
}
|
|
4059
5324
|
),
|
|
4060
5325
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
@@ -4063,8 +5328,8 @@ function CaptureMode() {
|
|
|
4063
5328
|
{
|
|
4064
5329
|
onClick: () => void save(),
|
|
4065
5330
|
disabled: !description.trim(),
|
|
4066
|
-
className: "qa-tap qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-
|
|
4067
|
-
style: {
|
|
5331
|
+
className: "qa-tap qa-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-px-3 qa-py-2 qa-text-sm qa-font-semibold qa-bg-accent",
|
|
5332
|
+
style: { border: "none", cursor: "pointer" },
|
|
4068
5333
|
children: [
|
|
4069
5334
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
|
|
4070
5335
|
t("save_point")
|
|
@@ -4079,14 +5344,11 @@ function CaptureMode() {
|
|
|
4079
5344
|
setSelection(null);
|
|
4080
5345
|
setShot(null);
|
|
4081
5346
|
setDescription("");
|
|
5347
|
+
setSeverity("bug");
|
|
5348
|
+
setTargetForensics(void 0);
|
|
4082
5349
|
},
|
|
4083
|
-
className: "qa-tap qa-rounded-lg qa-border qa-px-3 qa-py-2 qa-text-sm",
|
|
4084
|
-
style: {
|
|
4085
|
-
borderColor: `${theme.primary}33`,
|
|
4086
|
-
color: theme.primary,
|
|
4087
|
-
background: "transparent",
|
|
4088
|
-
cursor: "pointer"
|
|
4089
|
-
},
|
|
5350
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
5351
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4090
5352
|
children: t("reselect")
|
|
4091
5353
|
}
|
|
4092
5354
|
)
|
|
@@ -4140,8 +5402,9 @@ function CaptureGate() {
|
|
|
4140
5402
|
return captureActive ? /* @__PURE__ */ jsxRuntime.jsx(CaptureMode, {}) : null;
|
|
4141
5403
|
}
|
|
4142
5404
|
function QaRootInner({ config }) {
|
|
5405
|
+
const { isOpen, setIsOpen, testAlong } = useQa();
|
|
4143
5406
|
const shouldShowInitially = config.alwaysVisible === true || config.visible === true || config.visible === void 0 && !isProduction();
|
|
4144
|
-
const [
|
|
5407
|
+
const [widgetShown, setWidgetShown] = React.useState(shouldShowInitially);
|
|
4145
5408
|
React.useEffect(() => {
|
|
4146
5409
|
if (typeof document === "undefined") return;
|
|
4147
5410
|
const hk = parseHotkey(config.hotkey);
|
|
@@ -4149,16 +5412,22 @@ function QaRootInner({ config }) {
|
|
|
4149
5412
|
const handler = (e) => {
|
|
4150
5413
|
if (e.key.toLowerCase() === hk.key && !!e.shiftKey === hk.shift && !!e.altKey === hk.alt && !!e.ctrlKey === hk.ctrl && !!e.metaKey === hk.meta) {
|
|
4151
5414
|
e.preventDefault();
|
|
4152
|
-
|
|
5415
|
+
if (!widgetShown) {
|
|
5416
|
+
setWidgetShown(true);
|
|
5417
|
+
setIsOpen(true);
|
|
5418
|
+
} else {
|
|
5419
|
+
setIsOpen(!isOpen);
|
|
5420
|
+
}
|
|
4153
5421
|
}
|
|
4154
5422
|
};
|
|
4155
5423
|
document.addEventListener("keydown", handler);
|
|
4156
5424
|
return () => document.removeEventListener("keydown", handler);
|
|
4157
|
-
}, [config.hotkey]);
|
|
4158
|
-
if (!
|
|
5425
|
+
}, [config.hotkey, widgetShown, isOpen, setIsOpen]);
|
|
5426
|
+
if (!widgetShown) return null;
|
|
4159
5427
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
4160
5428
|
/* @__PURE__ */ jsxRuntime.jsx(QaFab, {}),
|
|
4161
|
-
/* @__PURE__ */ jsxRuntime.jsx(QaPanel, {}),
|
|
5429
|
+
testAlong.active ? /* @__PURE__ */ jsxRuntime.jsx(TestAlongHud, {}) : /* @__PURE__ */ jsxRuntime.jsx(QaPanel, {}),
|
|
5430
|
+
/* @__PURE__ */ jsxRuntime.jsx(NoticeHost, {}),
|
|
4162
5431
|
/* @__PURE__ */ jsxRuntime.jsx(CaptureGate, {})
|
|
4163
5432
|
] });
|
|
4164
5433
|
}
|
|
@@ -4177,18 +5446,23 @@ function mountQaStudio(config) {
|
|
|
4177
5446
|
document.body.appendChild(host);
|
|
4178
5447
|
const shadow = host.attachShadow({ mode: "open" });
|
|
4179
5448
|
injectStyles(shadow);
|
|
4180
|
-
applyThemeVars(host, config.theme);
|
|
4181
5449
|
const root = ReactDOM__default.default.createRoot(shadow);
|
|
4182
5450
|
root.render(React__default.default.createElement(QaRoot, { config }));
|
|
5451
|
+
if (config.captureContext !== false) {
|
|
5452
|
+
installContextCapture();
|
|
5453
|
+
}
|
|
4183
5454
|
return {
|
|
4184
5455
|
destroy() {
|
|
5456
|
+
if (config.captureContext !== false) {
|
|
5457
|
+
uninstallContextCapture();
|
|
5458
|
+
}
|
|
4185
5459
|
try {
|
|
4186
5460
|
root.unmount();
|
|
4187
5461
|
} catch {
|
|
4188
5462
|
}
|
|
4189
5463
|
if (host.parentNode) host.remove();
|
|
4190
5464
|
if (typeof document !== "undefined") {
|
|
4191
|
-
document.body.querySelectorAll(":scope > [data-qa-overlay]").forEach((el) => el.remove());
|
|
5465
|
+
document.body.querySelectorAll(":scope > [data-qa-overlay]:not(qapture-overlay)").forEach((el) => el.remove());
|
|
4192
5466
|
}
|
|
4193
5467
|
}
|
|
4194
5468
|
};
|
|
@@ -4209,7 +5483,9 @@ function initQaStudio(config) {
|
|
|
4209
5483
|
function Qapture({ config }) {
|
|
4210
5484
|
React.useEffect(() => {
|
|
4211
5485
|
const instance = initQaStudio(config);
|
|
4212
|
-
return () =>
|
|
5486
|
+
return () => {
|
|
5487
|
+
queueMicrotask(() => instance.destroy());
|
|
5488
|
+
};
|
|
4213
5489
|
}, []);
|
|
4214
5490
|
return null;
|
|
4215
5491
|
}
|
|
@@ -4217,5 +5493,5 @@ function Qapture({ config }) {
|
|
|
4217
5493
|
exports.Qapture = Qapture;
|
|
4218
5494
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
4219
5495
|
exports.initQaStudio = initQaStudio;
|
|
4220
|
-
//# sourceMappingURL=chunk-
|
|
4221
|
-
//# sourceMappingURL=chunk-
|
|
5496
|
+
//# sourceMappingURL=chunk-6KAXMN77.cjs.map
|
|
5497
|
+
//# sourceMappingURL=chunk-6KAXMN77.cjs.map
|