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