qapture2 0.2.4 → 0.3.1
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-RC7ZUQ5X.js → chunk-2CAMCMJP.js} +2314 -999
- package/dist/{chunk-DPJW626S.cjs → chunk-PIN23NW3.cjs} +2313 -998
- 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"];
|
|
@@ -295,6 +264,31 @@ function coerceJourney(raw, warnings) {
|
|
|
295
264
|
}
|
|
296
265
|
return out;
|
|
297
266
|
}
|
|
267
|
+
function warnMissingArabic(loginField, credentials, journey, warnings) {
|
|
268
|
+
const hasAr = (v) => typeof v === "object" && v !== null && isNonEmptyString(v.ar);
|
|
269
|
+
const usesArabic = isNonEmptyString(loginField.ar) || credentials.some((c) => isNonEmptyString(c.roleAr) || hasAr(c.hint)) || journey.some((lane) => hasAr(lane.role) || lane.steps.some((s) => hasAr(s.what) || hasAr(s.expect)));
|
|
270
|
+
if (!usesArabic) return;
|
|
271
|
+
const missing = [];
|
|
272
|
+
for (const lane of journey) {
|
|
273
|
+
for (const step of lane.steps) {
|
|
274
|
+
if (!hasAr(step.what)) missing.push(`journey "${lane.id}" \u2192 ${step.path} (what)`);
|
|
275
|
+
if (step.expect !== void 0 && !hasAr(step.expect)) {
|
|
276
|
+
missing.push(`journey "${lane.id}" \u2192 ${step.path} (expect)`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const c of credentials) {
|
|
281
|
+
if (!isNonEmptyString(c.roleAr)) missing.push(`credentials role="${c.role}" (roleAr)`);
|
|
282
|
+
if (c.hint !== void 0 && !hasAr(c.hint)) missing.push(`credentials role="${c.role}" (hint.ar)`);
|
|
283
|
+
}
|
|
284
|
+
if (!missing.length) return;
|
|
285
|
+
const LIMIT = 6;
|
|
286
|
+
const shown = missing.slice(0, LIMIT).join("; ");
|
|
287
|
+
const rest = missing.length > LIMIT ? ` (+${missing.length - LIMIT} more)` : "";
|
|
288
|
+
warnings.push(
|
|
289
|
+
`Arabic is used elsewhere in this config, but ${missing.length} bilingual field(s) have no "ar" and will show English to an Arabic-language tester: ${shown}${rest}. Every {en, ar} pair needs BOTH filled in \u2014 a plain string or an {en}-only object reads identically to a bilingual field that was simply never translated.`
|
|
290
|
+
);
|
|
291
|
+
}
|
|
298
292
|
function coercePreamble(raw) {
|
|
299
293
|
if (raw === null || raw === void 0) return null;
|
|
300
294
|
if (typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
@@ -309,7 +303,6 @@ function validateConfig(input) {
|
|
|
309
303
|
return {
|
|
310
304
|
config: {
|
|
311
305
|
namespace: DEFAULTS.namespace,
|
|
312
|
-
theme: { ...DEFAULT_THEME },
|
|
313
306
|
brand: { label: DEFAULTS.brandLabel },
|
|
314
307
|
loginField: { ...DEFAULTS.loginField },
|
|
315
308
|
credentials: [],
|
|
@@ -318,7 +311,8 @@ function validateConfig(input) {
|
|
|
318
311
|
rtl: DEFAULTS.rtl,
|
|
319
312
|
visible: DEFAULTS.visible,
|
|
320
313
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
321
|
-
hotkey: DEFAULTS.hotkey
|
|
314
|
+
hotkey: DEFAULTS.hotkey,
|
|
315
|
+
captureContext: DEFAULTS.captureContext
|
|
322
316
|
},
|
|
323
317
|
warnings
|
|
324
318
|
};
|
|
@@ -328,7 +322,6 @@ function validateConfig(input) {
|
|
|
328
322
|
return {
|
|
329
323
|
config: {
|
|
330
324
|
namespace: DEFAULTS.namespace,
|
|
331
|
-
theme: { ...DEFAULT_THEME },
|
|
332
325
|
brand: { label: DEFAULTS.brandLabel },
|
|
333
326
|
loginField: { ...DEFAULTS.loginField },
|
|
334
327
|
credentials: [],
|
|
@@ -337,14 +330,19 @@ function validateConfig(input) {
|
|
|
337
330
|
rtl: DEFAULTS.rtl,
|
|
338
331
|
visible: DEFAULTS.visible,
|
|
339
332
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
340
|
-
hotkey: DEFAULTS.hotkey
|
|
333
|
+
hotkey: DEFAULTS.hotkey,
|
|
334
|
+
captureContext: DEFAULTS.captureContext
|
|
341
335
|
},
|
|
342
336
|
warnings
|
|
343
337
|
};
|
|
344
338
|
}
|
|
345
339
|
const raw = input;
|
|
346
340
|
const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
|
|
347
|
-
|
|
341
|
+
if (raw["theme"] !== void 0) {
|
|
342
|
+
warnings.push(
|
|
343
|
+
'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.'
|
|
344
|
+
);
|
|
345
|
+
}
|
|
348
346
|
let brandLabel = DEFAULTS.brandLabel;
|
|
349
347
|
if (raw["brand"] !== void 0 && raw["brand"] !== null && typeof raw["brand"] === "object") {
|
|
350
348
|
const b = raw["brand"];
|
|
@@ -366,6 +364,7 @@ function validateConfig(input) {
|
|
|
366
364
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
367
365
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
368
366
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
367
|
+
const captureContext = typeof raw["captureContext"] === "boolean" ? raw["captureContext"] : DEFAULTS.captureContext;
|
|
369
368
|
let visible = DEFAULTS.visible;
|
|
370
369
|
if (raw["visible"] !== void 0) {
|
|
371
370
|
if (typeof raw["visible"] === "boolean") {
|
|
@@ -374,10 +373,10 @@ function validateConfig(input) {
|
|
|
374
373
|
warnings.push("visible: expected boolean \u2014 using default (dev-only)");
|
|
375
374
|
}
|
|
376
375
|
}
|
|
376
|
+
warnMissingArabic(loginField, credentials, journey, warnings);
|
|
377
377
|
return {
|
|
378
378
|
config: {
|
|
379
379
|
namespace,
|
|
380
|
-
theme,
|
|
381
380
|
brand: { label: brandLabel },
|
|
382
381
|
loginField,
|
|
383
382
|
credentials,
|
|
@@ -386,7 +385,8 @@ function validateConfig(input) {
|
|
|
386
385
|
rtl,
|
|
387
386
|
visible,
|
|
388
387
|
alwaysVisible,
|
|
389
|
-
hotkey
|
|
388
|
+
hotkey,
|
|
389
|
+
captureContext
|
|
390
390
|
},
|
|
391
391
|
warnings
|
|
392
392
|
};
|
|
@@ -397,6 +397,103 @@ var QA_CSS = `
|
|
|
397
397
|
/* \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
398
|
*, *::before, *::after { box-sizing: border-box; }
|
|
399
399
|
|
|
400
|
+
/* \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
|
|
401
|
+
Single source of truth for every colour, shadow, radius, font, motion
|
|
402
|
+
duration, and z-index the widget uses. Nothing here is themeable \u2014
|
|
403
|
+
qapture 0.3.0 ships one fixed, self-contained design. */
|
|
404
|
+
:host {
|
|
405
|
+
/* Surfaces */
|
|
406
|
+
--qa-surface-0: #101215;
|
|
407
|
+
--qa-surface-1: #181B20;
|
|
408
|
+
--qa-surface-2: #20242B;
|
|
409
|
+
--qa-surface-3: #2A2F37;
|
|
410
|
+
|
|
411
|
+
/* Ink */
|
|
412
|
+
--qa-ink-hi: #F4F5F7;
|
|
413
|
+
--qa-ink-mid: #A8AEB8;
|
|
414
|
+
--qa-ink-lo: #6B717C;
|
|
415
|
+
--qa-ink-faint: #4A4F58;
|
|
416
|
+
|
|
417
|
+
/* Accent */
|
|
418
|
+
--qa-accent: #4D9CFF;
|
|
419
|
+
--qa-accent-hover: #6FB0FF;
|
|
420
|
+
--qa-accent-active: #3B84E6;
|
|
421
|
+
--qa-on-accent: #0A0C10;
|
|
422
|
+
--qa-accent-tint: rgba(77,156,255,0.14);
|
|
423
|
+
--qa-accent-border: rgba(77,156,255,0.45);
|
|
424
|
+
|
|
425
|
+
/* Semantic */
|
|
426
|
+
--qa-danger: #FF6B6B;
|
|
427
|
+
--qa-danger-tint: rgba(255,107,107,0.14);
|
|
428
|
+
--qa-warn: #FBBF24;
|
|
429
|
+
--qa-warn-tint: rgba(251,191,36,0.14);
|
|
430
|
+
--qa-success: #34D399;
|
|
431
|
+
--qa-success-tint: rgba(52,211,153,0.14);
|
|
432
|
+
--qa-neutral: #5B616B;
|
|
433
|
+
|
|
434
|
+
/* Borders */
|
|
435
|
+
--qa-border-subtle: rgba(255,255,255,0.08);
|
|
436
|
+
--qa-border-strong: rgba(255,255,255,0.14);
|
|
437
|
+
|
|
438
|
+
/* Scrims */
|
|
439
|
+
--qa-scrim-dialog: rgba(8,9,12,0.50);
|
|
440
|
+
--qa-scrim-capture: rgba(8,9,12,0.32);
|
|
441
|
+
--qa-scrim-spot: rgba(8,9,12,0.55);
|
|
442
|
+
|
|
443
|
+
/* Elevation */
|
|
444
|
+
--qa-sheen: inset 0 1px 0 rgba(255,255,255,0.06);
|
|
445
|
+
--qa-elev-1: 0 1px 2px rgba(0,0,0,0.40);
|
|
446
|
+
--qa-elev-2: 0 8px 24px -8px rgba(0,0,0,0.55);
|
|
447
|
+
--qa-elev-3: 0 24px 60px -16px rgba(0,0,0,0.65);
|
|
448
|
+
|
|
449
|
+
/* Radius */
|
|
450
|
+
--qa-radius-sm: 6px;
|
|
451
|
+
--qa-radius-md: 10px;
|
|
452
|
+
--qa-radius-lg: 14px;
|
|
453
|
+
|
|
454
|
+
/* Fonts (same stack for Arabic \u2014 no separate Arabic typeface) */
|
|
455
|
+
--qa-font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
456
|
+
--qa-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
457
|
+
|
|
458
|
+
/* Motion */
|
|
459
|
+
--qa-dur-1: 120ms;
|
|
460
|
+
--qa-dur-2: 180ms;
|
|
461
|
+
--qa-dur-3: 240ms;
|
|
462
|
+
--qa-ease: cubic-bezier(0.4,0,0.2,1);
|
|
463
|
+
--qa-ease-out: cubic-bezier(0.16,1,0.3,1);
|
|
464
|
+
|
|
465
|
+
/* Z-index scale */
|
|
466
|
+
--qa-z-fab: 9990;
|
|
467
|
+
--qa-z-panel: 9995;
|
|
468
|
+
--qa-z-capture-dim: 10090;
|
|
469
|
+
--qa-z-capture-highlight: 10092;
|
|
470
|
+
--qa-z-capture-region-move: 10093;
|
|
471
|
+
--qa-z-capture-region-handle: 10094;
|
|
472
|
+
--qa-z-capture-hint: 10095;
|
|
473
|
+
--qa-z-capture-ui: 10096;
|
|
474
|
+
--qa-z-toast: 10097;
|
|
475
|
+
|
|
476
|
+
font-family: var(--qa-font);
|
|
477
|
+
color: var(--qa-ink-hi);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* Respect the user's OS-level motion preference: kill durations everywhere,
|
|
481
|
+
including the token defaults so any var(--qa-dur-*)-based rule inherits
|
|
482
|
+
the kill for free. */
|
|
483
|
+
@media (prefers-reduced-motion: reduce) {
|
|
484
|
+
:host {
|
|
485
|
+
--qa-dur-1: 0ms;
|
|
486
|
+
--qa-dur-2: 0ms;
|
|
487
|
+
--qa-dur-3: 0ms;
|
|
488
|
+
}
|
|
489
|
+
*, *::before, *::after {
|
|
490
|
+
animation-duration: 0.01ms !important;
|
|
491
|
+
animation-iteration-count: 1 !important;
|
|
492
|
+
transition-duration: 0.01ms !important;
|
|
493
|
+
scroll-behavior: auto !important;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
400
497
|
/* \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
498
|
.qa-fixed { position: fixed; }
|
|
402
499
|
.qa-absolute { position: absolute; }
|
|
@@ -412,17 +509,18 @@ var QA_CSS = `
|
|
|
412
509
|
.qa-left-half { left: 50%; }
|
|
413
510
|
.qa-right-0 { right: 0; }
|
|
414
511
|
|
|
415
|
-
/* z-index
|
|
512
|
+
/* z-index \u2014 values mirror the --qa-z-* tokens above; class NAMES are kept
|
|
513
|
+
verbatim (scripts/browser-test.mjs string-matches .qa-z-10093/.qa-z-10094). */
|
|
416
514
|
.qa-z-1 { z-index: 1; }
|
|
417
515
|
.qa-z-50 { z-index: 50; }
|
|
418
516
|
.qa-z-100 { z-index: 100; }
|
|
419
|
-
.qa-z-10090 { z-index:
|
|
420
|
-
.qa-z-10092 { z-index:
|
|
517
|
+
.qa-z-10090 { z-index: var(--qa-z-capture-dim); }
|
|
518
|
+
.qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
|
|
421
519
|
/* 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:
|
|
520
|
+
.qa-z-10093 { z-index: var(--qa-z-capture-region-move); }
|
|
521
|
+
.qa-z-10094 { z-index: var(--qa-z-capture-region-handle); }
|
|
522
|
+
.qa-z-10095 { z-index: var(--qa-z-capture-hint); }
|
|
523
|
+
.qa-z-10096 { z-index: var(--qa-z-capture-ui); }
|
|
426
524
|
|
|
427
525
|
/* \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
526
|
.qa-flex { display: flex; }
|
|
@@ -536,7 +634,12 @@ var QA_CSS = `
|
|
|
536
634
|
.qa-border-t { border-top-width: 1px; border-top-style: solid; }
|
|
537
635
|
.qa-border-b { border-bottom-width: 1px; border-bottom-style: solid; }
|
|
538
636
|
.qa-border-white { border-color: #ffffff; }
|
|
539
|
-
.qa-border-white-40 { border-color:
|
|
637
|
+
.qa-border-white-40 { border-color: var(--qa-border-strong); }
|
|
638
|
+
|
|
639
|
+
/* Semantic border colour (combine with .qa-border for width+style) */
|
|
640
|
+
.qa-border-subtle { border-color: var(--qa-border-subtle); }
|
|
641
|
+
.qa-border-strong { border-color: var(--qa-border-strong); }
|
|
642
|
+
.qa-border-accent { border-color: var(--qa-accent-border); }
|
|
540
643
|
|
|
541
644
|
/* \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
645
|
.qa-rounded { border-radius: 0.25rem; }
|
|
@@ -550,6 +653,13 @@ var QA_CSS = `
|
|
|
550
653
|
.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
654
|
.qa-shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
|
|
552
655
|
|
|
656
|
+
/* Semantic elevation \u2014 each layers --qa-sheen (a 1px inner highlight) on top
|
|
657
|
+
of the matching --qa-elev-* drop shadow, so raised surfaces read as
|
|
658
|
+
subtly lit from above rather than flat dark rectangles. */
|
|
659
|
+
.qa-elev-1 { box-shadow: var(--qa-elev-1), var(--qa-sheen); }
|
|
660
|
+
.qa-elev-2 { box-shadow: var(--qa-elev-2), var(--qa-sheen); }
|
|
661
|
+
.qa-elev-3 { box-shadow: var(--qa-elev-3), var(--qa-sheen); }
|
|
662
|
+
|
|
553
663
|
/* \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
664
|
.qa-text-10 { font-size: 10px; }
|
|
555
665
|
.qa-text-11 { font-size: 11px; }
|
|
@@ -560,7 +670,7 @@ var QA_CSS = `
|
|
|
560
670
|
.qa-font-medium { font-weight: 500; }
|
|
561
671
|
.qa-font-semibold { font-weight: 600; }
|
|
562
672
|
.qa-font-bold { font-weight: 700; }
|
|
563
|
-
.qa-font-mono { font-family:
|
|
673
|
+
.qa-font-mono { font-family: var(--qa-font-mono); }
|
|
564
674
|
.qa-leading-relaxed { line-height: 1.625; }
|
|
565
675
|
.qa-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
566
676
|
.qa-whitespace-pre-wrap { white-space: pre-wrap; }
|
|
@@ -583,19 +693,53 @@ var QA_CSS = `
|
|
|
583
693
|
/* \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
694
|
.qa-text-white { color: #ffffff; }
|
|
585
695
|
.qa-text-current { color: currentColor; }
|
|
586
|
-
|
|
587
|
-
.qa-text-slate-
|
|
588
|
-
.qa-text-slate-
|
|
589
|
-
.qa-text-
|
|
590
|
-
.qa-text-
|
|
591
|
-
.qa-text-red-
|
|
696
|
+
/* legacy slate scale, restyled onto the ink levels in place */
|
|
697
|
+
.qa-text-slate-300 { color: var(--qa-ink-faint); }
|
|
698
|
+
.qa-text-slate-400 { color: var(--qa-ink-lo); }
|
|
699
|
+
.qa-text-slate-500 { color: var(--qa-ink-mid); }
|
|
700
|
+
.qa-text-green-600 { color: var(--qa-success); }
|
|
701
|
+
.qa-text-red-500 { color: var(--qa-danger); }
|
|
702
|
+
.qa-text-red-600 { color: var(--qa-danger); }
|
|
703
|
+
|
|
704
|
+
/* Semantic text levels */
|
|
705
|
+
.qa-text-hi { color: var(--qa-ink-hi); }
|
|
706
|
+
.qa-text-mid { color: var(--qa-ink-mid); }
|
|
707
|
+
.qa-text-lo { color: var(--qa-ink-lo); }
|
|
708
|
+
.qa-text-faint { color: var(--qa-ink-faint); }
|
|
709
|
+
.qa-text-accent { color: var(--qa-accent); }
|
|
710
|
+
.qa-text-on-accent { color: var(--qa-on-accent); }
|
|
711
|
+
.qa-text-danger { color: var(--qa-danger); }
|
|
712
|
+
.qa-text-warn { color: var(--qa-warn); }
|
|
713
|
+
.qa-text-success { color: var(--qa-success); }
|
|
592
714
|
|
|
593
715
|
/* \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
|
-
|
|
716
|
+
/* legacy names, restyled onto Graphite tokens in place \u2014 components keep
|
|
717
|
+
using these class names unchanged. */
|
|
718
|
+
.qa-bg-white { background-color: var(--qa-surface-1); }
|
|
719
|
+
.qa-bg-white-25 { background-color: var(--qa-surface-3); }
|
|
596
720
|
.qa-bg-transparent { background-color: transparent; }
|
|
597
|
-
|
|
598
|
-
.
|
|
721
|
+
/* These two were 3%/5% black tints for a light theme, which is inert on a
|
|
722
|
+
dark surface. Restyled as low-alpha WHITE lifts of the same two
|
|
723
|
+
intensities \u2014 still legible as a step above the base surface. */
|
|
724
|
+
.qa-bg-black-3 { background-color: rgba(255,255,255,0.03); }
|
|
725
|
+
.qa-bg-black-5 { background-color: rgba(255,255,255,0.05); }
|
|
726
|
+
|
|
727
|
+
/* Semantic surfaces */
|
|
728
|
+
.qa-bg-0 { background-color: var(--qa-surface-0); }
|
|
729
|
+
.qa-bg-1 { background-color: var(--qa-surface-1); }
|
|
730
|
+
.qa-bg-2 { background-color: var(--qa-surface-2); }
|
|
731
|
+
.qa-bg-3 { background-color: var(--qa-surface-3); }
|
|
732
|
+
|
|
733
|
+
.qa-bg-accent {
|
|
734
|
+
background-color: var(--qa-accent);
|
|
735
|
+
color: var(--qa-on-accent);
|
|
736
|
+
}
|
|
737
|
+
.qa-bg-accent:hover { background-color: var(--qa-accent-hover); }
|
|
738
|
+
|
|
739
|
+
.qa-bg-accent-tint { background-color: var(--qa-accent-tint); }
|
|
740
|
+
.qa-bg-danger-tint { background-color: var(--qa-danger-tint); }
|
|
741
|
+
.qa-bg-warn-tint { background-color: var(--qa-warn-tint); }
|
|
742
|
+
.qa-bg-success-tint { background-color: var(--qa-success-tint); }
|
|
599
743
|
|
|
600
744
|
/* \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
745
|
.qa-opacity-0 { opacity: 0; }
|
|
@@ -614,8 +758,10 @@ var QA_CSS = `
|
|
|
614
758
|
.qa-touch-none { touch-action: none; }
|
|
615
759
|
.qa-touch-pan { touch-action: pan-x pan-y; }
|
|
616
760
|
|
|
617
|
-
|
|
618
|
-
|
|
761
|
+
/* Restyled onto :focus-visible (was :focus) so a mouse click no longer
|
|
762
|
+
leaves a persistent ring \u2014 only keyboard/AT focus does. */
|
|
763
|
+
.qa-focus-ring:focus-visible {
|
|
764
|
+
outline: 2px solid var(--qa-accent);
|
|
619
765
|
outline-offset: 2px;
|
|
620
766
|
}
|
|
621
767
|
|
|
@@ -627,13 +773,14 @@ input:disabled,
|
|
|
627
773
|
}
|
|
628
774
|
|
|
629
775
|
/* 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:
|
|
776
|
+
.qa-hover-bg-black-3:hover { background-color: var(--qa-surface-2); }
|
|
777
|
+
.qa-hover-bg-black-5:hover { background-color: var(--qa-surface-3); }
|
|
778
|
+
.qa-hover-bg-white-15:hover { background-color: var(--qa-surface-2); }
|
|
779
|
+
.qa-hover-bg-2:hover { background-color: var(--qa-surface-2); }
|
|
633
780
|
.qa-hover-opacity-80:hover { opacity: 0.80; }
|
|
634
781
|
.qa-hover-opacity-100:hover { opacity: 1; }
|
|
635
|
-
.qa-hover-text-red:hover { color:
|
|
636
|
-
.qa-hover-text-slate-600:hover { color:
|
|
782
|
+
.qa-hover-text-red:hover { color: var(--qa-danger); }
|
|
783
|
+
.qa-hover-text-slate-600:hover { color: var(--qa-ink-hi); }
|
|
637
784
|
|
|
638
785
|
/* Group-hover (child uses .qa-group-hover-opacity-80 inside a .qa-group parent) */
|
|
639
786
|
.qa-group .qa-group-hover-opacity-80 { opacity: 0.40; }
|
|
@@ -657,13 +804,25 @@ input:disabled,
|
|
|
657
804
|
50% { opacity: 0.5; box-shadow: 0 0 0 8px transparent; }
|
|
658
805
|
}
|
|
659
806
|
|
|
807
|
+
@keyframes qaShimmer {
|
|
808
|
+
0%, 100% { opacity: 0.55; }
|
|
809
|
+
50% { opacity: 1; }
|
|
810
|
+
}
|
|
811
|
+
|
|
660
812
|
.qa-animate-spin {
|
|
661
813
|
animation: qaSpin 1s linear infinite;
|
|
662
814
|
}
|
|
663
815
|
|
|
664
816
|
.qa-animate-pulse-accent {
|
|
665
817
|
animation: qaPulse 2s ease-in-out infinite;
|
|
666
|
-
color: var(--qa-accent
|
|
818
|
+
color: var(--qa-accent);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/* Loading placeholder rows (NoteList while notesLoading && !notes.length) */
|
|
822
|
+
.qa-skeleton {
|
|
823
|
+
background-color: var(--qa-surface-2);
|
|
824
|
+
border-radius: var(--qa-radius-sm);
|
|
825
|
+
animation: qaShimmer 1.4s ease-in-out infinite;
|
|
667
826
|
}
|
|
668
827
|
|
|
669
828
|
/* \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 +909,41 @@ input:disabled,
|
|
|
750
909
|
|
|
751
910
|
/* \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
911
|
.qa-space-y-1\\.5 > * + * { margin-top: 0.375rem; }
|
|
912
|
+
|
|
913
|
+
/* \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 */
|
|
914
|
+
.qa-toast-viewport {
|
|
915
|
+
position: fixed;
|
|
916
|
+
inset-inline: 0;
|
|
917
|
+
bottom: 1rem;
|
|
918
|
+
z-index: var(--qa-z-toast);
|
|
919
|
+
display: flex;
|
|
920
|
+
flex-direction: column;
|
|
921
|
+
align-items: center;
|
|
922
|
+
gap: 0.5rem;
|
|
923
|
+
pointer-events: none;
|
|
924
|
+
}
|
|
925
|
+
.qa-toast {
|
|
926
|
+
pointer-events: auto;
|
|
927
|
+
display: flex;
|
|
928
|
+
align-items: center;
|
|
929
|
+
gap: 0.5rem;
|
|
930
|
+
max-width: min(92vw, 360px);
|
|
931
|
+
padding: 0.625rem 0.75rem;
|
|
932
|
+
background-color: var(--qa-surface-2);
|
|
933
|
+
border: 1px solid var(--qa-border-subtle);
|
|
934
|
+
border-radius: var(--qa-radius-md);
|
|
935
|
+
box-shadow: var(--qa-elev-2), var(--qa-sheen);
|
|
936
|
+
color: var(--qa-ink-hi);
|
|
937
|
+
font-size: 13px;
|
|
938
|
+
opacity: 0;
|
|
939
|
+
transform: translateY(8px);
|
|
940
|
+
transition: opacity var(--qa-dur-2) var(--qa-ease-out),
|
|
941
|
+
transform var(--qa-dur-2) var(--qa-ease-out);
|
|
942
|
+
}
|
|
943
|
+
.qa-toast.qa-toast-in {
|
|
944
|
+
opacity: 1;
|
|
945
|
+
transform: translateY(0);
|
|
946
|
+
}
|
|
753
947
|
`;
|
|
754
948
|
function injectStyles(root) {
|
|
755
949
|
if (typeof CSSStyleSheet !== "undefined" && "adoptedStyleSheets" in Document.prototype) {
|
|
@@ -765,16 +959,295 @@ function injectStyles(root) {
|
|
|
765
959
|
style.textContent = QA_CSS;
|
|
766
960
|
root.appendChild(style);
|
|
767
961
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
962
|
+
|
|
963
|
+
// src/lib/contextBuffer.ts
|
|
964
|
+
var RING_CAP = 75;
|
|
965
|
+
var MAX_MESSAGE_CHARS = 600;
|
|
966
|
+
var MAX_HTML_CHARS = 600;
|
|
967
|
+
var ring = [];
|
|
968
|
+
var installed = false;
|
|
969
|
+
var refCount = 0;
|
|
970
|
+
var drainedUpTo = 0;
|
|
971
|
+
var original = {};
|
|
972
|
+
function push(ev) {
|
|
973
|
+
ring.push(ev);
|
|
974
|
+
if (ring.length > RING_CAP) {
|
|
975
|
+
const overflow = ring.length - RING_CAP;
|
|
976
|
+
ring = ring.slice(overflow);
|
|
977
|
+
drainedUpTo = Math.max(0, drainedUpTo - overflow);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
function clip(s, max = MAX_MESSAGE_CHARS) {
|
|
981
|
+
const str = typeof s === "string" ? s : safeStringify(s);
|
|
982
|
+
return str.length > max ? `${str.slice(0, max)}\u2026` : str;
|
|
983
|
+
}
|
|
984
|
+
function safeStringify(v) {
|
|
985
|
+
if (v === null) return "null";
|
|
986
|
+
if (v === void 0) return "undefined";
|
|
987
|
+
if (typeof v === "string") return v;
|
|
988
|
+
if (v instanceof Error) return `${v.name}: ${v.message}`;
|
|
989
|
+
try {
|
|
990
|
+
return JSON.stringify(v) ?? String(v);
|
|
991
|
+
} catch {
|
|
992
|
+
return String(v);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function now() {
|
|
996
|
+
return Date.now();
|
|
997
|
+
}
|
|
998
|
+
function redactUrl(raw) {
|
|
999
|
+
const s = String(raw ?? "");
|
|
1000
|
+
try {
|
|
1001
|
+
const u = new URL(s, typeof location !== "undefined" ? location.href : "http://localhost");
|
|
1002
|
+
const redacted = u.search ? "?\u2026" : "";
|
|
1003
|
+
return `${u.origin}${u.pathname}${redacted}`;
|
|
1004
|
+
} catch {
|
|
1005
|
+
const cut = s.split(/[?#]/)[0];
|
|
1006
|
+
return s.length > cut.length ? `${cut}?\u2026` : cut;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function installContextCapture() {
|
|
1010
|
+
refCount += 1;
|
|
1011
|
+
if (installed) return;
|
|
1012
|
+
if (typeof window === "undefined" || typeof document === "undefined") return;
|
|
1013
|
+
installed = true;
|
|
1014
|
+
original.consoleError = console.error.bind(console);
|
|
1015
|
+
original.consoleWarn = console.warn.bind(console);
|
|
1016
|
+
console.error = (...args) => {
|
|
1017
|
+
push({ t: now(), kind: "console", level: "error", message: clip(args.map(safeStringify).join(" ")) });
|
|
1018
|
+
original.consoleError?.(...args);
|
|
1019
|
+
};
|
|
1020
|
+
console.warn = (...args) => {
|
|
1021
|
+
push({ t: now(), kind: "console", level: "warn", message: clip(args.map(safeStringify).join(" ")) });
|
|
1022
|
+
original.consoleWarn?.(...args);
|
|
1023
|
+
};
|
|
1024
|
+
original.onError = (e) => {
|
|
1025
|
+
const ev = { t: now(), kind: "error", message: clip(e.message) };
|
|
1026
|
+
if (e.error?.stack) ev.stack = clip(e.error.stack);
|
|
1027
|
+
push(ev);
|
|
1028
|
+
};
|
|
1029
|
+
original.onRejection = (e) => {
|
|
1030
|
+
const reason = e.reason;
|
|
1031
|
+
const ev = {
|
|
1032
|
+
t: now(),
|
|
1033
|
+
kind: "error",
|
|
1034
|
+
message: clip(reason instanceof Error ? `${reason.name}: ${reason.message}` : safeStringify(reason))
|
|
1035
|
+
};
|
|
1036
|
+
if (reason instanceof Error && reason.stack) ev.stack = clip(reason.stack);
|
|
1037
|
+
push(ev);
|
|
1038
|
+
};
|
|
1039
|
+
window.addEventListener("error", original.onError);
|
|
1040
|
+
window.addEventListener("unhandledrejection", original.onRejection);
|
|
1041
|
+
if (typeof window.fetch === "function") {
|
|
1042
|
+
original.fetch = window.fetch.bind(window);
|
|
1043
|
+
window.fetch = async (input, init) => {
|
|
1044
|
+
const started = now();
|
|
1045
|
+
const method = (init?.method || (typeof input === "object" && "method" in input ? input.method : "GET") || "GET").toUpperCase();
|
|
1046
|
+
const url = redactUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
|
|
1047
|
+
try {
|
|
1048
|
+
const res = await original.fetch(input, init);
|
|
1049
|
+
push({ t: started, kind: "network", method, url, status: res.status, durationMs: now() - started });
|
|
1050
|
+
return res;
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
push({
|
|
1053
|
+
t: started,
|
|
1054
|
+
kind: "network",
|
|
1055
|
+
method,
|
|
1056
|
+
url,
|
|
1057
|
+
status: null,
|
|
1058
|
+
durationMs: now() - started,
|
|
1059
|
+
error: clip(err instanceof Error ? err.message : safeStringify(err))
|
|
1060
|
+
});
|
|
1061
|
+
throw err;
|
|
1062
|
+
}
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (typeof XMLHttpRequest !== "undefined") {
|
|
1066
|
+
original.xhrOpen = XMLHttpRequest.prototype.open;
|
|
1067
|
+
original.xhrSend = XMLHttpRequest.prototype.send;
|
|
1068
|
+
XMLHttpRequest.prototype.open = function(method, url, ...rest) {
|
|
1069
|
+
this.__qaMethod = String(method || "GET").toUpperCase();
|
|
1070
|
+
this.__qaUrl = redactUrl(typeof url === "string" ? url : url.href);
|
|
1071
|
+
return original.xhrOpen.call(this, method, url, ...rest);
|
|
1072
|
+
};
|
|
1073
|
+
XMLHttpRequest.prototype.send = function(...args) {
|
|
1074
|
+
this.__qaStart = now();
|
|
1075
|
+
const record = (error) => {
|
|
1076
|
+
const ev = {
|
|
1077
|
+
t: this.__qaStart ?? now(),
|
|
1078
|
+
kind: "network",
|
|
1079
|
+
method: this.__qaMethod ?? "GET",
|
|
1080
|
+
url: this.__qaUrl ?? "",
|
|
1081
|
+
status: error ? null : this.status,
|
|
1082
|
+
durationMs: now() - (this.__qaStart ?? now())
|
|
1083
|
+
};
|
|
1084
|
+
if (error) ev.error = error;
|
|
1085
|
+
push(ev);
|
|
1086
|
+
};
|
|
1087
|
+
this.addEventListener("load", () => record());
|
|
1088
|
+
this.addEventListener("error", () => record("network error"));
|
|
1089
|
+
this.addEventListener("timeout", () => record("timeout"));
|
|
1090
|
+
return original.xhrSend.apply(this, args);
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
function uninstallContextCapture() {
|
|
1095
|
+
if (refCount > 0) refCount -= 1;
|
|
1096
|
+
if (!installed || refCount > 0) return;
|
|
1097
|
+
installed = false;
|
|
1098
|
+
if (original.consoleError) console.error = original.consoleError;
|
|
1099
|
+
if (original.consoleWarn) console.warn = original.consoleWarn;
|
|
1100
|
+
if (original.fetch) window.fetch = original.fetch;
|
|
1101
|
+
if (original.xhrOpen) XMLHttpRequest.prototype.open = original.xhrOpen;
|
|
1102
|
+
if (original.xhrSend) XMLHttpRequest.prototype.send = original.xhrSend;
|
|
1103
|
+
if (original.onError) window.removeEventListener("error", original.onError);
|
|
1104
|
+
if (original.onRejection) {
|
|
1105
|
+
window.removeEventListener("unhandledrejection", original.onRejection);
|
|
1106
|
+
}
|
|
1107
|
+
ring = [];
|
|
1108
|
+
drainedUpTo = 0;
|
|
1109
|
+
}
|
|
1110
|
+
function drainSinceLastNote() {
|
|
1111
|
+
if (!installed) return [];
|
|
1112
|
+
const slice = ring.slice(drainedUpTo);
|
|
1113
|
+
drainedUpTo = ring.length;
|
|
1114
|
+
return slice;
|
|
1115
|
+
}
|
|
1116
|
+
function collectEnvSnapshot(route) {
|
|
1117
|
+
const snap = {
|
|
1118
|
+
url: typeof location !== "undefined" ? redactUrl(location.href) : "",
|
|
1119
|
+
route,
|
|
1120
|
+
viewportW: typeof window !== "undefined" ? window.innerWidth : 0,
|
|
1121
|
+
viewportH: typeof window !== "undefined" ? window.innerHeight : 0,
|
|
1122
|
+
dpr: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
|
|
1123
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1124
|
+
language: typeof navigator !== "undefined" ? navigator.language : "",
|
|
1125
|
+
online: typeof navigator !== "undefined" ? navigator.onLine !== false : true,
|
|
1126
|
+
timezone: ""
|
|
1127
|
+
};
|
|
1128
|
+
try {
|
|
1129
|
+
snap.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
|
|
1130
|
+
} catch {
|
|
1131
|
+
snap.timezone = "";
|
|
1132
|
+
}
|
|
1133
|
+
try {
|
|
1134
|
+
const nav = performance?.getEntriesByType?.("navigation")?.[0];
|
|
1135
|
+
if (nav && Number.isFinite(nav.duration) && nav.duration > 0) {
|
|
1136
|
+
snap.pageLoadMs = Math.round(nav.duration);
|
|
1137
|
+
}
|
|
1138
|
+
} catch {
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
const mem = performance.memory;
|
|
1142
|
+
if (mem?.usedJSHeapSize) snap.memoryUsedMB = Math.round(mem.usedJSHeapSize / 1048576);
|
|
1143
|
+
} catch {
|
|
1144
|
+
}
|
|
1145
|
+
return snap;
|
|
1146
|
+
}
|
|
1147
|
+
function luminance(rgb) {
|
|
1148
|
+
const [r, g, b] = rgb.map((c) => {
|
|
1149
|
+
const v = c / 255;
|
|
1150
|
+
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
1151
|
+
});
|
|
1152
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
1153
|
+
}
|
|
1154
|
+
function parseRgb(color) {
|
|
1155
|
+
const m = (color || "").match(/^rgba?\(([^)]+)\)$/i);
|
|
1156
|
+
if (!m) return null;
|
|
1157
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean).map(parseFloat);
|
|
1158
|
+
if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return null;
|
|
1159
|
+
return [parts[0], parts[1], parts[2]];
|
|
1160
|
+
}
|
|
1161
|
+
var SENSITIVE_FORENSICS_ATTRS = ["value", "checked", "selected"];
|
|
1162
|
+
function sanitizeForForensics(el) {
|
|
1163
|
+
const clone = el.cloneNode(true);
|
|
1164
|
+
const nodes = [clone, ...Array.from(clone.querySelectorAll("*"))];
|
|
1165
|
+
for (const node of nodes) {
|
|
1166
|
+
for (const attr of SENSITIVE_FORENSICS_ATTRS) {
|
|
1167
|
+
if (node.hasAttribute(attr)) node.removeAttribute(attr);
|
|
1168
|
+
}
|
|
1169
|
+
if (node.tagName === "TEXTAREA") node.textContent = "";
|
|
1170
|
+
}
|
|
1171
|
+
return clone;
|
|
1172
|
+
}
|
|
1173
|
+
function collectTargetForensics(el) {
|
|
1174
|
+
const out = {};
|
|
1175
|
+
if (!el || typeof window === "undefined") return out;
|
|
1176
|
+
try {
|
|
1177
|
+
out.html = clip(sanitizeForForensics(el).outerHTML, MAX_HTML_CHARS);
|
|
1178
|
+
} catch {
|
|
1179
|
+
}
|
|
1180
|
+
try {
|
|
1181
|
+
const cs = getComputedStyle(el);
|
|
1182
|
+
out.styles = {
|
|
1183
|
+
display: cs.display,
|
|
1184
|
+
position: cs.position,
|
|
1185
|
+
overflow: cs.overflow,
|
|
1186
|
+
"z-index": cs.zIndex,
|
|
1187
|
+
"font-size": cs.fontSize,
|
|
1188
|
+
color: cs.color,
|
|
1189
|
+
"background-color": cs.backgroundColor
|
|
1190
|
+
};
|
|
1191
|
+
const fg = parseRgb(cs.color);
|
|
1192
|
+
const bg = parseRgb(cs.backgroundColor);
|
|
1193
|
+
let contrastFlag = "unknown";
|
|
1194
|
+
if (fg && bg) {
|
|
1195
|
+
const l1 = luminance(fg);
|
|
1196
|
+
const l2 = luminance(bg);
|
|
1197
|
+
const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
1198
|
+
contrastFlag = ratio < 4.5 ? "low" : "ok";
|
|
1199
|
+
}
|
|
1200
|
+
const name = el.getAttribute("aria-label") || el.getAttribute("title") || el.innerText || el.textContent || "";
|
|
1201
|
+
const tabIndexAttr = el.getAttribute("tabindex");
|
|
1202
|
+
const nativelyFocusable = /^(a|button|input|select|textarea)$/i.test(el.tagName) && !el.disabled;
|
|
1203
|
+
out.a11y = {
|
|
1204
|
+
hasAccessibleName: name.trim().length > 0,
|
|
1205
|
+
tabReachable: nativelyFocusable || tabIndexAttr !== null && tabIndexAttr !== "-1",
|
|
1206
|
+
contrastFlag
|
|
1207
|
+
};
|
|
1208
|
+
const role = el.getAttribute("role");
|
|
1209
|
+
if (role) out.a11y.role = role;
|
|
1210
|
+
} catch {
|
|
1211
|
+
}
|
|
1212
|
+
return out;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// src/lib/journeyMatch.ts
|
|
1216
|
+
function normalizeRoute(route) {
|
|
1217
|
+
const path = String(route ?? "").split(/[?#]/)[0];
|
|
1218
|
+
if (path.length > 1 && path.endsWith("/")) return path.slice(0, -1);
|
|
1219
|
+
return path || "/";
|
|
1220
|
+
}
|
|
1221
|
+
function segments(path) {
|
|
1222
|
+
return normalizeRoute(path).split("/").filter(Boolean);
|
|
1223
|
+
}
|
|
1224
|
+
function matchesWithParams(stepPath, route) {
|
|
1225
|
+
const stepSegs = segments(stepPath);
|
|
1226
|
+
const routeSegs = segments(route);
|
|
1227
|
+
if (stepSegs.length !== routeSegs.length) return false;
|
|
1228
|
+
return stepSegs.every((seg, i) => {
|
|
1229
|
+
const isParam = seg.startsWith(":") || seg.startsWith("[") && seg.endsWith("]");
|
|
1230
|
+
return isParam || seg.toLowerCase() === routeSegs[i].toLowerCase();
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
function matchRouteToSteps(journey, route) {
|
|
1234
|
+
if (!Array.isArray(journey) || !journey.length) return [];
|
|
1235
|
+
const target = normalizeRoute(route);
|
|
1236
|
+
const exact = [];
|
|
1237
|
+
const param = [];
|
|
1238
|
+
for (const lane of journey) {
|
|
1239
|
+
if (!lane || !Array.isArray(lane.steps)) continue;
|
|
1240
|
+
for (const step of lane.steps) {
|
|
1241
|
+
if (!step || typeof step.path !== "string") continue;
|
|
1242
|
+
const ref = { laneId: lane.id, path: step.path };
|
|
1243
|
+
if (normalizeRoute(step.path).toLowerCase() === target.toLowerCase()) {
|
|
1244
|
+
exact.push(ref);
|
|
1245
|
+
} else if (matchesWithParams(step.path, target)) {
|
|
1246
|
+
param.push(ref);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return [...exact, ...param];
|
|
778
1251
|
}
|
|
779
1252
|
|
|
780
1253
|
// src/lib/storage.ts
|
|
@@ -886,7 +1359,36 @@ var STR = {
|
|
|
886
1359
|
use_this: "Use this",
|
|
887
1360
|
adjust: "Adjust",
|
|
888
1361
|
resize: "Resize",
|
|
889
|
-
confirm_region: "Confirm region"
|
|
1362
|
+
confirm_region: "Confirm region",
|
|
1363
|
+
capture_failed: "Screenshot failed",
|
|
1364
|
+
retry: "Retry",
|
|
1365
|
+
persist_failed: "Storage full \u2014 this note may not survive a reload",
|
|
1366
|
+
note_deleted: "Note deleted",
|
|
1367
|
+
notes_cleared: "All notes cleared",
|
|
1368
|
+
undo: "Undo",
|
|
1369
|
+
export_done: "Export downloaded",
|
|
1370
|
+
export_failed: "Export failed",
|
|
1371
|
+
copied: "Copied",
|
|
1372
|
+
copy_failed: "Copy failed",
|
|
1373
|
+
copy_prompt: "Copy as agent prompt",
|
|
1374
|
+
severity_label: "Severity",
|
|
1375
|
+
sev_bug: "Bug",
|
|
1376
|
+
sev_question: "Question",
|
|
1377
|
+
sev_polish: "Polish",
|
|
1378
|
+
status_open: "Open",
|
|
1379
|
+
status_verified: "Verified",
|
|
1380
|
+
context_attached: "{n} runtime events attached",
|
|
1381
|
+
start_walkthrough: "Start walkthrough",
|
|
1382
|
+
step_of: "Step {n} of {m}",
|
|
1383
|
+
next_step: "Next",
|
|
1384
|
+
prev_step: "Back",
|
|
1385
|
+
mark_pass: "Pass",
|
|
1386
|
+
mark_fail: "Fail",
|
|
1387
|
+
capture_here: "Capture here",
|
|
1388
|
+
exit_walkthrough: "Exit",
|
|
1389
|
+
evidence_n: "{n} attached",
|
|
1390
|
+
no_evidence: "ticked, no capture",
|
|
1391
|
+
expected_label: "Expected"
|
|
890
1392
|
},
|
|
891
1393
|
ar: {
|
|
892
1394
|
tab_notes: "\u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
|
|
@@ -937,7 +1439,36 @@ var STR = {
|
|
|
937
1439
|
use_this: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0627",
|
|
938
1440
|
adjust: "\u062A\u0639\u062F\u064A\u0644",
|
|
939
1441
|
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"
|
|
1442
|
+
confirm_region: "\u062A\u0623\u0643\u064A\u062F \u0627\u0644\u0645\u0646\u0637\u0642\u0629",
|
|
1443
|
+
capture_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629",
|
|
1444
|
+
retry: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629",
|
|
1445
|
+
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",
|
|
1446
|
+
note_deleted: "\u062A\u0645 \u062D\u0630\u0641 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629",
|
|
1447
|
+
notes_cleared: "\u062A\u0645 \u0645\u0633\u062D \u062C\u0645\u064A\u0639 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
|
|
1448
|
+
undo: "\u062A\u0631\u0627\u062C\u0639",
|
|
1449
|
+
export_done: "\u062A\u0645 \u062A\u0646\u0632\u064A\u0644 \u0627\u0644\u0645\u0644\u0641",
|
|
1450
|
+
export_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
|
|
1451
|
+
copied: "\u062A\u0645 \u0627\u0644\u0646\u0633\u062E",
|
|
1452
|
+
copy_failed: "\u0641\u0634\u0644 \u0627\u0644\u0646\u0633\u062E",
|
|
1453
|
+
copy_prompt: "\u0646\u0633\u062E \u0643\u0645\u0648\u062C\u0651\u0647 \u0644\u0644\u0648\u0643\u064A\u0644",
|
|
1454
|
+
severity_label: "\u0627\u0644\u0623\u0647\u0645\u064A\u0629",
|
|
1455
|
+
sev_bug: "\u062E\u0644\u0644",
|
|
1456
|
+
sev_question: "\u0633\u0624\u0627\u0644",
|
|
1457
|
+
sev_polish: "\u062A\u062D\u0633\u064A\u0646",
|
|
1458
|
+
status_open: "\u0645\u0641\u062A\u0648\u062D",
|
|
1459
|
+
status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
|
|
1460
|
+
context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
|
|
1461
|
+
start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
|
|
1462
|
+
step_of: "\u0627\u0644\u062E\u0637\u0648\u0629 {n} \u0645\u0646 {m}",
|
|
1463
|
+
next_step: "\u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1464
|
+
prev_step: "\u0627\u0644\u0633\u0627\u0628\u0642",
|
|
1465
|
+
mark_pass: "\u0646\u062C\u0627\u062D",
|
|
1466
|
+
mark_fail: "\u0641\u0634\u0644",
|
|
1467
|
+
capture_here: "\u0627\u0644\u062A\u0642\u0637 \u0647\u0646\u0627",
|
|
1468
|
+
exit_walkthrough: "\u062E\u0631\u0648\u062C",
|
|
1469
|
+
evidence_n: "{n} \u0645\u0631\u0641\u0642",
|
|
1470
|
+
no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
|
|
1471
|
+
expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639"
|
|
941
1472
|
}
|
|
942
1473
|
};
|
|
943
1474
|
function translate(lang, key, vars) {
|
|
@@ -957,10 +1488,14 @@ function pick(value, lang) {
|
|
|
957
1488
|
|
|
958
1489
|
// src/lib/coverage.ts
|
|
959
1490
|
var RISK_COLORS = {
|
|
960
|
-
red: "#
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1491
|
+
red: "#FF6B6B",
|
|
1492
|
+
// --qa-danger
|
|
1493
|
+
amber: "#FBBF24",
|
|
1494
|
+
// --qa-warn
|
|
1495
|
+
green: "#34D399",
|
|
1496
|
+
// --qa-success
|
|
1497
|
+
none: "#5B616B"
|
|
1498
|
+
// --qa-neutral
|
|
964
1499
|
};
|
|
965
1500
|
function computeCoverage(journey, guideChecked) {
|
|
966
1501
|
const red = { total: 0, covered: 0 };
|
|
@@ -1020,33 +1555,103 @@ function computeCoverage(journey, guideChecked) {
|
|
|
1020
1555
|
};
|
|
1021
1556
|
}
|
|
1022
1557
|
|
|
1023
|
-
// src/lib/
|
|
1024
|
-
function
|
|
1558
|
+
// src/lib/noteMarkdown.ts
|
|
1559
|
+
function oneLine(s) {
|
|
1560
|
+
return String(s ?? "").replace(/\r?\n|\r/g, " ").trim();
|
|
1561
|
+
}
|
|
1562
|
+
function formatEvent(ev, t0) {
|
|
1563
|
+
const rel = `${((ev.t - t0) / 1e3).toFixed(1)}s`;
|
|
1564
|
+
if (ev.kind === "network") {
|
|
1565
|
+
const status = ev.status === null ? ev.error ?? "failed" : String(ev.status);
|
|
1566
|
+
return `[${rel}] ${ev.method} ${ev.url} \u2192 ${status} (${ev.durationMs}ms)`;
|
|
1567
|
+
}
|
|
1568
|
+
if (ev.kind === "console") {
|
|
1569
|
+
return `[${rel}] console.${ev.level}: ${oneLine(ev.message)}`;
|
|
1570
|
+
}
|
|
1571
|
+
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
1572
|
+
}
|
|
1573
|
+
function noteToMarkdown(note, opts) {
|
|
1574
|
+
const brand = opts?.brand ?? "Qapture";
|
|
1575
|
+
const idx = opts?.index;
|
|
1025
1576
|
const lines = [];
|
|
1026
|
-
lines.push(
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1577
|
+
lines.push(idx != null ? `## Point ${idx}` : `## ${brand} point`);
|
|
1578
|
+
lines.push("");
|
|
1579
|
+
lines.push(`- **Page:** ${oneLine(note.route) || "/"}`);
|
|
1580
|
+
if (note.url) lines.push(`- **Full URL:** ${oneLine(note.url)}`);
|
|
1581
|
+
lines.push(`- **When:** ${oneLine(note.timestamp)}`);
|
|
1582
|
+
if (note.severity) lines.push(`- **Severity:** ${note.severity}`);
|
|
1583
|
+
if (note.status) lines.push(`- **Status:** ${note.status}`);
|
|
1584
|
+
if (note.journeyRef) {
|
|
1585
|
+
lines.push(`- **Journey step:** ${oneLine(note.journeyRef.laneId)} \u2192 ${oneLine(note.journeyRef.path)}`);
|
|
1586
|
+
}
|
|
1587
|
+
const target = note.target;
|
|
1588
|
+
if (target) {
|
|
1589
|
+
lines.push(`- **Target:** ${target.kind}`);
|
|
1590
|
+
if (target.selector) lines.push(`- **Selector:** \`${oneLine(target.selector)}\``);
|
|
1591
|
+
if (target.tagName) lines.push(`- **Tag:** \`<${oneLine(target.tagName)}>\``);
|
|
1592
|
+
if (target.text) lines.push(`- **Text:** ${oneLine(target.text)}`);
|
|
1593
|
+
const r = target.rect;
|
|
1594
|
+
if (r) {
|
|
1595
|
+
lines.push(
|
|
1596
|
+
`- **Position:** top ${Math.round(r.top)}, left ${Math.round(r.left)}, ${Math.round(r.width)}\xD7${Math.round(r.height)}`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1034
1599
|
}
|
|
1035
|
-
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
if (
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
lines.push(
|
|
1600
|
+
if (idx != null && note.screenshot) {
|
|
1601
|
+
lines.push(`- **Screenshot:** screenshots/point-${idx}.png`);
|
|
1602
|
+
}
|
|
1603
|
+
lines.push("");
|
|
1604
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(no description)_");
|
|
1605
|
+
const ctx = note.context;
|
|
1606
|
+
if (ctx) {
|
|
1607
|
+
const env = ctx.env;
|
|
1608
|
+
const events = Array.isArray(ctx.events) ? ctx.events : [];
|
|
1609
|
+
lines.push("");
|
|
1610
|
+
lines.push("<details><summary>Runtime context at capture</summary>");
|
|
1611
|
+
lines.push("");
|
|
1612
|
+
lines.push("```");
|
|
1613
|
+
if (env) {
|
|
1614
|
+
lines.push(`viewport ${env.viewportW}\xD7${env.viewportH} @${env.dpr}x`);
|
|
1615
|
+
if (env.language) lines.push(`language ${env.language}`);
|
|
1616
|
+
if (env.timezone) lines.push(`timezone ${env.timezone}`);
|
|
1617
|
+
lines.push(`online ${env.online}`);
|
|
1618
|
+
if (env.pageLoadMs != null) lines.push(`pageLoad ${env.pageLoadMs}ms`);
|
|
1619
|
+
if (env.memoryUsedMB != null) lines.push(`jsHeap ${env.memoryUsedMB}MB`);
|
|
1620
|
+
if (env.userAgent) lines.push(`userAgent ${env.userAgent}`);
|
|
1621
|
+
}
|
|
1622
|
+
if (events.length) {
|
|
1623
|
+
const t0 = Date.parse(note.timestamp) || (events[events.length - 1]?.t ?? 0);
|
|
1624
|
+
lines.push("");
|
|
1625
|
+
lines.push(`events (${events.length}, most recent last):`);
|
|
1626
|
+
for (const ev of events) lines.push(` ${formatEvent(ev, t0)}`);
|
|
1627
|
+
} else {
|
|
1628
|
+
lines.push("");
|
|
1629
|
+
lines.push("events (none recorded)");
|
|
1630
|
+
}
|
|
1631
|
+
lines.push("```");
|
|
1632
|
+
const f = ctx.forensics;
|
|
1633
|
+
if (f && (f.html || f.styles || f.a11y)) {
|
|
1634
|
+
lines.push("");
|
|
1635
|
+
lines.push("**Element forensics**");
|
|
1636
|
+
lines.push("");
|
|
1637
|
+
lines.push("```");
|
|
1638
|
+
if (f.html) lines.push(`html ${oneLine(f.html)}`);
|
|
1639
|
+
if (f.styles) {
|
|
1640
|
+
for (const [k, v] of Object.entries(f.styles)) lines.push(`${k.padEnd(7)} ${v}`);
|
|
1641
|
+
}
|
|
1642
|
+
if (f.a11y) {
|
|
1643
|
+
if (f.a11y.role) lines.push(`role ${f.a11y.role}`);
|
|
1644
|
+
lines.push(`a11y accessibleName=${f.a11y.hasAccessibleName} tabReachable=${f.a11y.tabReachable}` + (f.a11y.contrastFlag ? ` contrast=${f.a11y.contrastFlag}` : ""));
|
|
1645
|
+
}
|
|
1646
|
+
lines.push("```");
|
|
1647
|
+
}
|
|
1648
|
+
lines.push("");
|
|
1649
|
+
lines.push("</details>");
|
|
1045
1650
|
}
|
|
1046
|
-
if (note.screenshot) lines.push(`- **Screenshot:** screenshots/point-${num}.png`);
|
|
1047
|
-
lines.push("", note.description || "(no description)", "", "---", "");
|
|
1048
1651
|
return lines.join("\n");
|
|
1049
1652
|
}
|
|
1653
|
+
|
|
1654
|
+
// src/lib/exportZip.ts
|
|
1050
1655
|
function safeName(name, stamp) {
|
|
1051
1656
|
const fallback = `qa-notes-${stamp.slice(0, 10)}`;
|
|
1052
1657
|
let base = (name ?? "").trim().replace(/\.zip$/i, "");
|
|
@@ -1126,12 +1731,13 @@ ${list}`);
|
|
|
1126
1731
|
sections.push("## Conventions\n\n(not provided)");
|
|
1127
1732
|
}
|
|
1128
1733
|
const creds = config.credentials ?? [];
|
|
1734
|
+
const redactedCount = creds.filter((c) => !c.seeded).length;
|
|
1129
1735
|
let credBlock;
|
|
1130
1736
|
if (creds.length > 0) {
|
|
1131
1737
|
const credRows = creds.map((c) => [
|
|
1132
1738
|
c.role,
|
|
1133
1739
|
c.login,
|
|
1134
|
-
c.password || "(none)",
|
|
1740
|
+
c.seeded ? c.password || "(none)" : "(redacted \u2014 not marked seeded)",
|
|
1135
1741
|
c.seeded ? "seeded" : "manual",
|
|
1136
1742
|
c.hint?.en ?? "\u2014"
|
|
1137
1743
|
]);
|
|
@@ -1142,12 +1748,13 @@ ${list}`);
|
|
|
1142
1748
|
} else {
|
|
1143
1749
|
credBlock = "(not provided)";
|
|
1144
1750
|
}
|
|
1751
|
+
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
1752
|
sections.push(
|
|
1146
1753
|
`## Login Context
|
|
1147
1754
|
|
|
1148
1755
|
${credBlock}
|
|
1149
1756
|
|
|
1150
|
-
> **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production
|
|
1757
|
+
> **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production.${redactionNote}`
|
|
1151
1758
|
);
|
|
1152
1759
|
const cov = computeCoverage(journey, guideChecked);
|
|
1153
1760
|
const covTableRows = [
|
|
@@ -1224,7 +1831,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked)
|
|
|
1224
1831
|
"---",
|
|
1225
1832
|
""
|
|
1226
1833
|
].join("\n");
|
|
1227
|
-
const
|
|
1834
|
+
const noteBlocks = notes.map(
|
|
1835
|
+
(n, i) => noteToMarkdown(n, { brand: brandLabel, index: i + 1 })
|
|
1836
|
+
);
|
|
1837
|
+
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
1838
|
+
|
|
1839
|
+
---
|
|
1840
|
+
` : "";
|
|
1841
|
+
const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
|
|
1228
1842
|
zip.file("notes.md", notesMd);
|
|
1229
1843
|
notes.forEach((n, i) => {
|
|
1230
1844
|
if (n.screenshot && shots) {
|
|
@@ -1259,7 +1873,13 @@ function safeLocation() {
|
|
|
1259
1873
|
var QaContext = createContext(null);
|
|
1260
1874
|
var LANG_KEY = "lang";
|
|
1261
1875
|
var GUIDE_KEY = "guide";
|
|
1876
|
+
var GUIDE_FAILED_KEY = "guideFailed";
|
|
1262
1877
|
var LOGIN_KEY = "logins";
|
|
1878
|
+
var PENDING_DELETE_KEY = "pendingDeleteIds";
|
|
1879
|
+
var NOTICE_QUEUE_CAP = 3;
|
|
1880
|
+
var NOTICE_DURATION_INFO = 4e3;
|
|
1881
|
+
var NOTICE_DURATION_ERROR = 6e3;
|
|
1882
|
+
var SOFT_DELETE_MS = 5e3;
|
|
1263
1883
|
function QaProvider({
|
|
1264
1884
|
config,
|
|
1265
1885
|
children
|
|
@@ -1267,6 +1887,7 @@ function QaProvider({
|
|
|
1267
1887
|
const [storage] = useState(() => createStorage(config.namespace));
|
|
1268
1888
|
const [idb] = useState(() => createIdb(config.namespace));
|
|
1269
1889
|
const [notes, setNotes] = useState([]);
|
|
1890
|
+
const [notesLoading, setNotesLoading] = useState(true);
|
|
1270
1891
|
const [isOpen, setIsOpen] = useState(false);
|
|
1271
1892
|
const [activeTab, setActiveTab] = useState("notes");
|
|
1272
1893
|
const [captureActive, setCaptureActive] = useState(false);
|
|
@@ -1279,23 +1900,64 @@ function QaProvider({
|
|
|
1279
1900
|
const [guideChecked, setGuideChecked] = useState(
|
|
1280
1901
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
1281
1902
|
);
|
|
1903
|
+
const [guideFailed, setGuideFailed] = useState(
|
|
1904
|
+
() => new Set(storage.getJSON(GUIDE_FAILED_KEY, []))
|
|
1905
|
+
);
|
|
1282
1906
|
const [loginsUsed, setLoginsUsed] = useState(
|
|
1283
1907
|
() => new Set(storage.getJSON(LOGIN_KEY, []))
|
|
1284
1908
|
);
|
|
1909
|
+
const [notices, setNotices] = useState([]);
|
|
1910
|
+
const noticeTimers = useRef(/* @__PURE__ */ new Map());
|
|
1911
|
+
const [testAlong, setTestAlong] = useState({
|
|
1912
|
+
active: false,
|
|
1913
|
+
index: 0
|
|
1914
|
+
});
|
|
1915
|
+
const pendingDeletes = useRef(
|
|
1916
|
+
/* @__PURE__ */ new Map()
|
|
1917
|
+
);
|
|
1918
|
+
const pendingClear = useRef(null);
|
|
1919
|
+
const readPendingDeleteIds = useCallback(() => {
|
|
1920
|
+
return new Set(storage.getJSON(PENDING_DELETE_KEY, []));
|
|
1921
|
+
}, [storage]);
|
|
1922
|
+
const addPendingDeleteIds = useCallback((ids) => {
|
|
1923
|
+
if (ids.length === 0) return;
|
|
1924
|
+
const current = readPendingDeleteIds();
|
|
1925
|
+
for (const id of ids) current.add(id);
|
|
1926
|
+
storage.setJSON(PENDING_DELETE_KEY, [...current]);
|
|
1927
|
+
}, [storage, readPendingDeleteIds]);
|
|
1928
|
+
const removePendingDeleteIds = useCallback((ids) => {
|
|
1929
|
+
if (ids.length === 0) return;
|
|
1930
|
+
const current = readPendingDeleteIds();
|
|
1931
|
+
let changed = false;
|
|
1932
|
+
for (const id of ids) {
|
|
1933
|
+
if (current.delete(id)) changed = true;
|
|
1934
|
+
}
|
|
1935
|
+
if (changed) storage.setJSON(PENDING_DELETE_KEY, [...current]);
|
|
1936
|
+
}, [storage, readPendingDeleteIds]);
|
|
1285
1937
|
useEffect(() => {
|
|
1286
1938
|
let alive = true;
|
|
1287
1939
|
idb.getAll().then((rows) => {
|
|
1288
1940
|
if (!alive) return;
|
|
1289
|
-
|
|
1941
|
+
let live = rows;
|
|
1942
|
+
const pendingIds = storage.getJSON(PENDING_DELETE_KEY, []);
|
|
1943
|
+
if (pendingIds.length > 0) {
|
|
1944
|
+
const pendingSet = new Set(pendingIds);
|
|
1945
|
+
live = live.filter((n) => !pendingSet.has(n.id));
|
|
1946
|
+
for (const id of pendingIds) void idb.delete(id);
|
|
1947
|
+
storage.setJSON(PENDING_DELETE_KEY, []);
|
|
1948
|
+
}
|
|
1949
|
+
const sorted = live.slice().sort(
|
|
1290
1950
|
(a, b) => a.timestamp < b.timestamp ? 1 : -1
|
|
1291
1951
|
);
|
|
1292
1952
|
setNotes(sorted);
|
|
1293
1953
|
}).catch(() => {
|
|
1954
|
+
}).finally(() => {
|
|
1955
|
+
if (alive) setNotesLoading(false);
|
|
1294
1956
|
});
|
|
1295
1957
|
return () => {
|
|
1296
1958
|
alive = false;
|
|
1297
1959
|
};
|
|
1298
|
-
}, [idb]);
|
|
1960
|
+
}, [idb, storage]);
|
|
1299
1961
|
const setLang = useCallback((l) => {
|
|
1300
1962
|
setLangState(l);
|
|
1301
1963
|
storage.setItem(LANG_KEY, l);
|
|
@@ -1308,27 +1970,160 @@ function QaProvider({
|
|
|
1308
1970
|
(value2) => pick(value2, lang),
|
|
1309
1971
|
[lang]
|
|
1310
1972
|
);
|
|
1973
|
+
const dismissNotice = useCallback((id) => {
|
|
1974
|
+
const timer = noticeTimers.current.get(id);
|
|
1975
|
+
if (timer) {
|
|
1976
|
+
clearTimeout(timer);
|
|
1977
|
+
noticeTimers.current.delete(id);
|
|
1978
|
+
}
|
|
1979
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
1980
|
+
}, []);
|
|
1981
|
+
const notify = useCallback((message, opts) => {
|
|
1982
|
+
const tone = opts?.tone ?? "info";
|
|
1983
|
+
const id = opts?.id ?? uid();
|
|
1984
|
+
const duration = opts?.duration ?? (tone === "error" ? NOTICE_DURATION_ERROR : NOTICE_DURATION_INFO);
|
|
1985
|
+
const existingTimer = noticeTimers.current.get(id);
|
|
1986
|
+
if (existingTimer) clearTimeout(existingTimer);
|
|
1987
|
+
const notice = { id, message, tone, action: opts?.action, duration };
|
|
1988
|
+
setNotices((prev) => {
|
|
1989
|
+
const deduped = prev.filter((n) => n.id !== id);
|
|
1990
|
+
const next = [...deduped, notice];
|
|
1991
|
+
if (next.length <= NOTICE_QUEUE_CAP) return next;
|
|
1992
|
+
const overflow = next.length - NOTICE_QUEUE_CAP;
|
|
1993
|
+
for (const dropped of next.slice(0, overflow)) {
|
|
1994
|
+
const droppedTimer = noticeTimers.current.get(dropped.id);
|
|
1995
|
+
if (droppedTimer) {
|
|
1996
|
+
clearTimeout(droppedTimer);
|
|
1997
|
+
noticeTimers.current.delete(dropped.id);
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
return next.slice(overflow);
|
|
2001
|
+
});
|
|
2002
|
+
const timer = setTimeout(() => {
|
|
2003
|
+
noticeTimers.current.delete(id);
|
|
2004
|
+
setNotices((prev) => prev.filter((n) => n.id !== id));
|
|
2005
|
+
}, duration);
|
|
2006
|
+
noticeTimers.current.set(id, timer);
|
|
2007
|
+
return id;
|
|
2008
|
+
}, []);
|
|
2009
|
+
const testAlongSteps = useMemo(() => {
|
|
2010
|
+
const out = [];
|
|
2011
|
+
for (const lane of journeyOrEmpty(config.journey)) {
|
|
2012
|
+
const laneRole = pick2(lane.role);
|
|
2013
|
+
const color = lane.color ?? "var(--qa-accent)";
|
|
2014
|
+
for (const step of lane.steps ?? []) {
|
|
2015
|
+
out.push({
|
|
2016
|
+
key: `${lane.id}::${step.path}`,
|
|
2017
|
+
laneId: lane.id,
|
|
2018
|
+
laneRole,
|
|
2019
|
+
color,
|
|
2020
|
+
path: step.path,
|
|
2021
|
+
what: step.what,
|
|
2022
|
+
expect: step.expect,
|
|
2023
|
+
risk: step.risk ?? "green"
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
return out;
|
|
2028
|
+
}, [config.journey, pick2]);
|
|
2029
|
+
const startTestAlong = useCallback(() => {
|
|
2030
|
+
setTestAlong({ active: true, index: 0 });
|
|
2031
|
+
setIsOpen(false);
|
|
2032
|
+
}, []);
|
|
2033
|
+
const exitTestAlong = useCallback(() => {
|
|
2034
|
+
setTestAlong({ active: false, index: 0 });
|
|
2035
|
+
}, []);
|
|
2036
|
+
const gotoStep = useCallback((index) => {
|
|
2037
|
+
setTestAlong((prev) => {
|
|
2038
|
+
if (!prev.active) return prev;
|
|
2039
|
+
const maxIndex = Math.max(0, testAlongSteps.length - 1);
|
|
2040
|
+
const clamped = Math.max(0, Math.min(index, maxIndex));
|
|
2041
|
+
if (clamped === prev.index) return prev;
|
|
2042
|
+
return { ...prev, index: clamped };
|
|
2043
|
+
});
|
|
2044
|
+
}, [testAlongSteps.length]);
|
|
2045
|
+
const gradeStep = useCallback((key, grade) => {
|
|
2046
|
+
if (grade === "pass") {
|
|
2047
|
+
setGuideChecked((prev) => {
|
|
2048
|
+
const next = new Set(prev);
|
|
2049
|
+
next.add(key);
|
|
2050
|
+
storage.setJSON(GUIDE_KEY, [...next]);
|
|
2051
|
+
return next;
|
|
2052
|
+
});
|
|
2053
|
+
setGuideFailed((prev) => {
|
|
2054
|
+
const next = new Set(prev);
|
|
2055
|
+
next.delete(key);
|
|
2056
|
+
storage.setJSON(GUIDE_FAILED_KEY, [...next]);
|
|
2057
|
+
return next;
|
|
2058
|
+
});
|
|
2059
|
+
} else {
|
|
2060
|
+
setGuideFailed((prev) => {
|
|
2061
|
+
const next = new Set(prev);
|
|
2062
|
+
next.add(key);
|
|
2063
|
+
storage.setJSON(GUIDE_FAILED_KEY, [...next]);
|
|
2064
|
+
return next;
|
|
2065
|
+
});
|
|
2066
|
+
setGuideChecked((prev) => {
|
|
2067
|
+
const next = new Set(prev);
|
|
2068
|
+
next.delete(key);
|
|
2069
|
+
storage.setJSON(GUIDE_KEY, [...next]);
|
|
2070
|
+
return next;
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
}, [storage]);
|
|
2074
|
+
const evidenceByStep = useMemo(() => {
|
|
2075
|
+
const map = /* @__PURE__ */ new Map();
|
|
2076
|
+
for (let i = notes.length - 1; i >= 0; i--) {
|
|
2077
|
+
const note = notes[i];
|
|
2078
|
+
const ref = note.journeyRef;
|
|
2079
|
+
if (!ref) continue;
|
|
2080
|
+
const key = `${ref.laneId}::${ref.path}`;
|
|
2081
|
+
const arr = map.get(key);
|
|
2082
|
+
if (arr) arr.push(note);
|
|
2083
|
+
else map.set(key, [note]);
|
|
2084
|
+
}
|
|
2085
|
+
return map;
|
|
2086
|
+
}, [notes]);
|
|
1311
2087
|
const addNote = useCallback(
|
|
1312
|
-
async ({
|
|
1313
|
-
description,
|
|
1314
|
-
screenshot,
|
|
1315
|
-
target
|
|
1316
|
-
}) => {
|
|
2088
|
+
async (input) => {
|
|
1317
2089
|
const loc = safeLocation();
|
|
2090
|
+
const route = loc.pathname + (loc.search ? "?\u2026" : "");
|
|
2091
|
+
let journeyRef;
|
|
2092
|
+
if (testAlong.active) {
|
|
2093
|
+
const step = testAlongSteps[testAlong.index];
|
|
2094
|
+
if (step) journeyRef = { laneId: step.laneId, path: step.path };
|
|
2095
|
+
} else {
|
|
2096
|
+
const hits = matchRouteToSteps(config.journey, route);
|
|
2097
|
+
if (hits.length) journeyRef = hits[0];
|
|
2098
|
+
}
|
|
2099
|
+
let context;
|
|
2100
|
+
if (config.captureContext !== false) {
|
|
2101
|
+
context = {
|
|
2102
|
+
events: drainSinceLastNote(),
|
|
2103
|
+
env: collectEnvSnapshot(route),
|
|
2104
|
+
forensics: input.forensics
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
1318
2107
|
const note = {
|
|
1319
2108
|
id: uid(),
|
|
1320
|
-
url: loc.href,
|
|
1321
|
-
route
|
|
2109
|
+
url: redactUrl(loc.href),
|
|
2110
|
+
route,
|
|
1322
2111
|
timestamp: nowIso(),
|
|
1323
|
-
description: (description || "").trim(),
|
|
1324
|
-
screenshot: screenshot ?? void 0,
|
|
1325
|
-
target: target ?? void 0
|
|
2112
|
+
description: (input.description || "").trim(),
|
|
2113
|
+
screenshot: input.screenshot ?? void 0,
|
|
2114
|
+
target: input.target ?? void 0,
|
|
2115
|
+
severity: input.severity,
|
|
2116
|
+
status: input.status,
|
|
2117
|
+
journeyRef,
|
|
2118
|
+
context
|
|
1326
2119
|
};
|
|
1327
2120
|
setNotes((prev) => [note, ...prev]);
|
|
1328
|
-
await idb.put(note);
|
|
1329
|
-
|
|
2121
|
+
const persisted = await idb.put(note);
|
|
2122
|
+
if (!persisted) {
|
|
2123
|
+
notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
|
|
2124
|
+
}
|
|
1330
2125
|
},
|
|
1331
|
-
[idb]
|
|
2126
|
+
[idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t]
|
|
1332
2127
|
);
|
|
1333
2128
|
const updateNote = useCallback(
|
|
1334
2129
|
async (id, patch) => {
|
|
@@ -1343,27 +2138,132 @@ function QaProvider({
|
|
|
1343
2138
|
} else if (patch.screenshot !== void 0) {
|
|
1344
2139
|
next.screenshot = patch.screenshot;
|
|
1345
2140
|
}
|
|
2141
|
+
if (patch.severity !== void 0) next.severity = patch.severity;
|
|
2142
|
+
if (patch.status !== void 0) next.status = patch.status;
|
|
1346
2143
|
updated = next;
|
|
1347
2144
|
return next;
|
|
1348
2145
|
})
|
|
1349
2146
|
);
|
|
1350
2147
|
if (updated) {
|
|
1351
|
-
await idb.put(updated);
|
|
2148
|
+
const persisted = await idb.put(updated);
|
|
2149
|
+
if (!persisted) {
|
|
2150
|
+
notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
|
|
2151
|
+
}
|
|
1352
2152
|
}
|
|
1353
2153
|
},
|
|
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]
|
|
2154
|
+
[idb, notify, t]
|
|
1362
2155
|
);
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
2156
|
+
const deleteNote = useCallback(async (id) => {
|
|
2157
|
+
let removedNote = null;
|
|
2158
|
+
let removedAfterId = null;
|
|
2159
|
+
let found = false;
|
|
2160
|
+
setNotes((prev) => {
|
|
2161
|
+
const idx = prev.findIndex((n) => n.id === id);
|
|
2162
|
+
if (idx === -1) return prev;
|
|
2163
|
+
found = true;
|
|
2164
|
+
removedNote = prev[idx];
|
|
2165
|
+
removedAfterId = prev[idx + 1]?.id ?? null;
|
|
2166
|
+
return prev.filter((n) => n.id !== id);
|
|
2167
|
+
});
|
|
2168
|
+
if (!removedNote || !found) return;
|
|
2169
|
+
const noteToRestore = removedNote;
|
|
2170
|
+
const afterIdToRestore = removedAfterId;
|
|
2171
|
+
const existingPending = pendingDeletes.current.get(id);
|
|
2172
|
+
if (existingPending) clearTimeout(existingPending.timer);
|
|
2173
|
+
addPendingDeleteIds([id]);
|
|
2174
|
+
const timer = setTimeout(() => {
|
|
2175
|
+
pendingDeletes.current.delete(id);
|
|
2176
|
+
void idb.delete(id).then(() => removePendingDeleteIds([id]));
|
|
2177
|
+
}, SOFT_DELETE_MS);
|
|
2178
|
+
pendingDeletes.current.set(id, { note: noteToRestore, afterId: afterIdToRestore, timer });
|
|
2179
|
+
notify(t("note_deleted"), {
|
|
2180
|
+
duration: SOFT_DELETE_MS,
|
|
2181
|
+
id: `delete-${id}`,
|
|
2182
|
+
action: {
|
|
2183
|
+
label: t("undo"),
|
|
2184
|
+
onAction: () => {
|
|
2185
|
+
const pending = pendingDeletes.current.get(id);
|
|
2186
|
+
if (!pending) return;
|
|
2187
|
+
clearTimeout(pending.timer);
|
|
2188
|
+
pendingDeletes.current.delete(id);
|
|
2189
|
+
removePendingDeleteIds([id]);
|
|
2190
|
+
setNotes((prev) => {
|
|
2191
|
+
if (prev.some((n) => n.id === id)) return prev;
|
|
2192
|
+
const next = prev.slice();
|
|
2193
|
+
const anchorIndex = pending.afterId != null ? next.findIndex((n) => n.id === pending.afterId) : -1;
|
|
2194
|
+
const insertAt = anchorIndex === -1 ? next.length : anchorIndex;
|
|
2195
|
+
next.splice(insertAt, 0, pending.note);
|
|
2196
|
+
return next;
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
});
|
|
2201
|
+
}, [idb, notify, t, addPendingDeleteIds, removePendingDeleteIds]);
|
|
2202
|
+
const clearNotes = useCallback(async () => {
|
|
2203
|
+
let snapshot = [];
|
|
2204
|
+
setNotes((prev) => {
|
|
2205
|
+
snapshot = prev;
|
|
2206
|
+
return [];
|
|
2207
|
+
});
|
|
2208
|
+
if (pendingClear.current) clearTimeout(pendingClear.current.timer);
|
|
2209
|
+
for (const [pendingId, pending] of pendingDeletes.current) {
|
|
2210
|
+
clearTimeout(pending.timer);
|
|
2211
|
+
dismissNotice(`delete-${pendingId}`);
|
|
2212
|
+
void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
|
|
2213
|
+
}
|
|
2214
|
+
pendingDeletes.current.clear();
|
|
2215
|
+
const snapshotIds = snapshot.map((n) => n.id);
|
|
2216
|
+
addPendingDeleteIds(snapshotIds);
|
|
2217
|
+
const timer = setTimeout(() => {
|
|
2218
|
+
pendingClear.current = null;
|
|
2219
|
+
void Promise.all(snapshot.map((n) => idb.delete(n.id))).then(
|
|
2220
|
+
() => removePendingDeleteIds(snapshotIds)
|
|
2221
|
+
);
|
|
2222
|
+
}, SOFT_DELETE_MS);
|
|
2223
|
+
pendingClear.current = { notes: snapshot, timer };
|
|
2224
|
+
notify(t("notes_cleared"), {
|
|
2225
|
+
duration: SOFT_DELETE_MS,
|
|
2226
|
+
id: "clear-all",
|
|
2227
|
+
action: {
|
|
2228
|
+
label: t("undo"),
|
|
2229
|
+
onAction: () => {
|
|
2230
|
+
const pending = pendingClear.current;
|
|
2231
|
+
if (!pending) return;
|
|
2232
|
+
clearTimeout(pending.timer);
|
|
2233
|
+
pendingClear.current = null;
|
|
2234
|
+
removePendingDeleteIds(pending.notes.map((n) => n.id));
|
|
2235
|
+
setNotes(pending.notes);
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
});
|
|
2239
|
+
}, [idb, notify, dismissNotice, t, addPendingDeleteIds, removePendingDeleteIds]);
|
|
2240
|
+
const flushPendingDeletes = useCallback(() => {
|
|
2241
|
+
for (const [pendingId, pending] of pendingDeletes.current) {
|
|
2242
|
+
clearTimeout(pending.timer);
|
|
2243
|
+
void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
|
|
2244
|
+
}
|
|
2245
|
+
pendingDeletes.current.clear();
|
|
2246
|
+
if (pendingClear.current) {
|
|
2247
|
+
const { notes: clearedNotes } = pendingClear.current;
|
|
2248
|
+
clearTimeout(pendingClear.current.timer);
|
|
2249
|
+
pendingClear.current = null;
|
|
2250
|
+
const clearedIds = clearedNotes.map((n) => n.id);
|
|
2251
|
+
void Promise.all(clearedNotes.map((n) => idb.delete(n.id))).then(
|
|
2252
|
+
() => removePendingDeleteIds(clearedIds)
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
}, [idb, removePendingDeleteIds]);
|
|
2256
|
+
useEffect(() => {
|
|
2257
|
+
if (typeof window === "undefined") return void 0;
|
|
2258
|
+
const onBeforeUnload = () => flushPendingDeletes();
|
|
2259
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
2260
|
+
return () => {
|
|
2261
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
2262
|
+
flushPendingDeletes();
|
|
2263
|
+
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
2264
|
+
noticeTimers.current.clear();
|
|
2265
|
+
};
|
|
2266
|
+
}, [flushPendingDeletes]);
|
|
1367
2267
|
const startCapture = useCallback(() => {
|
|
1368
2268
|
setIsOpen(false);
|
|
1369
2269
|
setCaptureActive(true);
|
|
@@ -1408,17 +2308,19 @@ function QaProvider({
|
|
|
1408
2308
|
// Data
|
|
1409
2309
|
notes,
|
|
1410
2310
|
guideChecked,
|
|
2311
|
+
guideFailed,
|
|
1411
2312
|
loginsUsed,
|
|
1412
2313
|
// UI state
|
|
1413
2314
|
isOpen,
|
|
1414
2315
|
activeTab,
|
|
1415
2316
|
captureActive,
|
|
1416
2317
|
isExporting,
|
|
2318
|
+
notesLoading,
|
|
1417
2319
|
// i18n
|
|
1418
2320
|
lang,
|
|
1419
2321
|
dir: lang === "ar" ? "rtl" : "ltr",
|
|
1420
2322
|
// Config passthrough
|
|
1421
|
-
|
|
2323
|
+
namespace: config.namespace,
|
|
1422
2324
|
brand: config.brand,
|
|
1423
2325
|
loginField: config.loginField,
|
|
1424
2326
|
credentials: config.credentials,
|
|
@@ -1427,6 +2329,10 @@ function QaProvider({
|
|
|
1427
2329
|
// i18n helpers
|
|
1428
2330
|
t,
|
|
1429
2331
|
pick: pick2,
|
|
2332
|
+
// Notices
|
|
2333
|
+
notices,
|
|
2334
|
+
notify,
|
|
2335
|
+
dismissNotice,
|
|
1430
2336
|
// Actions
|
|
1431
2337
|
setIsOpen,
|
|
1432
2338
|
setActiveTab,
|
|
@@ -1434,15 +2340,26 @@ function QaProvider({
|
|
|
1434
2340
|
addNote,
|
|
1435
2341
|
updateNote,
|
|
1436
2342
|
deleteNote,
|
|
1437
|
-
|
|
2343
|
+
clearNotes,
|
|
1438
2344
|
startCapture,
|
|
1439
2345
|
endCapture,
|
|
1440
2346
|
toggleGuide,
|
|
1441
2347
|
toggleLogin,
|
|
2348
|
+
// Test-along
|
|
2349
|
+
testAlong,
|
|
2350
|
+
testAlongSteps,
|
|
2351
|
+
startTestAlong,
|
|
2352
|
+
exitTestAlong,
|
|
2353
|
+
gotoStep,
|
|
2354
|
+
gradeStep,
|
|
2355
|
+
evidenceByStep,
|
|
1442
2356
|
exportZip: exportZipFn
|
|
1443
2357
|
};
|
|
1444
2358
|
return /* @__PURE__ */ jsx(QaContext.Provider, { value, children });
|
|
1445
2359
|
}
|
|
2360
|
+
function journeyOrEmpty(journey) {
|
|
2361
|
+
return Array.isArray(journey) ? journey : [];
|
|
2362
|
+
}
|
|
1446
2363
|
function useQa() {
|
|
1447
2364
|
const ctx = useContext(QaContext);
|
|
1448
2365
|
if (!ctx) throw new Error("useQa must be used inside <QaProvider>");
|
|
@@ -1452,6 +2369,37 @@ var ICONS = {
|
|
|
1452
2369
|
Check: [
|
|
1453
2370
|
["path", { d: "M20 6 9 17l-5-5" }]
|
|
1454
2371
|
],
|
|
2372
|
+
Bug: [
|
|
2373
|
+
["path", { d: "m8 2 1.88 1.88" }],
|
|
2374
|
+
["path", { d: "M14.12 3.88 16 2" }],
|
|
2375
|
+
["path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }],
|
|
2376
|
+
["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" }],
|
|
2377
|
+
["path", { d: "M12 20v-9" }],
|
|
2378
|
+
["path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }],
|
|
2379
|
+
["path", { d: "M6 13H2" }],
|
|
2380
|
+
["path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }],
|
|
2381
|
+
["path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }],
|
|
2382
|
+
["path", { d: "M22 13h-4" }],
|
|
2383
|
+
["path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" }]
|
|
2384
|
+
],
|
|
2385
|
+
AlertTriangle: [
|
|
2386
|
+
["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" }],
|
|
2387
|
+
["path", { d: "M12 9v4" }],
|
|
2388
|
+
["path", { d: "M12 17h.01" }]
|
|
2389
|
+
],
|
|
2390
|
+
RotateCcw: [
|
|
2391
|
+
["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }],
|
|
2392
|
+
["path", { d: "M3 3v5h5" }]
|
|
2393
|
+
],
|
|
2394
|
+
ChevronLeft: [
|
|
2395
|
+
["path", { d: "m15 18-6-6 6-6" }]
|
|
2396
|
+
],
|
|
2397
|
+
ChevronRight: [
|
|
2398
|
+
["path", { d: "m9 18 6-6-6-6" }]
|
|
2399
|
+
],
|
|
2400
|
+
Play: [
|
|
2401
|
+
["polygon", { points: "6 3 20 12 6 21 6 3" }]
|
|
2402
|
+
],
|
|
1455
2403
|
X: [
|
|
1456
2404
|
["path", { d: "M18 6 6 18" }],
|
|
1457
2405
|
["path", { d: "m6 6 12 12" }]
|
|
@@ -1625,27 +2573,41 @@ var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
|
|
|
1625
2573
|
var FAB_SIZE_PX = 56;
|
|
1626
2574
|
var EDGE_MARGIN = 12;
|
|
1627
2575
|
var DRAG_THRESHOLD = 8;
|
|
1628
|
-
var
|
|
2576
|
+
var LEGACY_FAB_POS_KEY = "qapture:fabpos";
|
|
2577
|
+
function fabPosKey(namespace) {
|
|
2578
|
+
return `${namespace}:fabpos`;
|
|
2579
|
+
}
|
|
1629
2580
|
function isFabPos(v) {
|
|
1630
2581
|
if (!v || typeof v !== "object") return false;
|
|
1631
2582
|
const o = v;
|
|
1632
2583
|
return typeof o.left === "number" && Number.isFinite(o.left) && typeof o.bottom === "number" && Number.isFinite(o.bottom);
|
|
1633
2584
|
}
|
|
1634
|
-
function loadFabPos() {
|
|
2585
|
+
function loadFabPos(namespace) {
|
|
1635
2586
|
if (typeof window === "undefined") return null;
|
|
2587
|
+
const key = fabPosKey(namespace);
|
|
1636
2588
|
try {
|
|
1637
|
-
const raw = window.localStorage.getItem(
|
|
1638
|
-
if (
|
|
1639
|
-
|
|
1640
|
-
|
|
2589
|
+
const raw = window.localStorage.getItem(key);
|
|
2590
|
+
if (raw) {
|
|
2591
|
+
const parsed = JSON.parse(raw);
|
|
2592
|
+
if (isFabPos(parsed)) return parsed;
|
|
2593
|
+
}
|
|
2594
|
+
const legacyRaw = window.localStorage.getItem(LEGACY_FAB_POS_KEY);
|
|
2595
|
+
if (!legacyRaw) return null;
|
|
2596
|
+
const legacyParsed = JSON.parse(legacyRaw);
|
|
2597
|
+
if (!isFabPos(legacyParsed)) return null;
|
|
2598
|
+
try {
|
|
2599
|
+
window.localStorage.setItem(key, JSON.stringify(legacyParsed));
|
|
2600
|
+
} catch {
|
|
2601
|
+
}
|
|
2602
|
+
return legacyParsed;
|
|
1641
2603
|
} catch {
|
|
1642
2604
|
return null;
|
|
1643
2605
|
}
|
|
1644
2606
|
}
|
|
1645
|
-
function saveFabPos(pos) {
|
|
2607
|
+
function saveFabPos(namespace, pos) {
|
|
1646
2608
|
if (typeof window === "undefined") return;
|
|
1647
2609
|
try {
|
|
1648
|
-
window.localStorage.setItem(
|
|
2610
|
+
window.localStorage.setItem(fabPosKey(namespace), JSON.stringify(pos));
|
|
1649
2611
|
} catch {
|
|
1650
2612
|
}
|
|
1651
2613
|
}
|
|
@@ -1662,9 +2624,9 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
1662
2624
|
};
|
|
1663
2625
|
}
|
|
1664
2626
|
function QaFab() {
|
|
1665
|
-
const { isOpen, setIsOpen, notes, captureActive,
|
|
2627
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace } = useQa();
|
|
1666
2628
|
const coarse = useCoarsePointer();
|
|
1667
|
-
const [pos, setPos] = useState(() => loadFabPos());
|
|
2629
|
+
const [pos, setPos] = useState(() => loadFabPos(namespace));
|
|
1668
2630
|
const dragRef = useRef(null);
|
|
1669
2631
|
const didDragRef = useRef(false);
|
|
1670
2632
|
const [, setViewportTick] = useState(0);
|
|
@@ -1728,7 +2690,7 @@ function QaFab() {
|
|
|
1728
2690
|
const dy = e.clientY - d.startY;
|
|
1729
2691
|
const next = clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height);
|
|
1730
2692
|
setPos(next);
|
|
1731
|
-
saveFabPos(next);
|
|
2693
|
+
saveFabPos(namespace, next);
|
|
1732
2694
|
didDragRef.current = true;
|
|
1733
2695
|
}
|
|
1734
2696
|
};
|
|
@@ -1748,9 +2710,7 @@ function QaFab() {
|
|
|
1748
2710
|
bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
|
|
1749
2711
|
width: "3.5rem",
|
|
1750
2712
|
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
|
|
2713
|
+
zIndex: "var(--qa-z-fab)"
|
|
1754
2714
|
};
|
|
1755
2715
|
return /* @__PURE__ */ jsxs(
|
|
1756
2716
|
"button",
|
|
@@ -1765,7 +2725,7 @@ function QaFab() {
|
|
|
1765
2725
|
onPointerCancel: coarse ? onPointerCancel : void 0,
|
|
1766
2726
|
"aria-label": "Qapture \u2014 testing notes",
|
|
1767
2727
|
title: "Qapture",
|
|
1768
|
-
className: `qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-
|
|
2728
|
+
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
2729
|
style: fabStyle,
|
|
1770
2730
|
children: [
|
|
1771
2731
|
!isOpen && /* @__PURE__ */ jsx(
|
|
@@ -1780,17 +2740,14 @@ function QaFab() {
|
|
|
1780
2740
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsx(
|
|
1781
2741
|
"span",
|
|
1782
2742
|
{
|
|
1783
|
-
className: "qa-absolute qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-xs qa-font-bold",
|
|
2743
|
+
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
2744
|
"aria-label": `${notes.length} notes`,
|
|
1785
2745
|
style: {
|
|
1786
2746
|
top: "-4px",
|
|
1787
2747
|
right: "-4px",
|
|
1788
2748
|
minWidth: "1.5rem",
|
|
1789
2749
|
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)"
|
|
2750
|
+
padding: "0 4px"
|
|
1794
2751
|
},
|
|
1795
2752
|
children: notes.length
|
|
1796
2753
|
}
|
|
@@ -1799,13 +2756,47 @@ function QaFab() {
|
|
|
1799
2756
|
}
|
|
1800
2757
|
);
|
|
1801
2758
|
}
|
|
2759
|
+
var SEVERITIES = [
|
|
2760
|
+
{ value: "bug", labelKey: "sev_bug", icon: "Bug" },
|
|
2761
|
+
{ value: "question", labelKey: "sev_question" },
|
|
2762
|
+
{ value: "polish", labelKey: "sev_polish" }
|
|
2763
|
+
];
|
|
2764
|
+
function SeverityChipRow({
|
|
2765
|
+
value,
|
|
2766
|
+
onChange,
|
|
2767
|
+
t
|
|
2768
|
+
}) {
|
|
2769
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2770
|
+
/* @__PURE__ */ jsx("div", { className: "qa-mb-1 qa-text-11 qa-text-lo", children: t("severity_label") }),
|
|
2771
|
+
/* @__PURE__ */ jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1.5", role: "radiogroup", "aria-label": t("severity_label"), children: SEVERITIES.map((s) => {
|
|
2772
|
+
const active = value === s.value;
|
|
2773
|
+
return /* @__PURE__ */ jsxs(
|
|
2774
|
+
"button",
|
|
2775
|
+
{
|
|
2776
|
+
type: "button",
|
|
2777
|
+
role: "radio",
|
|
2778
|
+
"aria-checked": active,
|
|
2779
|
+
onClick: () => onChange(s.value),
|
|
2780
|
+
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"}`,
|
|
2781
|
+
style: { border: "none", cursor: "pointer" },
|
|
2782
|
+
children: [
|
|
2783
|
+
s.icon && /* @__PURE__ */ jsx(Icon, { name: s.icon, size: 12 }),
|
|
2784
|
+
t(s.labelKey)
|
|
2785
|
+
]
|
|
2786
|
+
},
|
|
2787
|
+
s.value
|
|
2788
|
+
);
|
|
2789
|
+
}) })
|
|
2790
|
+
] });
|
|
2791
|
+
}
|
|
1802
2792
|
function NoteEditor() {
|
|
1803
|
-
const { addNote, startCapture, t
|
|
2793
|
+
const { addNote, startCapture, t } = useQa();
|
|
1804
2794
|
const [open, setOpen] = useState(false);
|
|
1805
2795
|
const [description, setDescription] = useState("");
|
|
1806
2796
|
const [screenshot, setScreenshot] = useState(null);
|
|
1807
2797
|
const [previewUrl, setPreviewUrl] = useState(null);
|
|
1808
2798
|
const [dragOver, setDragOver] = useState(false);
|
|
2799
|
+
const [severity, setSeverity] = useState("bug");
|
|
1809
2800
|
const fileRef = useRef(null);
|
|
1810
2801
|
const previewUrlRef = useRef(null);
|
|
1811
2802
|
useEffect(() => {
|
|
@@ -1856,11 +2847,18 @@ function NoteEditor() {
|
|
|
1856
2847
|
if (f?.type.startsWith("image/")) setImage(f);
|
|
1857
2848
|
e.target.value = "";
|
|
1858
2849
|
};
|
|
2850
|
+
const resetForm = () => {
|
|
2851
|
+
setOpen(false);
|
|
2852
|
+
clearImage();
|
|
2853
|
+
setDescription("");
|
|
2854
|
+
setSeverity("bug");
|
|
2855
|
+
};
|
|
1859
2856
|
const save = async () => {
|
|
1860
2857
|
if (!description.trim()) return;
|
|
1861
|
-
await addNote({ description, screenshot: screenshot ?? void 0 });
|
|
2858
|
+
await addNote({ description, screenshot: screenshot ?? void 0, severity });
|
|
1862
2859
|
setDescription("");
|
|
1863
2860
|
clearImage();
|
|
2861
|
+
setSeverity("bug");
|
|
1864
2862
|
setOpen(false);
|
|
1865
2863
|
};
|
|
1866
2864
|
return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
|
|
@@ -1868,12 +2866,8 @@ function NoteEditor() {
|
|
|
1868
2866
|
"button",
|
|
1869
2867
|
{
|
|
1870
2868
|
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
|
-
},
|
|
2869
|
+
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",
|
|
2870
|
+
style: { border: "none", cursor: "pointer" },
|
|
1877
2871
|
children: [
|
|
1878
2872
|
/* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 16 }),
|
|
1879
2873
|
t("capture_cta")
|
|
@@ -1884,13 +2878,8 @@ function NoteEditor() {
|
|
|
1884
2878
|
"button",
|
|
1885
2879
|
{
|
|
1886
2880
|
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
|
-
},
|
|
2881
|
+
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",
|
|
2882
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
1894
2883
|
children: [
|
|
1895
2884
|
/* @__PURE__ */ jsx(Icon, { name: "Plus", size: 14 }),
|
|
1896
2885
|
t("quick_note")
|
|
@@ -1900,8 +2889,7 @@ function NoteEditor() {
|
|
|
1900
2889
|
"div",
|
|
1901
2890
|
{
|
|
1902
2891
|
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 },
|
|
2892
|
+
className: "qa-space-y-2 qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-2.5",
|
|
1905
2893
|
children: [
|
|
1906
2894
|
/* @__PURE__ */ jsx(
|
|
1907
2895
|
"textarea",
|
|
@@ -1911,10 +2899,10 @@ function NoteEditor() {
|
|
|
1911
2899
|
onChange: (e) => setDescription(e.target.value),
|
|
1912
2900
|
rows: 3,
|
|
1913
2901
|
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" }
|
|
2902
|
+
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
2903
|
}
|
|
1917
2904
|
),
|
|
2905
|
+
/* @__PURE__ */ jsx(SeverityChipRow, { value: severity, onChange: setSeverity, t }),
|
|
1918
2906
|
/* @__PURE__ */ jsxs(
|
|
1919
2907
|
"div",
|
|
1920
2908
|
{
|
|
@@ -1924,11 +2912,7 @@ function NoteEditor() {
|
|
|
1924
2912
|
},
|
|
1925
2913
|
onDragLeave: () => setDragOver(false),
|
|
1926
2914
|
onDrop,
|
|
1927
|
-
className:
|
|
1928
|
-
style: {
|
|
1929
|
-
borderColor: dragOver ? theme.accent : `${theme.primary}33`,
|
|
1930
|
-
background: dragOver ? `${theme.accent}12` : "#fff"
|
|
1931
|
-
},
|
|
2915
|
+
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
2916
|
children: [
|
|
1933
2917
|
previewUrl ? /* @__PURE__ */ jsxs("div", { className: "qa-relative qa-inline-block", children: [
|
|
1934
2918
|
/* @__PURE__ */ jsx("img", { src: previewUrl, alt: "preview", style: { maxHeight: "7rem", borderRadius: "0.25rem" } }),
|
|
@@ -1936,11 +2920,10 @@ function NoteEditor() {
|
|
|
1936
2920
|
"button",
|
|
1937
2921
|
{
|
|
1938
2922
|
onClick: clearImage,
|
|
1939
|
-
className: "qa-absolute qa-rounded-full qa-
|
|
2923
|
+
className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
|
|
1940
2924
|
style: {
|
|
1941
2925
|
top: "-8px",
|
|
1942
2926
|
insetInlineEnd: "-8px",
|
|
1943
|
-
background: theme.primary,
|
|
1944
2927
|
border: "none",
|
|
1945
2928
|
cursor: "pointer"
|
|
1946
2929
|
},
|
|
@@ -1951,8 +2934,8 @@ function NoteEditor() {
|
|
|
1951
2934
|
"button",
|
|
1952
2935
|
{
|
|
1953
2936
|
onClick: () => fileRef.current?.click(),
|
|
1954
|
-
className: "qa-inline-flex qa-items-center qa-gap-1 qa-
|
|
1955
|
-
style: {
|
|
2937
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
|
|
2938
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
1956
2939
|
children: [
|
|
1957
2940
|
/* @__PURE__ */ jsx(Icon, { name: "ImagePlus", size: 16 }),
|
|
1958
2941
|
t("image_hint")
|
|
@@ -1976,28 +2959,19 @@ function NoteEditor() {
|
|
|
1976
2959
|
/* @__PURE__ */ jsx(
|
|
1977
2960
|
"button",
|
|
1978
2961
|
{
|
|
1979
|
-
onClick: save,
|
|
2962
|
+
onClick: () => void save(),
|
|
1980
2963
|
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: {
|
|
2964
|
+
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",
|
|
2965
|
+
style: { border: "none", cursor: "pointer" },
|
|
1983
2966
|
children: t("add_point")
|
|
1984
2967
|
}
|
|
1985
2968
|
),
|
|
1986
2969
|
/* @__PURE__ */ jsx(
|
|
1987
2970
|
"button",
|
|
1988
2971
|
{
|
|
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
|
-
},
|
|
2972
|
+
onClick: resetForm,
|
|
2973
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
|
|
2974
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
2001
2975
|
children: t("cancel")
|
|
2002
2976
|
}
|
|
2003
2977
|
)
|
|
@@ -2010,16 +2984,11 @@ function NoteEditor() {
|
|
|
2010
2984
|
|
|
2011
2985
|
// src/lib/highlight.ts
|
|
2012
2986
|
var SETTLE_TIMEOUT_MS = 400;
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
return val || fallback;
|
|
2017
|
-
}
|
|
2018
|
-
function paint(rect, colors) {
|
|
2987
|
+
var ACCENT = "#4D9CFF";
|
|
2988
|
+
var DANGER = "#FF6B6B";
|
|
2989
|
+
function paint(rect) {
|
|
2019
2990
|
if (typeof document === "undefined") return;
|
|
2020
2991
|
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
2992
|
const box = document.createElement("div");
|
|
2024
2993
|
box.setAttribute("data-qa-overlay", "true");
|
|
2025
2994
|
Object.assign(box.style, {
|
|
@@ -2031,9 +3000,9 @@ function paint(rect, colors) {
|
|
|
2031
3000
|
zIndex: "10098",
|
|
2032
3001
|
pointerEvents: "none",
|
|
2033
3002
|
borderRadius: "3px",
|
|
2034
|
-
outline: `3px solid ${
|
|
2035
|
-
background: `${
|
|
2036
|
-
boxShadow: `0 0 0 4px ${
|
|
3003
|
+
outline: `3px solid ${ACCENT}`,
|
|
3004
|
+
background: `${ACCENT}22`,
|
|
3005
|
+
boxShadow: `0 0 0 4px ${DANGER}55`,
|
|
2037
3006
|
transition: "opacity 0.45s ease",
|
|
2038
3007
|
opacity: "1"
|
|
2039
3008
|
});
|
|
@@ -2045,9 +3014,9 @@ function paint(rect, colors) {
|
|
|
2045
3014
|
if (box.parentNode) box.remove();
|
|
2046
3015
|
}, 1500);
|
|
2047
3016
|
}
|
|
2048
|
-
function settleThenPaint(el
|
|
2049
|
-
const
|
|
2050
|
-
const start =
|
|
3017
|
+
function settleThenPaint(el) {
|
|
3018
|
+
const now2 = () => typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
3019
|
+
const start = now2();
|
|
2051
3020
|
let last = null;
|
|
2052
3021
|
let stableFrames = 0;
|
|
2053
3022
|
const tick = () => {
|
|
@@ -2055,15 +3024,15 @@ function settleThenPaint(el, colors) {
|
|
|
2055
3024
|
const unchanged = !!last && r.top === last.top && r.left === last.left && r.width === last.width && r.height === last.height;
|
|
2056
3025
|
stableFrames = unchanged ? stableFrames + 1 : 0;
|
|
2057
3026
|
last = r;
|
|
2058
|
-
if (stableFrames >= 2 ||
|
|
2059
|
-
paint({ top: r.top, left: r.left, width: r.width, height: r.height }
|
|
3027
|
+
if (stableFrames >= 2 || now2() - start >= SETTLE_TIMEOUT_MS) {
|
|
3028
|
+
paint({ top: r.top, left: r.left, width: r.width, height: r.height });
|
|
2060
3029
|
return;
|
|
2061
3030
|
}
|
|
2062
3031
|
requestAnimationFrame(tick);
|
|
2063
3032
|
};
|
|
2064
3033
|
requestAnimationFrame(tick);
|
|
2065
3034
|
}
|
|
2066
|
-
function flashLocate(target
|
|
3035
|
+
function flashLocate(target) {
|
|
2067
3036
|
if (typeof document === "undefined" || !target) return;
|
|
2068
3037
|
let el = null;
|
|
2069
3038
|
if (target.selector) {
|
|
@@ -2075,7 +3044,7 @@ function flashLocate(target, colors) {
|
|
|
2075
3044
|
}
|
|
2076
3045
|
if (el) {
|
|
2077
3046
|
el.scrollIntoView({ block: "center", inline: "center" });
|
|
2078
|
-
settleThenPaint(el
|
|
3047
|
+
settleThenPaint(el);
|
|
2079
3048
|
} else if (target.rect) {
|
|
2080
3049
|
let rect = target.rect;
|
|
2081
3050
|
const snap = target.scroll;
|
|
@@ -2086,106 +3055,92 @@ function flashLocate(target, colors) {
|
|
|
2086
3055
|
rect = { ...rect, left: rect.left - dx, top: rect.top - dy };
|
|
2087
3056
|
}
|
|
2088
3057
|
}
|
|
2089
|
-
paint(rect
|
|
3058
|
+
paint(rect);
|
|
2090
3059
|
}
|
|
2091
3060
|
}
|
|
2092
3061
|
function LocationReveal({ target }) {
|
|
2093
|
-
const { t
|
|
3062
|
+
const { t } = useQa();
|
|
2094
3063
|
const [open, setOpen] = useState(false);
|
|
2095
3064
|
if (!target) return null;
|
|
2096
3065
|
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
|
-
]
|
|
3066
|
+
return /* @__PURE__ */ jsxs("div", { className: "qa-rounded-lg qa-border qa-border-subtle qa-bg-2", children: [
|
|
3067
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1.5 qa-px-2 qa-py-1.5 qa-text-11", children: [
|
|
3068
|
+
/* @__PURE__ */ jsx(Icon, { name: "CheckCircle2", size: 14, className: "qa-text-success" }),
|
|
3069
|
+
/* @__PURE__ */ jsx("span", { className: "qa-font-medium qa-text-hi", children: t("loc_captured") }),
|
|
3070
|
+
/* @__PURE__ */ jsxs(
|
|
3071
|
+
"button",
|
|
3072
|
+
{
|
|
3073
|
+
onClick: () => setOpen((o) => !o),
|
|
3074
|
+
className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-font-medium qa-tap qa-text-accent",
|
|
3075
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3076
|
+
children: [
|
|
3077
|
+
open ? t("loc_hide") : t("loc_show"),
|
|
3078
|
+
/* @__PURE__ */ jsx(
|
|
3079
|
+
Icon,
|
|
3080
|
+
{
|
|
3081
|
+
name: "ChevronDown",
|
|
3082
|
+
size: 14,
|
|
3083
|
+
style: {
|
|
3084
|
+
transition: "transform 150ms",
|
|
3085
|
+
transform: open ? "rotate(180deg)" : "rotate(0deg)"
|
|
2181
3086
|
}
|
|
2182
|
-
|
|
2183
|
-
|
|
3087
|
+
}
|
|
3088
|
+
)
|
|
3089
|
+
]
|
|
3090
|
+
}
|
|
3091
|
+
)
|
|
3092
|
+
] }),
|
|
3093
|
+
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: [
|
|
3094
|
+
target.selector && /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-gap-1", children: [
|
|
3095
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "selector" }),
|
|
3096
|
+
/* @__PURE__ */ jsx(
|
|
3097
|
+
"code",
|
|
3098
|
+
{
|
|
3099
|
+
className: "qa-min-w-0 qa-flex-1 qa-truncate qa-rounded qa-bg-3 qa-px-1",
|
|
3100
|
+
title: target.selector,
|
|
3101
|
+
children: target.selector
|
|
2184
3102
|
}
|
|
2185
3103
|
)
|
|
2186
|
-
]
|
|
2187
|
-
|
|
2188
|
-
|
|
3104
|
+
] }),
|
|
3105
|
+
target.tagName && /* @__PURE__ */ jsxs("div", { children: [
|
|
3106
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "tag " }),
|
|
3107
|
+
/* @__PURE__ */ jsxs("code", { className: "qa-rounded qa-bg-3 qa-px-1", children: [
|
|
3108
|
+
"<",
|
|
3109
|
+
target.tagName,
|
|
3110
|
+
">"
|
|
3111
|
+
] })
|
|
3112
|
+
] }),
|
|
3113
|
+
target.text && /* @__PURE__ */ jsxs("div", { className: "qa-truncate", children: [
|
|
3114
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "text " }),
|
|
3115
|
+
'"',
|
|
3116
|
+
target.text,
|
|
3117
|
+
'"'
|
|
3118
|
+
] }),
|
|
3119
|
+
r && /* @__PURE__ */ jsxs("div", { children: [
|
|
3120
|
+
/* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "pos " }),
|
|
3121
|
+
Math.round(r.left),
|
|
3122
|
+
", ",
|
|
3123
|
+
Math.round(r.top),
|
|
3124
|
+
" \xB7 ",
|
|
3125
|
+
Math.round(r.width),
|
|
3126
|
+
"\xD7",
|
|
3127
|
+
Math.round(r.height)
|
|
3128
|
+
] }),
|
|
3129
|
+
/* @__PURE__ */ jsxs(
|
|
3130
|
+
"button",
|
|
3131
|
+
{
|
|
3132
|
+
onClick: () => flashLocate(target),
|
|
3133
|
+
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",
|
|
3134
|
+
style: { border: "none", cursor: "pointer" },
|
|
3135
|
+
children: [
|
|
3136
|
+
/* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 12 }),
|
|
3137
|
+
/* @__PURE__ */ jsx(Icon, { name: "MapPinned", size: 12 }),
|
|
3138
|
+
t("loc_locate")
|
|
3139
|
+
]
|
|
3140
|
+
}
|
|
3141
|
+
)
|
|
3142
|
+
] })
|
|
3143
|
+
] });
|
|
2189
3144
|
}
|
|
2190
3145
|
function useObjectUrl(blob) {
|
|
2191
3146
|
const [url, setUrl] = useState(null);
|
|
@@ -2202,11 +3157,10 @@ function useObjectUrl(blob) {
|
|
|
2202
3157
|
}
|
|
2203
3158
|
function KindBadge({
|
|
2204
3159
|
target,
|
|
2205
|
-
t
|
|
2206
|
-
theme
|
|
3160
|
+
t
|
|
2207
3161
|
}) {
|
|
2208
3162
|
if (!target) {
|
|
2209
|
-
return /* @__PURE__ */ jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-
|
|
3163
|
+
return /* @__PURE__ */ jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-lo", children: [
|
|
2210
3164
|
/* @__PURE__ */ jsx(Icon, { name: "FileText", size: 12 }),
|
|
2211
3165
|
t("kind_note")
|
|
2212
3166
|
] });
|
|
@@ -2215,8 +3169,7 @@ function KindBadge({
|
|
|
2215
3169
|
return /* @__PURE__ */ jsxs(
|
|
2216
3170
|
"span",
|
|
2217
3171
|
{
|
|
2218
|
-
className:
|
|
2219
|
-
style: { background: region ? theme.accentDark : theme.primary },
|
|
3172
|
+
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
3173
|
children: [
|
|
2221
3174
|
/* @__PURE__ */ jsx(Icon, { name: region ? "Square" : "MousePointerClick", size: 10 }),
|
|
2222
3175
|
region ? t("kind_region") : t("kind_element")
|
|
@@ -2224,13 +3177,59 @@ function KindBadge({
|
|
|
2224
3177
|
}
|
|
2225
3178
|
);
|
|
2226
3179
|
}
|
|
3180
|
+
var SEVERITY_CLASS = {
|
|
3181
|
+
bug: "qa-bg-danger-tint qa-text-danger",
|
|
3182
|
+
question: "qa-bg-warn-tint qa-text-warn",
|
|
3183
|
+
polish: "qa-bg-accent-tint qa-text-accent"
|
|
3184
|
+
};
|
|
3185
|
+
var SEVERITY_LABEL_KEY = {
|
|
3186
|
+
bug: "sev_bug",
|
|
3187
|
+
question: "sev_question",
|
|
3188
|
+
polish: "sev_polish"
|
|
3189
|
+
};
|
|
3190
|
+
function SeverityChip({ severity, t }) {
|
|
3191
|
+
return /* @__PURE__ */ jsxs(
|
|
3192
|
+
"span",
|
|
3193
|
+
{
|
|
3194
|
+
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]}`,
|
|
3195
|
+
children: [
|
|
3196
|
+
severity === "bug" && /* @__PURE__ */ jsx(Icon, { name: "Bug", size: 10 }),
|
|
3197
|
+
t(SEVERITY_LABEL_KEY[severity])
|
|
3198
|
+
]
|
|
3199
|
+
}
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
function StatusPill({
|
|
3203
|
+
status,
|
|
3204
|
+
onToggle,
|
|
3205
|
+
t
|
|
3206
|
+
}) {
|
|
3207
|
+
const verified = status === "verified";
|
|
3208
|
+
return /* @__PURE__ */ jsxs(
|
|
3209
|
+
"button",
|
|
3210
|
+
{
|
|
3211
|
+
type: "button",
|
|
3212
|
+
onClick: onToggle,
|
|
3213
|
+
"aria-label": t(verified ? "status_verified" : "status_open"),
|
|
3214
|
+
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"}`,
|
|
3215
|
+
style: { border: "none", cursor: "pointer" },
|
|
3216
|
+
children: [
|
|
3217
|
+
/* @__PURE__ */ jsx(Icon, { name: verified ? "CheckCircle2" : "Circle", size: 10 }),
|
|
3218
|
+
t(verified ? "status_verified" : "status_open")
|
|
3219
|
+
]
|
|
3220
|
+
}
|
|
3221
|
+
);
|
|
3222
|
+
}
|
|
2227
3223
|
function NoteItem({ note, index }) {
|
|
2228
|
-
const { deleteNote, updateNote,
|
|
3224
|
+
const { deleteNote, updateNote, notify, t } = useQa();
|
|
2229
3225
|
const [editing, setEditing] = useState(false);
|
|
2230
3226
|
const [desc, setDesc] = useState(note.description);
|
|
2231
3227
|
const [img, setImg] = useState(note.screenshot ?? null);
|
|
2232
3228
|
const fileRef = useRef(null);
|
|
2233
3229
|
const thumbUrl = useObjectUrl(editing ? img ?? void 0 : note.screenshot);
|
|
3230
|
+
const severity = note.severity ?? "bug";
|
|
3231
|
+
const status = note.status ?? "open";
|
|
3232
|
+
const contextEventCount = note.context?.events.length ?? 0;
|
|
2234
3233
|
const startEdit = () => {
|
|
2235
3234
|
setDesc(note.description);
|
|
2236
3235
|
setImg(note.screenshot ?? null);
|
|
@@ -2259,188 +3258,178 @@ function NoteItem({ note, index }) {
|
|
|
2259
3258
|
updateNote(note.id, patch);
|
|
2260
3259
|
setEditing(false);
|
|
2261
3260
|
};
|
|
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
|
-
|
|
3261
|
+
const toggleStatus = () => {
|
|
3262
|
+
updateNote(note.id, { status: status === "open" ? "verified" : "open" });
|
|
3263
|
+
};
|
|
3264
|
+
const copyPrompt = async () => {
|
|
3265
|
+
try {
|
|
3266
|
+
if (!navigator.clipboard?.writeText) throw new Error("clipboard unavailable");
|
|
3267
|
+
await navigator.clipboard.writeText(noteToMarkdown(note));
|
|
3268
|
+
notify(t("copied"));
|
|
3269
|
+
} catch {
|
|
3270
|
+
notify(t("copy_failed"), { tone: "error" });
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
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: [
|
|
3274
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-flex-wrap qa-items-center qa-gap-1.5", children: [
|
|
3275
|
+
/* @__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 }),
|
|
3276
|
+
/* @__PURE__ */ jsx(KindBadge, { target: note.target, t }),
|
|
3277
|
+
/* @__PURE__ */ jsx(SeverityChip, { severity, t }),
|
|
3278
|
+
/* @__PURE__ */ jsx(StatusPill, { status, onToggle: toggleStatus, t }),
|
|
3279
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-ms-auto qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3280
|
+
/* @__PURE__ */ jsx(
|
|
3281
|
+
"button",
|
|
3282
|
+
{
|
|
3283
|
+
onClick: () => void copyPrompt(),
|
|
3284
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
|
|
3285
|
+
title: t("copy_prompt"),
|
|
3286
|
+
"aria-label": t("copy_prompt"),
|
|
3287
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3288
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "Copy", size: 14 })
|
|
3289
|
+
}
|
|
3290
|
+
),
|
|
3291
|
+
!editing && /* @__PURE__ */ jsx(
|
|
3292
|
+
"button",
|
|
3293
|
+
{
|
|
3294
|
+
onClick: startEdit,
|
|
3295
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
|
|
3296
|
+
title: t("edit"),
|
|
3297
|
+
"aria-label": t("edit"),
|
|
3298
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3299
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "Pencil", size: 14 })
|
|
3300
|
+
}
|
|
3301
|
+
),
|
|
3302
|
+
/* @__PURE__ */ jsx(
|
|
3303
|
+
"button",
|
|
3304
|
+
{
|
|
3305
|
+
onClick: () => deleteNote(note.id),
|
|
3306
|
+
className: "qa-tap-icon qa-text-mid qa-hover-text-red",
|
|
3307
|
+
"aria-label": "delete",
|
|
3308
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3309
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "Trash2", size: 16 })
|
|
3310
|
+
}
|
|
3311
|
+
)
|
|
3312
|
+
] })
|
|
3313
|
+
] }),
|
|
3314
|
+
editing ? /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", onPaste, children: [
|
|
3315
|
+
/* @__PURE__ */ jsx(
|
|
3316
|
+
"textarea",
|
|
3317
|
+
{
|
|
3318
|
+
autoFocus: true,
|
|
3319
|
+
value: desc,
|
|
3320
|
+
onChange: (e) => setDesc(e.target.value),
|
|
3321
|
+
rows: 3,
|
|
3322
|
+
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"
|
|
3323
|
+
}
|
|
3324
|
+
),
|
|
3325
|
+
/* @__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: [
|
|
3326
|
+
thumbUrl ? /* @__PURE__ */ jsxs("div", { className: "qa-relative qa-inline-block", children: [
|
|
2303
3327
|
/* @__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",
|
|
3328
|
+
"img",
|
|
2316
3329
|
{
|
|
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
|
-
]
|
|
3330
|
+
src: thumbUrl,
|
|
3331
|
+
alt: "screenshot",
|
|
3332
|
+
style: { maxHeight: "7rem", borderRadius: "0.25rem" }
|
|
2368
3333
|
}
|
|
2369
3334
|
),
|
|
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
3335
|
/* @__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",
|
|
3336
|
+
"button",
|
|
2417
3337
|
{
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
style: {
|
|
3338
|
+
onClick: () => setImg(null),
|
|
3339
|
+
className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
|
|
3340
|
+
title: t("remove_image"),
|
|
3341
|
+
style: {
|
|
3342
|
+
top: "-8px",
|
|
3343
|
+
insetInlineEnd: "-8px",
|
|
3344
|
+
border: "none",
|
|
3345
|
+
cursor: "pointer"
|
|
3346
|
+
},
|
|
3347
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "X", size: 12 })
|
|
2422
3348
|
}
|
|
2423
3349
|
)
|
|
2424
|
-
] })
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3350
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
3351
|
+
"button",
|
|
3352
|
+
{
|
|
3353
|
+
onClick: () => fileRef.current?.click(),
|
|
3354
|
+
className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
|
|
3355
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3356
|
+
children: [
|
|
3357
|
+
/* @__PURE__ */ jsx(Icon, { name: "ImagePlus", size: 16 }),
|
|
3358
|
+
t("image_hint")
|
|
3359
|
+
]
|
|
3360
|
+
}
|
|
3361
|
+
),
|
|
3362
|
+
/* @__PURE__ */ jsx(
|
|
3363
|
+
"input",
|
|
3364
|
+
{
|
|
3365
|
+
ref: fileRef,
|
|
3366
|
+
type: "file",
|
|
3367
|
+
accept: "image/*",
|
|
3368
|
+
onChange: onFile,
|
|
3369
|
+
className: "qa-hidden"
|
|
3370
|
+
}
|
|
3371
|
+
)
|
|
3372
|
+
] }),
|
|
3373
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-gap-2", children: [
|
|
3374
|
+
/* @__PURE__ */ jsxs(
|
|
3375
|
+
"button",
|
|
3376
|
+
{
|
|
3377
|
+
onClick: save,
|
|
3378
|
+
disabled: !desc.trim(),
|
|
3379
|
+
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",
|
|
3380
|
+
style: { border: "none", cursor: "pointer" },
|
|
3381
|
+
children: [
|
|
3382
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
|
|
3383
|
+
t("save")
|
|
3384
|
+
]
|
|
3385
|
+
}
|
|
3386
|
+
),
|
|
3387
|
+
/* @__PURE__ */ jsx(
|
|
3388
|
+
"button",
|
|
3389
|
+
{
|
|
3390
|
+
onClick: () => setEditing(false),
|
|
3391
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
|
|
3392
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3393
|
+
children: t("cancel")
|
|
3394
|
+
}
|
|
3395
|
+
)
|
|
3396
|
+
] })
|
|
3397
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3398
|
+
/* @__PURE__ */ jsx("p", { className: "qa-whitespace-pre-wrap qa-break-words qa-text-hi", children: note.description }),
|
|
3399
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-mt-1.5 qa-space-y-1.5 qa-text-11 qa-text-lo", children: [
|
|
3400
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
|
|
3401
|
+
/* @__PURE__ */ jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
|
|
3402
|
+
/* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
|
|
3403
|
+
] }),
|
|
3404
|
+
note.target && /* @__PURE__ */ jsx(LocationReveal, { target: note.target }),
|
|
3405
|
+
contextEventCount > 0 && /* @__PURE__ */ jsx("div", { children: t("context_attached", { n: contextEventCount }) })
|
|
3406
|
+
] }),
|
|
3407
|
+
thumbUrl && /* @__PURE__ */ jsx(
|
|
3408
|
+
"img",
|
|
3409
|
+
{
|
|
3410
|
+
src: thumbUrl,
|
|
3411
|
+
alt: "screenshot",
|
|
3412
|
+
className: "qa-mt-2 qa-w-full qa-rounded-lg qa-border qa-border-subtle"
|
|
3413
|
+
}
|
|
3414
|
+
)
|
|
3415
|
+
] })
|
|
3416
|
+
] });
|
|
2428
3417
|
}
|
|
2429
3418
|
function NoteList() {
|
|
2430
|
-
const { notes,
|
|
3419
|
+
const { notes, notesLoading, t } = useQa();
|
|
3420
|
+
if (notesLoading && !notes.length) {
|
|
3421
|
+
return /* @__PURE__ */ jsxs("ul", { className: "qa-space-y-2", "aria-hidden": "true", children: [
|
|
3422
|
+
/* @__PURE__ */ jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
|
|
3423
|
+
/* @__PURE__ */ jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
|
|
3424
|
+
/* @__PURE__ */ jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } })
|
|
3425
|
+
] });
|
|
3426
|
+
}
|
|
2431
3427
|
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
|
-
);
|
|
3428
|
+
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: [
|
|
3429
|
+
t("no_points"),
|
|
3430
|
+
/* @__PURE__ */ jsx("br", {}),
|
|
3431
|
+
t("no_points_hint", { cta: t("capture_cta") })
|
|
3432
|
+
] });
|
|
2444
3433
|
}
|
|
2445
3434
|
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
3435
|
}
|
|
@@ -2472,17 +3461,27 @@ function EyeIcon({ open, size = 12, className }) {
|
|
|
2472
3461
|
);
|
|
2473
3462
|
}
|
|
2474
3463
|
var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
|
|
2475
|
-
function CopyField({
|
|
3464
|
+
function CopyField({
|
|
3465
|
+
value,
|
|
3466
|
+
maskable = false,
|
|
3467
|
+
notify,
|
|
3468
|
+
t
|
|
3469
|
+
}) {
|
|
2476
3470
|
const [done, setDone] = useState(false);
|
|
2477
3471
|
const [revealed, setRevealed] = useState(true);
|
|
2478
3472
|
const copy = async () => {
|
|
2479
3473
|
if (value === "\u2014") return;
|
|
2480
|
-
if (typeof navigator === "undefined" || !navigator.clipboard)
|
|
3474
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) {
|
|
3475
|
+
notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
|
|
3476
|
+
return;
|
|
3477
|
+
}
|
|
2481
3478
|
try {
|
|
2482
3479
|
await navigator.clipboard.writeText(value);
|
|
2483
3480
|
setDone(true);
|
|
2484
3481
|
setTimeout(() => setDone(false), 1100);
|
|
3482
|
+
notify(t("copied"), { tone: "success", id: "credentials-copy" });
|
|
2485
3483
|
} catch {
|
|
3484
|
+
notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
|
|
2486
3485
|
}
|
|
2487
3486
|
};
|
|
2488
3487
|
const hidden = maskable && !revealed && value !== "\u2014";
|
|
@@ -2491,14 +3490,14 @@ function CopyField({ value, ink, maskable = false }) {
|
|
|
2491
3490
|
/* @__PURE__ */ jsxs(
|
|
2492
3491
|
"button",
|
|
2493
3492
|
{
|
|
2494
|
-
onClick: copy,
|
|
3493
|
+
onClick: () => void copy(),
|
|
2495
3494
|
disabled: value === "\u2014",
|
|
2496
3495
|
dir: "ltr",
|
|
2497
3496
|
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
3497
|
style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
|
|
2499
3498
|
children: [
|
|
2500
|
-
/* @__PURE__ */ jsx("span", {
|
|
2501
|
-
value !== "\u2014" && (done ? /* @__PURE__ */ jsx(Icon, { name: "Check", size: 12, className: "qa-text-
|
|
3499
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-hi", children: displayValue }),
|
|
3500
|
+
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
3501
|
]
|
|
2503
3502
|
}
|
|
2504
3503
|
),
|
|
@@ -2517,7 +3516,7 @@ function CopyField({ value, ink, maskable = false }) {
|
|
|
2517
3516
|
] });
|
|
2518
3517
|
}
|
|
2519
3518
|
function CredentialsSection() {
|
|
2520
|
-
const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials,
|
|
3519
|
+
const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, notify } = useQa();
|
|
2521
3520
|
const usedCount = credentials.filter((c) => loginsUsed.has(c.role)).length;
|
|
2522
3521
|
const field = pick2(loginField);
|
|
2523
3522
|
return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2.5", children: [
|
|
@@ -2526,8 +3525,8 @@ function CredentialsSection() {
|
|
|
2526
3525
|
/* @__PURE__ */ jsx(
|
|
2527
3526
|
"span",
|
|
2528
3527
|
{
|
|
2529
|
-
className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium
|
|
2530
|
-
style: { background:
|
|
3528
|
+
className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium",
|
|
3529
|
+
style: { background: "var(--qa-success)", color: "var(--qa-on-accent)" },
|
|
2531
3530
|
children: t("used_count", { n: usedCount, m: credentials.length })
|
|
2532
3531
|
}
|
|
2533
3532
|
)
|
|
@@ -2538,24 +3537,20 @@ function CredentialsSection() {
|
|
|
2538
3537
|
return /* @__PURE__ */ jsxs(
|
|
2539
3538
|
"div",
|
|
2540
3539
|
{
|
|
2541
|
-
className:
|
|
2542
|
-
style: {
|
|
2543
|
-
borderColor: used ? theme.sage : `${theme.primary}14`,
|
|
2544
|
-
background: used ? `${theme.sage}12` : "#fff"
|
|
2545
|
-
},
|
|
3540
|
+
className: `qa-rounded-xl qa-border qa-p-2.5 qa-elev-1 qa-transition ${used ? "qa-bg-success-tint" : "qa-bg-1"}`,
|
|
3541
|
+
style: { borderColor: used ? "var(--qa-success)" : "var(--qa-border-subtle)" },
|
|
2546
3542
|
children: [
|
|
2547
3543
|
/* @__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",
|
|
3544
|
+
/* @__PURE__ */ jsx(Icon, { name: "CircleUser", size: 16, className: "qa-shrink-0 qa-text-accent" }),
|
|
3545
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-sm qa-font-semibold qa-text-hi", children: label }),
|
|
2550
3546
|
c.hint && /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-slate-400", children: pick2(c.hint) }),
|
|
2551
3547
|
/* @__PURE__ */ jsxs(
|
|
2552
3548
|
"button",
|
|
2553
3549
|
{
|
|
2554
3550
|
onClick: () => toggleLogin(c.role),
|
|
2555
3551
|
disabled: !c.seeded,
|
|
2556
|
-
className:
|
|
3552
|
+
className: `qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-text-xs ${used ? "qa-text-success" : "qa-text-lo"}`,
|
|
2557
3553
|
style: {
|
|
2558
|
-
color: used ? theme.sage : "#94a3b8",
|
|
2559
3554
|
background: "transparent",
|
|
2560
3555
|
border: "none",
|
|
2561
3556
|
cursor: c.seeded ? "pointer" : "default"
|
|
@@ -2568,9 +3563,9 @@ function CredentialsSection() {
|
|
|
2568
3563
|
)
|
|
2569
3564
|
] }),
|
|
2570
3565
|
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,
|
|
3566
|
+
/* @__PURE__ */ jsx(CopyField, { value: c.login, notify, t }),
|
|
2572
3567
|
/* @__PURE__ */ jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
|
|
2573
|
-
/* @__PURE__ */ jsx(CopyField, { value: c.password,
|
|
3568
|
+
/* @__PURE__ */ jsx(CopyField, { value: c.password, notify, t, maskable: true })
|
|
2574
3569
|
] })
|
|
2575
3570
|
]
|
|
2576
3571
|
},
|
|
@@ -2580,193 +3575,192 @@ function CredentialsSection() {
|
|
|
2580
3575
|
] });
|
|
2581
3576
|
}
|
|
2582
3577
|
var keyOf = (id, path) => `${id}::${path}`;
|
|
3578
|
+
var DEFAULT_LANE_COLOR = "#4D9CFF";
|
|
2583
3579
|
function Lane({
|
|
2584
3580
|
group,
|
|
2585
3581
|
checked,
|
|
2586
3582
|
toggle,
|
|
2587
3583
|
pick: pick2
|
|
2588
3584
|
}) {
|
|
2589
|
-
const {
|
|
2590
|
-
const { id, color =
|
|
3585
|
+
const { lang, t, guideFailed, evidenceByStep } = useQa();
|
|
3586
|
+
const { id, color = DEFAULT_LANE_COLOR, steps } = group;
|
|
2591
3587
|
const done = steps.filter((s) => checked.has(keyOf(id, s.path))).length;
|
|
2592
3588
|
const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
|
|
2593
3589
|
const uncoveredRedCount = steps.filter(
|
|
2594
3590
|
(s) => s.risk === "red" && !checked.has(keyOf(id, s.path))
|
|
2595
3591
|
).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(
|
|
3592
|
+
return /* @__PURE__ */ jsxs("div", { className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-3 qa-elev-1", children: [
|
|
3593
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-mb-2 qa-flex qa-items-center qa-gap-2", children: [
|
|
3594
|
+
/* @__PURE__ */ jsx(
|
|
3595
|
+
"span",
|
|
3596
|
+
{
|
|
3597
|
+
className: "qa-h-2.5 qa-w-2.5 qa-rounded-full",
|
|
3598
|
+
style: { background: color }
|
|
3599
|
+
}
|
|
3600
|
+
),
|
|
3601
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-sm qa-font-bold qa-text-hi", children: pick2(group.role) }),
|
|
3602
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-ms-auto qa-text-11 qa-font-medium qa-text-slate-400", children: [
|
|
3603
|
+
done,
|
|
3604
|
+
"/",
|
|
3605
|
+
steps.length
|
|
3606
|
+
] }),
|
|
3607
|
+
uncoveredRedCount > 0 && /* @__PURE__ */ jsx(
|
|
3608
|
+
"span",
|
|
3609
|
+
{
|
|
3610
|
+
className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-text-10 qa-font-medium",
|
|
3611
|
+
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)`,
|
|
3612
|
+
children: lang === "ar" ? `\u0623\u062D\u0645\u0631: ${uncoveredRedCount}` : `red: ${uncoveredRedCount}`
|
|
3613
|
+
}
|
|
3614
|
+
)
|
|
3615
|
+
] }),
|
|
3616
|
+
/* @__PURE__ */ jsx(
|
|
3617
|
+
"div",
|
|
3618
|
+
{
|
|
3619
|
+
className: "qa-mb-3 qa-h-1.5 qa-overflow-hidden qa-rounded-full",
|
|
3620
|
+
style: { background: `${color}22` },
|
|
3621
|
+
children: /* @__PURE__ */ jsx(
|
|
2627
3622
|
"div",
|
|
2628
3623
|
{
|
|
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
|
-
)
|
|
3624
|
+
className: "qa-h-full qa-rounded-full qa-transition-all",
|
|
3625
|
+
style: { width: `${pct}%`, background: color }
|
|
2638
3626
|
}
|
|
2639
|
-
)
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
3627
|
+
)
|
|
3628
|
+
}
|
|
3629
|
+
),
|
|
3630
|
+
/* @__PURE__ */ jsxs("ol", { className: "qa-relative qa-ms-1.5", children: [
|
|
3631
|
+
/* @__PURE__ */ jsx(
|
|
3632
|
+
"span",
|
|
3633
|
+
{
|
|
3634
|
+
className: "qa-absolute qa-top-1 qa-bottom-0 qa-w-px",
|
|
3635
|
+
style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
|
|
3636
|
+
}
|
|
3637
|
+
),
|
|
3638
|
+
steps.map((s, i) => {
|
|
3639
|
+
const k = keyOf(id, s.path);
|
|
3640
|
+
const on = checked.has(k);
|
|
3641
|
+
const failed = guideFailed.has(k);
|
|
3642
|
+
const evidence = evidenceByStep.get(k);
|
|
3643
|
+
const evidenceCount = evidence ? evidence.length : 0;
|
|
3644
|
+
const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
|
|
3645
|
+
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;
|
|
3646
|
+
return /* @__PURE__ */ jsx("li", { className: "qa-relative qa-mb-2 qa-last-mb-0", children: /* @__PURE__ */ jsxs(
|
|
3647
|
+
"button",
|
|
3648
|
+
{
|
|
3649
|
+
onClick: () => toggle(k),
|
|
3650
|
+
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" : ""}`,
|
|
3651
|
+
style: { background: failed ? void 0 : "transparent", border: "none", cursor: "pointer" },
|
|
3652
|
+
children: [
|
|
3653
|
+
/* @__PURE__ */ jsxs(
|
|
3654
|
+
"span",
|
|
3655
|
+
{
|
|
3656
|
+
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",
|
|
3657
|
+
style: {
|
|
3658
|
+
borderColor: failed ? "var(--qa-danger)" : color,
|
|
3659
|
+
background: on ? color : failed ? "var(--qa-danger-tint)" : "var(--qa-surface-1)",
|
|
3660
|
+
zIndex: 1
|
|
3661
|
+
},
|
|
3662
|
+
children: [
|
|
3663
|
+
on && /* @__PURE__ */ jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" }),
|
|
3664
|
+
!on && failed && /* @__PURE__ */ jsx(Icon, { name: "AlertTriangle", size: 9, strokeWidth: 2.5, className: "qa-text-danger" })
|
|
3665
|
+
]
|
|
3666
|
+
}
|
|
3667
|
+
),
|
|
3668
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-min-w-0", children: [
|
|
3669
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1", children: [
|
|
2660
3670
|
/* @__PURE__ */ jsx(
|
|
2661
|
-
"
|
|
3671
|
+
"code",
|
|
2662
3672
|
{
|
|
2663
|
-
className: "qa-
|
|
3673
|
+
className: "qa-rounded qa-px-1 qa-text-11 qa-font-semibold qa-dir-ltr qa-text-hi",
|
|
2664
3674
|
style: {
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
3675
|
+
background: failed ? "var(--qa-danger-tint)" : `${color}14`,
|
|
3676
|
+
textDecoration: on ? "line-through" : "none",
|
|
3677
|
+
opacity: on ? 0.55 : 1
|
|
2668
3678
|
},
|
|
2669
|
-
children:
|
|
3679
|
+
children: s.path
|
|
2670
3680
|
}
|
|
2671
3681
|
),
|
|
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
|
-
);
|
|
3682
|
+
/* @__PURE__ */ jsx(
|
|
3683
|
+
"span",
|
|
3684
|
+
{
|
|
3685
|
+
className: "qa-inline-block qa-rounded-full qa-shrink-0",
|
|
3686
|
+
style: {
|
|
3687
|
+
width: "6px",
|
|
3688
|
+
height: "6px",
|
|
3689
|
+
background: riskColor,
|
|
3690
|
+
flexShrink: 0
|
|
3691
|
+
},
|
|
3692
|
+
title: dotTitle
|
|
3693
|
+
}
|
|
3694
|
+
)
|
|
3695
|
+
] }),
|
|
3696
|
+
/* @__PURE__ */ jsx(
|
|
3697
|
+
"span",
|
|
3698
|
+
{
|
|
3699
|
+
className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
|
|
3700
|
+
style: { opacity: on ? 0.5 : 1 },
|
|
3701
|
+
children: pick2(s.what)
|
|
3702
|
+
}
|
|
3703
|
+
),
|
|
3704
|
+
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 }) }),
|
|
3705
|
+
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") })
|
|
3706
|
+
] })
|
|
3707
|
+
]
|
|
3708
|
+
}
|
|
3709
|
+
) }, `${k}-${i}`);
|
|
3710
|
+
})
|
|
3711
|
+
] })
|
|
3712
|
+
] });
|
|
2718
3713
|
}
|
|
2719
3714
|
function GuideSection() {
|
|
2720
|
-
const { guideChecked, toggleGuide, t, journey, pick: pick2,
|
|
3715
|
+
const { guideChecked, toggleGuide, t, journey, pick: pick2, lang, startTestAlong } = useQa();
|
|
2721
3716
|
const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
|
|
2722
3717
|
const done = all.filter((k) => guideChecked.has(k)).length;
|
|
2723
3718
|
const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
|
|
2724
3719
|
const coverage = computeCoverage(journey, guideChecked);
|
|
2725
3720
|
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
|
-
),
|
|
3721
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
|
|
3722
|
+
coverage.red.total > 0 && /* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
|
|
3723
|
+
/* @__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" }),
|
|
3724
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-dir-ltr qa-font-semibold qa-text-hi", children: [
|
|
3725
|
+
coverage.red.covered,
|
|
3726
|
+
"/",
|
|
3727
|
+
coverage.red.total,
|
|
3728
|
+
" ",
|
|
3729
|
+
lang === "ar" ? "\u0645\u063A\u0637\u0649" : "covered"
|
|
3730
|
+
] })
|
|
3731
|
+
] }),
|
|
3732
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-text-sm qa-font-semibold qa-text-hi", children: [
|
|
3733
|
+
/* @__PURE__ */ jsx("span", { children: t("journey_title") }),
|
|
3734
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-dir-ltr", children: [
|
|
3735
|
+
done,
|
|
3736
|
+
"/",
|
|
3737
|
+
all.length,
|
|
3738
|
+
" \xB7 ",
|
|
3739
|
+
pct,
|
|
3740
|
+
"%"
|
|
3741
|
+
] })
|
|
3742
|
+
] }),
|
|
3743
|
+
/* @__PURE__ */ jsx("div", { className: "qa-mt-2 qa-h-2 qa-overflow-hidden qa-rounded-full qa-bg-3", children: /* @__PURE__ */ jsx(
|
|
3744
|
+
"div",
|
|
3745
|
+
{
|
|
3746
|
+
className: "qa-h-full qa-rounded-full qa-bg-accent qa-transition-all",
|
|
3747
|
+
style: { width: `${pct}%` }
|
|
3748
|
+
}
|
|
3749
|
+
) }),
|
|
3750
|
+
/* @__PURE__ */ jsx("div", { className: "qa-mt-2 qa-flex qa-justify-end", children: /* @__PURE__ */ jsxs(
|
|
3751
|
+
"button",
|
|
3752
|
+
{
|
|
3753
|
+
type: "button",
|
|
3754
|
+
onClick: startTestAlong,
|
|
3755
|
+
disabled: all.length === 0,
|
|
3756
|
+
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",
|
|
3757
|
+
children: [
|
|
3758
|
+
/* @__PURE__ */ jsx(Icon, { name: "Play", size: 13 }),
|
|
3759
|
+
t("start_walkthrough")
|
|
3760
|
+
]
|
|
3761
|
+
}
|
|
3762
|
+
) })
|
|
3763
|
+
] }),
|
|
2770
3764
|
journey.map((g) => /* @__PURE__ */ jsx(
|
|
2771
3765
|
Lane,
|
|
2772
3766
|
{
|
|
@@ -2824,13 +3818,13 @@ function QaPanel() {
|
|
|
2824
3818
|
notes,
|
|
2825
3819
|
exportZip,
|
|
2826
3820
|
isExporting,
|
|
2827
|
-
|
|
3821
|
+
clearNotes,
|
|
3822
|
+
startCapture,
|
|
2828
3823
|
t,
|
|
2829
3824
|
lang,
|
|
2830
3825
|
setLang,
|
|
2831
3826
|
dir,
|
|
2832
3827
|
brand,
|
|
2833
|
-
theme,
|
|
2834
3828
|
journey,
|
|
2835
3829
|
guideChecked
|
|
2836
3830
|
} = useQa();
|
|
@@ -2936,6 +3930,14 @@ function QaPanel() {
|
|
|
2936
3930
|
useEffect(() => {
|
|
2937
3931
|
if (!isOpen) setKeyboardLift(0);
|
|
2938
3932
|
}, [isOpen]);
|
|
3933
|
+
useEffect(() => {
|
|
3934
|
+
if (!naming) return void 0;
|
|
3935
|
+
const onKeyDown = (e) => {
|
|
3936
|
+
if (e.key === "Escape") setNaming(false);
|
|
3937
|
+
};
|
|
3938
|
+
document.addEventListener("keydown", onKeyDown);
|
|
3939
|
+
return () => document.removeEventListener("keydown", onKeyDown);
|
|
3940
|
+
}, [naming]);
|
|
2939
3941
|
const keyboardLiftActive = coarse && !isIpadLandscape;
|
|
2940
3942
|
const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
|
|
2941
3943
|
if (phase === "hidden") return null;
|
|
@@ -2957,7 +3959,7 @@ function QaPanel() {
|
|
|
2957
3959
|
"data-qa-overlay": "true",
|
|
2958
3960
|
dir,
|
|
2959
3961
|
onTransitionEnd: handleTransitionEnd,
|
|
2960
|
-
className: `qa-fixed qa-flex qa-flex-col qa-overflow-hidden qa-rounded-2xl qa-border qa-
|
|
3962
|
+
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
3963
|
style: {
|
|
2962
3964
|
// Floating popover position (default). Fully overridden below when
|
|
2963
3965
|
// docked as an iPad-landscape side-sheet.
|
|
@@ -2971,10 +3973,7 @@ function QaPanel() {
|
|
|
2971
3973
|
// full height — neutralize it only in the docked sheet variant.
|
|
2972
3974
|
maxHeight: isIpadLandscape ? "none" : void 0,
|
|
2973
3975
|
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,
|
|
3976
|
+
zIndex: "var(--qa-z-panel)",
|
|
2978
3977
|
// Keyboard-avoidance lift (coarse/touch only — see effect above).
|
|
2979
3978
|
// undefined ⇒ !keyboardLiftActive, so desktop and the iPad-landscape
|
|
2980
3979
|
// side-sheet render this property exactly as before (the class's own
|
|
@@ -2982,156 +3981,141 @@ function QaPanel() {
|
|
|
2982
3981
|
transition: keyboardLiftActive ? PANEL_TRANSITION_WITH_LIFT : void 0
|
|
2983
3982
|
},
|
|
2984
3983
|
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(
|
|
3984
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-px-4 qa-py-3 qa-bg-1", children: [
|
|
3985
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5 qa-dir-ltr", dir: "ltr", children: [
|
|
3986
|
+
/* @__PURE__ */ jsx(
|
|
3987
|
+
"span",
|
|
3988
|
+
{
|
|
3989
|
+
"aria-hidden": "true",
|
|
3990
|
+
className: "qa-shrink-0",
|
|
3991
|
+
style: { width: 6, height: 6, background: "var(--qa-accent)" }
|
|
3992
|
+
}
|
|
3993
|
+
),
|
|
3994
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-hi", style: { fontSize: 13, fontWeight: 600 }, children: brand.label })
|
|
3995
|
+
] }),
|
|
3996
|
+
/* @__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 }),
|
|
3997
|
+
/* @__PURE__ */ jsx(
|
|
3998
|
+
"div",
|
|
3999
|
+
{
|
|
4000
|
+
className: "qa-ms-auto qa-flex qa-items-center qa-overflow-hidden qa-rounded-lg qa-text-11 qa-font-semibold qa-bg-2",
|
|
4001
|
+
dir: "ltr",
|
|
4002
|
+
children: ["en", "ar"].map((l) => /* @__PURE__ */ jsx(
|
|
3025
4003
|
"button",
|
|
3026
4004
|
{
|
|
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",
|
|
4005
|
+
onClick: () => setLang(l),
|
|
4006
|
+
className: `qa-px-2 qa-py-1 qa-transition qa-tap ${lang === l ? "qa-bg-accent" : "qa-bg-transparent qa-text-mid"}`,
|
|
4007
|
+
style: { border: "none", cursor: "pointer" },
|
|
4008
|
+
children: l === "en" ? "EN" : "\u0639"
|
|
4009
|
+
},
|
|
4010
|
+
l
|
|
4011
|
+
))
|
|
4012
|
+
}
|
|
4013
|
+
),
|
|
4014
|
+
/* @__PURE__ */ jsx(
|
|
4015
|
+
"button",
|
|
4016
|
+
{
|
|
4017
|
+
onClick: startCapture,
|
|
4018
|
+
title: t("capture_cta"),
|
|
4019
|
+
"aria-label": t("capture_cta"),
|
|
4020
|
+
className: "qa-tap-icon qa-rounded-lg qa-border qa-border-subtle qa-bg-transparent qa-text-hi qa-hover-bg-2 qa-transition",
|
|
4021
|
+
style: { cursor: "pointer" },
|
|
4022
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 16 })
|
|
4023
|
+
}
|
|
4024
|
+
),
|
|
4025
|
+
/* @__PURE__ */ jsxs(
|
|
4026
|
+
"button",
|
|
4027
|
+
{
|
|
4028
|
+
onClick: openNaming,
|
|
4029
|
+
disabled: !notes.length || isExporting,
|
|
4030
|
+
title: t("export"),
|
|
4031
|
+
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",
|
|
4032
|
+
style: { cursor: "pointer" },
|
|
4033
|
+
children: [
|
|
4034
|
+
/* @__PURE__ */ jsx(
|
|
4035
|
+
Icon,
|
|
3095
4036
|
{
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
children: [
|
|
3100
|
-
/* @__PURE__ */ jsx(Icon, { name: "Trash", size: 12 }),
|
|
3101
|
-
t("clear_all")
|
|
3102
|
-
]
|
|
4037
|
+
name: isExporting ? "Loader2" : "Download",
|
|
4038
|
+
size: 14,
|
|
4039
|
+
className: isExporting ? "qa-animate-spin" : void 0
|
|
3103
4040
|
}
|
|
3104
|
-
)
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
4041
|
+
),
|
|
4042
|
+
t("export")
|
|
4043
|
+
]
|
|
4044
|
+
}
|
|
4045
|
+
)
|
|
4046
|
+
] }),
|
|
4047
|
+
/* @__PURE__ */ jsx(
|
|
4048
|
+
TabsBar,
|
|
4049
|
+
{
|
|
4050
|
+
activeTab,
|
|
4051
|
+
setActiveTab,
|
|
4052
|
+
t,
|
|
4053
|
+
lang
|
|
3109
4054
|
}
|
|
3110
4055
|
),
|
|
4056
|
+
/* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3" }),
|
|
4057
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex-1 qa-space-y-3 qa-overflow-y-auto qa-p-3 qa-bg-0", children: [
|
|
4058
|
+
activeTab === "notes" && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4059
|
+
/* @__PURE__ */ jsx(NoteEditor, {}),
|
|
4060
|
+
/* @__PURE__ */ jsx(NoteList, {}),
|
|
4061
|
+
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: [
|
|
4062
|
+
t("delete_all_q", { n: notes.length }),
|
|
4063
|
+
" ",
|
|
4064
|
+
/* @__PURE__ */ jsx(
|
|
4065
|
+
"button",
|
|
4066
|
+
{
|
|
4067
|
+
onClick: () => {
|
|
4068
|
+
void clearNotes();
|
|
4069
|
+
setConfirmClear(false);
|
|
4070
|
+
},
|
|
4071
|
+
className: "qa-font-semibold qa-text-danger qa-tap",
|
|
4072
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4073
|
+
children: t("yes")
|
|
4074
|
+
}
|
|
4075
|
+
),
|
|
4076
|
+
" / ",
|
|
4077
|
+
/* @__PURE__ */ jsx(
|
|
4078
|
+
"button",
|
|
4079
|
+
{
|
|
4080
|
+
onClick: () => setConfirmClear(false),
|
|
4081
|
+
className: "qa-text-accent qa-tap",
|
|
4082
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4083
|
+
children: t("no")
|
|
4084
|
+
}
|
|
4085
|
+
)
|
|
4086
|
+
] }) : /* @__PURE__ */ jsxs(
|
|
4087
|
+
"button",
|
|
4088
|
+
{
|
|
4089
|
+
onClick: () => setConfirmClear(true),
|
|
4090
|
+
className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-xs qa-text-lo qa-hover-text-red",
|
|
4091
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4092
|
+
children: [
|
|
4093
|
+
/* @__PURE__ */ jsx(Icon, { name: "Trash", size: 12 }),
|
|
4094
|
+
t("clear_all")
|
|
4095
|
+
]
|
|
4096
|
+
}
|
|
4097
|
+
) })
|
|
4098
|
+
] }),
|
|
4099
|
+
activeTab === "logins" && /* @__PURE__ */ jsx(CredentialsSection, {}),
|
|
4100
|
+
activeTab === "guide" && /* @__PURE__ */ jsx(GuideSection, {})
|
|
4101
|
+
] }),
|
|
3111
4102
|
naming && /* @__PURE__ */ jsx(
|
|
3112
4103
|
"div",
|
|
3113
4104
|
{
|
|
3114
4105
|
className: "qa-absolute qa-inset-0 qa-z-50 qa-flex qa-items-center qa-justify-center qa-p-5",
|
|
3115
|
-
style: { background: "
|
|
4106
|
+
style: { background: "var(--qa-scrim-dialog)" },
|
|
4107
|
+
onClick: () => setNaming(false),
|
|
3116
4108
|
children: /* @__PURE__ */ jsxs(
|
|
3117
4109
|
"div",
|
|
3118
4110
|
{
|
|
3119
|
-
className: "qa-w-full qa-rounded-xl qa-border qa-bg-
|
|
3120
|
-
|
|
4111
|
+
className: "qa-w-full qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-4 qa-elev-3",
|
|
4112
|
+
onClick: (e) => e.stopPropagation(),
|
|
3121
4113
|
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
|
-
),
|
|
4114
|
+
/* @__PURE__ */ jsx("p", { className: "qa-mb-2 qa-text-sm qa-font-semibold qa-text-hi", children: t("export_name_title") }),
|
|
3130
4115
|
/* @__PURE__ */ jsxs(
|
|
3131
4116
|
"div",
|
|
3132
4117
|
{
|
|
3133
|
-
className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-dir-ltr",
|
|
3134
|
-
style: { borderColor: `${theme.primary}33` },
|
|
4118
|
+
className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-border-subtle qa-dir-ltr",
|
|
3135
4119
|
children: [
|
|
3136
4120
|
/* @__PURE__ */ jsx(
|
|
3137
4121
|
"input",
|
|
@@ -3141,32 +4125,24 @@ function QaPanel() {
|
|
|
3141
4125
|
onChange: (e) => setFilename(e.target.value),
|
|
3142
4126
|
onKeyDown: (e) => {
|
|
3143
4127
|
if (e.key === "Enter") doExport();
|
|
3144
|
-
if (e.key === "Escape") setNaming(false);
|
|
3145
4128
|
},
|
|
3146
4129
|
placeholder: t("export_name_placeholder"),
|
|
3147
4130
|
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
4131
|
style: { outline: "none", background: "transparent", color: "inherit" }
|
|
3149
4132
|
}
|
|
3150
4133
|
),
|
|
3151
|
-
/* @__PURE__ */ jsx("span", { className: "qa-px-2 qa-text-xs qa-text-
|
|
4134
|
+
/* @__PURE__ */ jsx("span", { className: "qa-px-2 qa-text-xs qa-text-lo", children: ".zip" })
|
|
3152
4135
|
]
|
|
3153
4136
|
}
|
|
3154
4137
|
),
|
|
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
|
-
),
|
|
4138
|
+
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
4139
|
/* @__PURE__ */ jsxs("div", { className: "qa-mt-3 qa-flex qa-gap-2", children: [
|
|
3164
4140
|
/* @__PURE__ */ jsxs(
|
|
3165
4141
|
"button",
|
|
3166
4142
|
{
|
|
3167
4143
|
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: {
|
|
4144
|
+
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",
|
|
4145
|
+
style: { border: "none", cursor: "pointer" },
|
|
3170
4146
|
children: [
|
|
3171
4147
|
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
|
|
3172
4148
|
t("export")
|
|
@@ -3177,13 +4153,8 @@ function QaPanel() {
|
|
|
3177
4153
|
"button",
|
|
3178
4154
|
{
|
|
3179
4155
|
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
|
-
},
|
|
4156
|
+
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",
|
|
4157
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3187
4158
|
children: [
|
|
3188
4159
|
/* @__PURE__ */ jsx(Icon, { name: "X", size: 16 }),
|
|
3189
4160
|
t("cancel")
|
|
@@ -3204,7 +4175,6 @@ function TabsBar({
|
|
|
3204
4175
|
activeTab,
|
|
3205
4176
|
setActiveTab,
|
|
3206
4177
|
t,
|
|
3207
|
-
theme,
|
|
3208
4178
|
lang
|
|
3209
4179
|
}) {
|
|
3210
4180
|
const tabRefs = useRef([]);
|
|
@@ -3228,7 +4198,7 @@ function TabsBar({
|
|
|
3228
4198
|
ro.observe(container);
|
|
3229
4199
|
return () => ro.disconnect();
|
|
3230
4200
|
}, [reposition]);
|
|
3231
|
-
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative", children: [
|
|
4201
|
+
return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative qa-bg-1", children: [
|
|
3232
4202
|
TABS.map((tab, i) => {
|
|
3233
4203
|
const on = activeTab === tab.key;
|
|
3234
4204
|
return /* @__PURE__ */ jsxs(
|
|
@@ -3238,13 +4208,8 @@ function TabsBar({
|
|
|
3238
4208
|
tabRefs.current[i] = el;
|
|
3239
4209
|
},
|
|
3240
4210
|
onClick: () => setActiveTab(tab.key),
|
|
3241
|
-
className:
|
|
3242
|
-
style: {
|
|
3243
|
-
color: on ? theme.primary : "#94a3b8",
|
|
3244
|
-
background: "transparent",
|
|
3245
|
-
border: "none",
|
|
3246
|
-
cursor: "pointer"
|
|
3247
|
-
},
|
|
4211
|
+
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"}`,
|
|
4212
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
3248
4213
|
children: [
|
|
3249
4214
|
/* @__PURE__ */ jsx(Icon, { name: tab.icon, size: 16 }),
|
|
3250
4215
|
t(tab.labelKey)
|
|
@@ -3258,20 +4223,265 @@ function TabsBar({
|
|
|
3258
4223
|
{
|
|
3259
4224
|
ref: barRef,
|
|
3260
4225
|
className: "qa-tab-indicator",
|
|
3261
|
-
style: { background:
|
|
4226
|
+
style: { background: "var(--qa-accent)" },
|
|
3262
4227
|
"aria-hidden": "true"
|
|
3263
4228
|
}
|
|
3264
4229
|
)
|
|
3265
4230
|
] });
|
|
3266
4231
|
}
|
|
4232
|
+
function toneIcon(tone) {
|
|
4233
|
+
switch (tone) {
|
|
4234
|
+
case "success":
|
|
4235
|
+
return { name: "Check", colorClass: "qa-text-success" };
|
|
4236
|
+
case "error":
|
|
4237
|
+
return { name: "AlertTriangle", colorClass: "qa-text-danger" };
|
|
4238
|
+
default:
|
|
4239
|
+
return { name: "X", colorClass: "qa-text-accent" };
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
function Toast({ notice }) {
|
|
4243
|
+
const { dismissNotice } = useQa();
|
|
4244
|
+
const icon = toneIcon(notice.tone);
|
|
4245
|
+
return /* @__PURE__ */ jsxs(
|
|
4246
|
+
"div",
|
|
4247
|
+
{
|
|
4248
|
+
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",
|
|
4249
|
+
style: { fontSize: 13 },
|
|
4250
|
+
children: [
|
|
4251
|
+
/* @__PURE__ */ jsx(Icon, { name: icon.name, size: 16, className: `qa-shrink-0 ${icon.colorClass}` }),
|
|
4252
|
+
/* @__PURE__ */ jsx("span", { className: "qa-min-w-0 qa-flex-1", children: notice.message }),
|
|
4253
|
+
notice.action && /* @__PURE__ */ jsx(
|
|
4254
|
+
"button",
|
|
4255
|
+
{
|
|
4256
|
+
type: "button",
|
|
4257
|
+
onClick: () => {
|
|
4258
|
+
notice.action?.onAction();
|
|
4259
|
+
dismissNotice(notice.id);
|
|
4260
|
+
},
|
|
4261
|
+
className: "qa-tap qa-text-accent qa-focus-ring qa-shrink-0 qa-rounded qa-px-2 qa-py-1 qa-font-semibold",
|
|
4262
|
+
style: { background: "transparent", border: "none", cursor: "pointer", fontSize: 13, pointerEvents: "auto" },
|
|
4263
|
+
children: notice.action.label
|
|
4264
|
+
}
|
|
4265
|
+
)
|
|
4266
|
+
]
|
|
4267
|
+
}
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
function NoticeHost() {
|
|
4271
|
+
const { notices, dir } = useQa();
|
|
4272
|
+
const politeNotices = notices.filter((n) => n.tone !== "error");
|
|
4273
|
+
const errorNotices = notices.filter((n) => n.tone === "error");
|
|
4274
|
+
return /* @__PURE__ */ jsxs(
|
|
4275
|
+
"div",
|
|
4276
|
+
{
|
|
4277
|
+
"data-qa-overlay": "true",
|
|
4278
|
+
dir,
|
|
4279
|
+
className: "qa-toast-viewport qa-print-hidden qa-fixed",
|
|
4280
|
+
style: { zIndex: "var(--qa-z-toast)", pointerEvents: "none" },
|
|
4281
|
+
children: [
|
|
4282
|
+
/* @__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)) }),
|
|
4283
|
+
/* @__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)) })
|
|
4284
|
+
]
|
|
4285
|
+
}
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
function TestAlongHud() {
|
|
4289
|
+
const {
|
|
4290
|
+
dir,
|
|
4291
|
+
t,
|
|
4292
|
+
pick: pick2,
|
|
4293
|
+
testAlong,
|
|
4294
|
+
testAlongSteps,
|
|
4295
|
+
gotoStep,
|
|
4296
|
+
gradeStep,
|
|
4297
|
+
exitTestAlong,
|
|
4298
|
+
startCapture,
|
|
4299
|
+
evidenceByStep
|
|
4300
|
+
} = useQa();
|
|
4301
|
+
const index = testAlong.index;
|
|
4302
|
+
const steps = testAlongSteps;
|
|
4303
|
+
const currentStep = steps[index];
|
|
4304
|
+
const backIcon = dir === "rtl" ? "ChevronRight" : "ChevronLeft";
|
|
4305
|
+
const nextIcon = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
|
|
4306
|
+
useEffect(() => {
|
|
4307
|
+
const step = steps[index];
|
|
4308
|
+
if (!step) return;
|
|
4309
|
+
const notesForStep = evidenceByStep.get(step.key);
|
|
4310
|
+
if (!notesForStep || notesForStep.length === 0) return;
|
|
4311
|
+
const latest = notesForStep[notesForStep.length - 1];
|
|
4312
|
+
if (latest.target) flashLocate(latest.target);
|
|
4313
|
+
}, [testAlong.index]);
|
|
4314
|
+
if (!testAlong.active) return null;
|
|
4315
|
+
const atFirst = index <= 0;
|
|
4316
|
+
const atLast = index >= steps.length - 1;
|
|
4317
|
+
const riskColor = RISK_COLORS[currentStep?.risk ?? "green"];
|
|
4318
|
+
return /* @__PURE__ */ jsx(
|
|
4319
|
+
"div",
|
|
4320
|
+
{
|
|
4321
|
+
"data-qa-overlay": "true",
|
|
4322
|
+
dir,
|
|
4323
|
+
className: "qa-fixed qa-print-hidden",
|
|
4324
|
+
style: {
|
|
4325
|
+
left: "env(safe-area-inset-left)",
|
|
4326
|
+
right: "env(safe-area-inset-right)",
|
|
4327
|
+
bottom: "env(safe-area-inset-bottom)",
|
|
4328
|
+
zIndex: "var(--qa-z-panel)",
|
|
4329
|
+
padding: "0.75rem",
|
|
4330
|
+
pointerEvents: "none"
|
|
4331
|
+
},
|
|
4332
|
+
children: /* @__PURE__ */ jsxs(
|
|
4333
|
+
"div",
|
|
4334
|
+
{
|
|
4335
|
+
role: "region",
|
|
4336
|
+
"aria-label": t("journey_title"),
|
|
4337
|
+
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",
|
|
4338
|
+
style: { maxWidth: "32rem", marginInline: "auto", pointerEvents: "auto" },
|
|
4339
|
+
children: [
|
|
4340
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
4341
|
+
/* @__PURE__ */ jsx(
|
|
4342
|
+
"span",
|
|
4343
|
+
{
|
|
4344
|
+
"aria-hidden": "true",
|
|
4345
|
+
className: "qa-shrink-0 qa-rounded-full",
|
|
4346
|
+
style: { width: 8, height: 8, backgroundColor: riskColor }
|
|
4347
|
+
}
|
|
4348
|
+
),
|
|
4349
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-mid", children: t("step_of", { n: index + 1, m: steps.length }) }),
|
|
4350
|
+
/* @__PURE__ */ jsxs(
|
|
4351
|
+
"button",
|
|
4352
|
+
{
|
|
4353
|
+
type: "button",
|
|
4354
|
+
onClick: exitTestAlong,
|
|
4355
|
+
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",
|
|
4356
|
+
style: { background: "transparent", border: "none", cursor: "pointer" },
|
|
4357
|
+
children: [
|
|
4358
|
+
/* @__PURE__ */ jsx(Icon, { name: "X", size: 14 }),
|
|
4359
|
+
t("exit_walkthrough")
|
|
4360
|
+
]
|
|
4361
|
+
}
|
|
4362
|
+
)
|
|
4363
|
+
] }),
|
|
4364
|
+
currentStep && /* @__PURE__ */ jsxs("div", { className: "qa-min-w-0", children: [
|
|
4365
|
+
/* @__PURE__ */ jsx("p", { className: "qa-text-sm qa-font-medium qa-text-hi qa-break-words", children: pick2(currentStep.what) }),
|
|
4366
|
+
currentStep.expect && /* @__PURE__ */ jsxs("p", { className: "qa-text-11 qa-text-mid qa-mt-1 qa-break-words", children: [
|
|
4367
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-font-semibold", children: [
|
|
4368
|
+
t("expected_label"),
|
|
4369
|
+
": "
|
|
4370
|
+
] }),
|
|
4371
|
+
pick2(currentStep.expect)
|
|
4372
|
+
] })
|
|
4373
|
+
] }),
|
|
4374
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-flex-wrap qa-items-center qa-gap-2", children: [
|
|
4375
|
+
/* @__PURE__ */ jsxs(
|
|
4376
|
+
"button",
|
|
4377
|
+
{
|
|
4378
|
+
type: "button",
|
|
4379
|
+
onClick: () => gotoStep(index - 1),
|
|
4380
|
+
disabled: atFirst,
|
|
4381
|
+
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",
|
|
4382
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4383
|
+
children: [
|
|
4384
|
+
/* @__PURE__ */ jsx(Icon, { name: backIcon, size: 14 }),
|
|
4385
|
+
t("prev_step")
|
|
4386
|
+
]
|
|
4387
|
+
}
|
|
4388
|
+
),
|
|
4389
|
+
/* @__PURE__ */ jsxs(
|
|
4390
|
+
"button",
|
|
4391
|
+
{
|
|
4392
|
+
type: "button",
|
|
4393
|
+
onClick: () => currentStep && gradeStep(currentStep.key, "fail"),
|
|
4394
|
+
disabled: !currentStep,
|
|
4395
|
+
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",
|
|
4396
|
+
style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
|
|
4397
|
+
children: [
|
|
4398
|
+
/* @__PURE__ */ jsx(Icon, { name: "AlertTriangle", size: 14 }),
|
|
4399
|
+
t("mark_fail")
|
|
4400
|
+
]
|
|
4401
|
+
}
|
|
4402
|
+
),
|
|
4403
|
+
/* @__PURE__ */ jsxs(
|
|
4404
|
+
"button",
|
|
4405
|
+
{
|
|
4406
|
+
type: "button",
|
|
4407
|
+
onClick: () => currentStep && gradeStep(currentStep.key, "pass"),
|
|
4408
|
+
disabled: !currentStep,
|
|
4409
|
+
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",
|
|
4410
|
+
style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
|
|
4411
|
+
children: [
|
|
4412
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 14 }),
|
|
4413
|
+
t("mark_pass")
|
|
4414
|
+
]
|
|
4415
|
+
}
|
|
4416
|
+
),
|
|
4417
|
+
/* @__PURE__ */ jsxs(
|
|
4418
|
+
"button",
|
|
4419
|
+
{
|
|
4420
|
+
type: "button",
|
|
4421
|
+
onClick: () => startCapture(),
|
|
4422
|
+
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",
|
|
4423
|
+
style: { border: "none", cursor: "pointer", minWidth: "6rem" },
|
|
4424
|
+
children: [
|
|
4425
|
+
/* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 14 }),
|
|
4426
|
+
t("capture_here")
|
|
4427
|
+
]
|
|
4428
|
+
}
|
|
4429
|
+
),
|
|
4430
|
+
/* @__PURE__ */ jsxs(
|
|
4431
|
+
"button",
|
|
4432
|
+
{
|
|
4433
|
+
type: "button",
|
|
4434
|
+
onClick: () => gotoStep(index + 1),
|
|
4435
|
+
disabled: atLast,
|
|
4436
|
+
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",
|
|
4437
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4438
|
+
children: [
|
|
4439
|
+
t("next_step"),
|
|
4440
|
+
/* @__PURE__ */ jsx(Icon, { name: nextIcon, size: 14 })
|
|
4441
|
+
]
|
|
4442
|
+
}
|
|
4443
|
+
)
|
|
4444
|
+
] })
|
|
4445
|
+
]
|
|
4446
|
+
}
|
|
4447
|
+
)
|
|
4448
|
+
}
|
|
4449
|
+
);
|
|
4450
|
+
}
|
|
3267
4451
|
|
|
3268
4452
|
// src/lib/capture.ts
|
|
3269
4453
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
4454
|
+
var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
3270
4455
|
function withTimeout(promise, ms) {
|
|
4456
|
+
let timer;
|
|
3271
4457
|
return Promise.race([
|
|
3272
4458
|
promise,
|
|
3273
|
-
new Promise((resolve) =>
|
|
3274
|
-
|
|
4459
|
+
new Promise((resolve) => {
|
|
4460
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
4461
|
+
})
|
|
4462
|
+
]).finally(() => {
|
|
4463
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
function isTransparent(color) {
|
|
4467
|
+
const c = (color || "").trim().toLowerCase();
|
|
4468
|
+
if (!c || c === "transparent") return true;
|
|
4469
|
+
const m = c.match(/^rgba?\(([^)]+)\)$/);
|
|
4470
|
+
if (!m) return false;
|
|
4471
|
+
const parts = m[1].split(/[,/\s]+/).filter(Boolean);
|
|
4472
|
+
return parts.length >= 4 && parseFloat(parts[3]) === 0;
|
|
4473
|
+
}
|
|
4474
|
+
function resolvePageBackground() {
|
|
4475
|
+
if (typeof getComputedStyle !== "function") return FALLBACK_PAGE_BACKGROUND;
|
|
4476
|
+
for (const el of [document.body, document.documentElement]) {
|
|
4477
|
+
if (!el) continue;
|
|
4478
|
+
try {
|
|
4479
|
+
const bg = getComputedStyle(el).backgroundColor;
|
|
4480
|
+
if (!isTransparent(bg)) return bg;
|
|
4481
|
+
} catch {
|
|
4482
|
+
}
|
|
4483
|
+
}
|
|
4484
|
+
return FALLBACK_PAGE_BACKGROUND;
|
|
3275
4485
|
}
|
|
3276
4486
|
function toBlob(canvas) {
|
|
3277
4487
|
return new Promise((resolve) => {
|
|
@@ -3287,8 +4497,10 @@ function toBlob(canvas) {
|
|
|
3287
4497
|
});
|
|
3288
4498
|
}
|
|
3289
4499
|
async function captureRegion(rect, scroll) {
|
|
3290
|
-
if (typeof document === "undefined" || typeof window === "undefined")
|
|
3291
|
-
|
|
4500
|
+
if (typeof document === "undefined" || typeof window === "undefined") {
|
|
4501
|
+
return { status: "empty" };
|
|
4502
|
+
}
|
|
4503
|
+
if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
|
|
3292
4504
|
const sx = scroll?.x ?? window.scrollX;
|
|
3293
4505
|
const sy = scroll?.y ?? window.scrollY;
|
|
3294
4506
|
try {
|
|
@@ -3303,7 +4515,8 @@ async function captureRegion(rect, scroll) {
|
|
|
3303
4515
|
scale,
|
|
3304
4516
|
useCORS: true,
|
|
3305
4517
|
allowTaint: true,
|
|
3306
|
-
|
|
4518
|
+
// The page's own background, never null — see resolvePageBackground().
|
|
4519
|
+
backgroundColor: resolvePageBackground(),
|
|
3307
4520
|
logging: false,
|
|
3308
4521
|
scrollX: sx,
|
|
3309
4522
|
scrollY: sy,
|
|
@@ -3315,11 +4528,12 @@ async function captureRegion(rect, scroll) {
|
|
|
3315
4528
|
}),
|
|
3316
4529
|
HTML2CANVAS_TIMEOUT_MS
|
|
3317
4530
|
);
|
|
3318
|
-
if (!canvas) return
|
|
3319
|
-
|
|
4531
|
+
if (!canvas) return { status: "failed" };
|
|
4532
|
+
const blob = await toBlob(canvas);
|
|
4533
|
+
return blob ? { status: "ok", blob } : { status: "failed" };
|
|
3320
4534
|
} catch (err) {
|
|
3321
4535
|
console.warn("[QA] region capture failed:", err);
|
|
3322
|
-
return
|
|
4536
|
+
return { status: "failed" };
|
|
3323
4537
|
}
|
|
3324
4538
|
}
|
|
3325
4539
|
|
|
@@ -3423,6 +4637,26 @@ function unlockPageScroll() {
|
|
|
3423
4637
|
var DRAG_THRESHOLD2 = 6;
|
|
3424
4638
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
3425
4639
|
var MIN_REGION_SIZE = 8;
|
|
4640
|
+
var SEVERITIES2 = ["bug", "question", "polish"];
|
|
4641
|
+
var SEVERITY_ICON = {
|
|
4642
|
+
bug: "Bug",
|
|
4643
|
+
question: "AlertTriangle",
|
|
4644
|
+
polish: "Pencil"
|
|
4645
|
+
};
|
|
4646
|
+
var SEVERITY_LABEL_KEY2 = {
|
|
4647
|
+
bug: "sev_bug",
|
|
4648
|
+
question: "sev_question",
|
|
4649
|
+
polish: "sev_polish"
|
|
4650
|
+
};
|
|
4651
|
+
function clampRegionRect(rect) {
|
|
4652
|
+
const vw = typeof window !== "undefined" ? window.innerWidth : rect.left + rect.width;
|
|
4653
|
+
const vh = typeof window !== "undefined" ? window.innerHeight : rect.top + rect.height;
|
|
4654
|
+
const width = Math.min(Math.max(MIN_REGION_SIZE, rect.width), Math.max(MIN_REGION_SIZE, vw));
|
|
4655
|
+
const height = Math.min(Math.max(MIN_REGION_SIZE, rect.height), Math.max(MIN_REGION_SIZE, vh));
|
|
4656
|
+
const left = Math.min(Math.max(0, rect.left), Math.max(0, vw - width));
|
|
4657
|
+
const top = Math.min(Math.max(0, rect.top), Math.max(0, vh - height));
|
|
4658
|
+
return { top, left, width, height };
|
|
4659
|
+
}
|
|
3426
4660
|
var REGION_HANDLES = [
|
|
3427
4661
|
{ edge: "nw", top: "0%", left: "0%", cursor: "nwse-resize" },
|
|
3428
4662
|
{ edge: "n", top: "0%", left: "50%", cursor: "ns-resize" },
|
|
@@ -3434,7 +4668,7 @@ var REGION_HANDLES = [
|
|
|
3434
4668
|
{ edge: "se", top: "100%", left: "100%", cursor: "nwse-resize" }
|
|
3435
4669
|
];
|
|
3436
4670
|
function CaptureMode() {
|
|
3437
|
-
const { addNote, endCapture, t, dir
|
|
4671
|
+
const { addNote, endCapture, t, dir } = useQa();
|
|
3438
4672
|
const coarse = useCoarsePointer();
|
|
3439
4673
|
const layerRef = useRef(null);
|
|
3440
4674
|
const overlayRootRef = useRef(null);
|
|
@@ -3448,7 +4682,10 @@ function CaptureMode() {
|
|
|
3448
4682
|
const [shot, setShot] = useState(null);
|
|
3449
4683
|
const [shotUrl, setShotUrl] = useState(null);
|
|
3450
4684
|
const [capturing, setCapturing] = useState(false);
|
|
4685
|
+
const [captureError, setCaptureError] = useState(false);
|
|
3451
4686
|
const [description, setDescription] = useState("");
|
|
4687
|
+
const [severity, setSeverity] = useState("bug");
|
|
4688
|
+
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
3452
4689
|
const taRef = useRef(null);
|
|
3453
4690
|
const activePointerId = useRef(null);
|
|
3454
4691
|
const pointerKind = useRef("mouse");
|
|
@@ -3469,31 +4706,39 @@ function CaptureMode() {
|
|
|
3469
4706
|
if (!el || el.closest?.("[data-qa-overlay]")) return null;
|
|
3470
4707
|
return el;
|
|
3471
4708
|
}, []);
|
|
3472
|
-
const
|
|
3473
|
-
setSelection(sel);
|
|
3474
|
-
setCandidate(null);
|
|
3475
|
-
setHover(null);
|
|
3476
|
-
setRegionMode(false);
|
|
3477
|
-
setPhase("annotating");
|
|
3478
|
-
setCardIn(false);
|
|
4709
|
+
const runCapture = useCallback(async (rect) => {
|
|
3479
4710
|
setCapturing(true);
|
|
4711
|
+
setCaptureError(false);
|
|
3480
4712
|
lockPageScroll();
|
|
3481
4713
|
try {
|
|
3482
|
-
const
|
|
4714
|
+
const outcome = await captureRegion(rect, scrollSnap.current);
|
|
4715
|
+
const blob = outcome.status === "ok" ? outcome.blob : null;
|
|
4716
|
+
const url = blob ? URL.createObjectURL(blob) : null;
|
|
3483
4717
|
if (!mountedRef.current) {
|
|
3484
|
-
if (
|
|
4718
|
+
if (url) URL.revokeObjectURL(url);
|
|
3485
4719
|
return;
|
|
3486
4720
|
}
|
|
4721
|
+
setCaptureError(outcome.status === "failed");
|
|
3487
4722
|
setShot(blob);
|
|
3488
4723
|
setShotUrl((old) => {
|
|
3489
4724
|
if (old) URL.revokeObjectURL(old);
|
|
3490
|
-
return
|
|
4725
|
+
return url;
|
|
3491
4726
|
});
|
|
3492
4727
|
} finally {
|
|
3493
4728
|
unlockPageScroll();
|
|
3494
4729
|
if (mountedRef.current) setCapturing(false);
|
|
3495
4730
|
}
|
|
3496
4731
|
}, []);
|
|
4732
|
+
const beginAnnotation = useCallback(async (sel) => {
|
|
4733
|
+
setSelection(sel);
|
|
4734
|
+
setCandidate(null);
|
|
4735
|
+
setHover(null);
|
|
4736
|
+
setRegionMode(false);
|
|
4737
|
+
setSeverity("bug");
|
|
4738
|
+
setPhase("annotating");
|
|
4739
|
+
setCardIn(false);
|
|
4740
|
+
await runCapture(sel.rect);
|
|
4741
|
+
}, [runCapture]);
|
|
3497
4742
|
useEffect(() => {
|
|
3498
4743
|
if (phase !== "annotating") {
|
|
3499
4744
|
setCardIn(false);
|
|
@@ -3552,42 +4797,50 @@ function CaptureMode() {
|
|
|
3552
4797
|
activePointerId.current = null;
|
|
3553
4798
|
const d = dragRef.current;
|
|
3554
4799
|
dragRef.current = null;
|
|
4800
|
+
setDrag(null);
|
|
3555
4801
|
const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD2 : TOUCH_DRAG_THRESHOLD;
|
|
3556
4802
|
const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
|
|
3557
4803
|
scrollSnap.current = { x: window.scrollX, y: window.scrollY };
|
|
4804
|
+
let regionRect = null;
|
|
3558
4805
|
if (moved && d) {
|
|
3559
|
-
const
|
|
4806
|
+
const rawRect = {
|
|
3560
4807
|
left: Math.min(d.x0, e.clientX),
|
|
3561
4808
|
top: Math.min(d.y0, e.clientY),
|
|
3562
4809
|
width: Math.abs(e.clientX - d.x0),
|
|
3563
4810
|
height: Math.abs(e.clientY - d.y0)
|
|
3564
4811
|
};
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
if (coarse) {
|
|
3568
|
-
setCandidate(sel);
|
|
3569
|
-
setPhase("confirming");
|
|
3570
|
-
} else {
|
|
3571
|
-
void beginAnnotation(sel);
|
|
4812
|
+
if (rawRect.width >= MIN_REGION_SIZE || rawRect.height >= MIN_REGION_SIZE) {
|
|
4813
|
+
regionRect = clampRegionRect(rawRect);
|
|
3572
4814
|
}
|
|
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
|
-
};
|
|
4815
|
+
}
|
|
4816
|
+
if (regionRect) {
|
|
4817
|
+
const sel2 = { kind: "region", rect: regionRect };
|
|
4818
|
+
setTargetForensics(void 0);
|
|
3584
4819
|
if (coarse) {
|
|
3585
|
-
setCandidate(
|
|
3586
|
-
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
4820
|
+
setCandidate(sel2);
|
|
3587
4821
|
setPhase("confirming");
|
|
3588
4822
|
} else {
|
|
3589
|
-
void beginAnnotation(
|
|
4823
|
+
void beginAnnotation(sel2);
|
|
3590
4824
|
}
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
const el = elementUnder(e.clientX, e.clientY);
|
|
4828
|
+
if (!el) return;
|
|
4829
|
+
const r = el.getBoundingClientRect();
|
|
4830
|
+
const sel = {
|
|
4831
|
+
kind: "element",
|
|
4832
|
+
rect: { top: r.top, left: r.left, width: r.width, height: r.height },
|
|
4833
|
+
selector: getStableSelector(el),
|
|
4834
|
+
text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
|
|
4835
|
+
tagName: el.tagName.toLowerCase()
|
|
4836
|
+
};
|
|
4837
|
+
setTargetForensics(collectTargetForensics(el));
|
|
4838
|
+
if (coarse) {
|
|
4839
|
+
setCandidate(sel);
|
|
4840
|
+
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
4841
|
+
setPhase("confirming");
|
|
4842
|
+
} else {
|
|
4843
|
+
void beginAnnotation(sel);
|
|
3591
4844
|
}
|
|
3592
4845
|
};
|
|
3593
4846
|
const onPointerCancel = (e) => {
|
|
@@ -3622,23 +4875,29 @@ function CaptureMode() {
|
|
|
3622
4875
|
const dx = e.clientX - hd.startX;
|
|
3623
4876
|
const dy = e.clientY - hd.startY;
|
|
3624
4877
|
const { startRect, edge } = hd;
|
|
3625
|
-
let
|
|
4878
|
+
let top = startRect.top;
|
|
4879
|
+
let left = startRect.left;
|
|
4880
|
+
let right = startRect.left + startRect.width;
|
|
4881
|
+
let bottom = startRect.top + startRect.height;
|
|
3626
4882
|
if (edge === "move") {
|
|
3627
4883
|
left = startRect.left + dx;
|
|
3628
4884
|
top = startRect.top + dy;
|
|
4885
|
+
right = left + startRect.width;
|
|
4886
|
+
bottom = top + startRect.height;
|
|
3629
4887
|
} 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
|
-
}
|
|
4888
|
+
if (edge.includes("e")) right += dx;
|
|
4889
|
+
if (edge.includes("w")) left += dx;
|
|
4890
|
+
if (edge.includes("s")) bottom += dy;
|
|
4891
|
+
if (edge.includes("n")) top += dy;
|
|
3640
4892
|
}
|
|
3641
|
-
|
|
4893
|
+
const rawRect = {
|
|
4894
|
+
left: Math.min(left, right),
|
|
4895
|
+
top: Math.min(top, bottom),
|
|
4896
|
+
width: Math.abs(right - left),
|
|
4897
|
+
height: Math.abs(bottom - top)
|
|
4898
|
+
};
|
|
4899
|
+
const rect = clampRegionRect(rawRect);
|
|
4900
|
+
setCandidate((prev) => prev ? { ...prev, rect } : prev);
|
|
3642
4901
|
}, []);
|
|
3643
4902
|
const onHandlePointerUp = useCallback((e) => {
|
|
3644
4903
|
const hd = handleDragRef.current;
|
|
@@ -3675,7 +4934,8 @@ function CaptureMode() {
|
|
|
3675
4934
|
}
|
|
3676
4935
|
const first = focusable[0];
|
|
3677
4936
|
const last = focusable[focusable.length - 1];
|
|
3678
|
-
const
|
|
4937
|
+
const rootNode = root.getRootNode();
|
|
4938
|
+
const active = rootNode.activeElement;
|
|
3679
4939
|
const activeInside = !!active && root.contains(active);
|
|
3680
4940
|
if (e.shiftKey) {
|
|
3681
4941
|
if (!activeInside || active === first) {
|
|
@@ -3716,21 +4976,31 @@ function CaptureMode() {
|
|
|
3716
4976
|
},
|
|
3717
4977
|
scroll: { ...scrollSnap.current }
|
|
3718
4978
|
};
|
|
3719
|
-
await addNote({
|
|
4979
|
+
await addNote({
|
|
4980
|
+
description,
|
|
4981
|
+
screenshot: shot ?? void 0,
|
|
4982
|
+
target,
|
|
4983
|
+
severity,
|
|
4984
|
+
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
4985
|
+
});
|
|
3720
4986
|
endCapture();
|
|
3721
4987
|
};
|
|
3722
4988
|
const popStyleFor = useCallback((r) => {
|
|
3723
4989
|
if (typeof window === "undefined") return {};
|
|
3724
|
-
const
|
|
3725
|
-
const
|
|
3726
|
-
const
|
|
4990
|
+
const margin = 12;
|
|
4991
|
+
const spaceBelow = window.innerHeight - (r.top + r.height + margin);
|
|
4992
|
+
const spaceAbove = r.top - margin;
|
|
4993
|
+
const placeAbove = spaceBelow < spaceAbove;
|
|
4994
|
+
const top = placeAbove ? Math.max(margin, r.top - margin) : r.top + r.height + margin;
|
|
3727
4995
|
let left = r.left;
|
|
3728
4996
|
left = Math.min(left, window.innerWidth - 340);
|
|
3729
|
-
left = Math.max(
|
|
4997
|
+
left = Math.max(margin, left);
|
|
4998
|
+
const available = (placeAbove ? spaceAbove : spaceBelow) - margin;
|
|
3730
4999
|
return {
|
|
3731
5000
|
top,
|
|
3732
5001
|
left,
|
|
3733
|
-
transform: placeAbove ? "translateY(-100%)" : "none"
|
|
5002
|
+
transform: placeAbove ? "translateY(-100%)" : "none",
|
|
5003
|
+
maxHeight: `max(${margin * 4}px, ${Math.max(0, available)}px)`
|
|
3734
5004
|
};
|
|
3735
5005
|
}, []);
|
|
3736
5006
|
const popStyle = selection ? popStyleFor(selection.rect) : {};
|
|
@@ -3738,7 +5008,7 @@ function CaptureMode() {
|
|
|
3738
5008
|
const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
|
|
3739
5009
|
const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
|
|
3740
5010
|
const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
|
|
3741
|
-
return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", ref: overlayRootRef, children: [
|
|
5011
|
+
return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
|
|
3742
5012
|
/* @__PURE__ */ jsx(
|
|
3743
5013
|
"div",
|
|
3744
5014
|
{
|
|
@@ -3751,15 +5021,14 @@ function CaptureMode() {
|
|
|
3751
5021
|
style: {
|
|
3752
5022
|
cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
|
|
3753
5023
|
touchAction: coarse ? "none" : "auto",
|
|
3754
|
-
background: "
|
|
5024
|
+
background: "var(--qa-scrim-capture)"
|
|
3755
5025
|
}
|
|
3756
5026
|
}
|
|
3757
5027
|
),
|
|
3758
5028
|
phase === "selecting" && !coarse && /* @__PURE__ */ jsxs(
|
|
3759
5029
|
"div",
|
|
3760
5030
|
{
|
|
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 },
|
|
5031
|
+
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
5032
|
children: [
|
|
3764
5033
|
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3765
5034
|
/* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
@@ -3774,8 +5043,8 @@ function CaptureMode() {
|
|
|
3774
5043
|
"button",
|
|
3775
5044
|
{
|
|
3776
5045
|
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",
|
|
5046
|
+
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",
|
|
5047
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3779
5048
|
children: "Esc"
|
|
3780
5049
|
}
|
|
3781
5050
|
)
|
|
@@ -3785,8 +5054,7 @@ function CaptureMode() {
|
|
|
3785
5054
|
phase === "selecting" && coarse && /* @__PURE__ */ jsxs(
|
|
3786
5055
|
"div",
|
|
3787
5056
|
{
|
|
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 },
|
|
5057
|
+
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
5058
|
children: [
|
|
3791
5059
|
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
|
|
3792
5060
|
/* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
|
|
@@ -3815,8 +5083,8 @@ function CaptureMode() {
|
|
|
3815
5083
|
"button",
|
|
3816
5084
|
{
|
|
3817
5085
|
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",
|
|
5086
|
+
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",
|
|
5087
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3820
5088
|
children: "Esc"
|
|
3821
5089
|
}
|
|
3822
5090
|
)
|
|
@@ -3833,10 +5101,10 @@ function CaptureMode() {
|
|
|
3833
5101
|
width: activeRect.width,
|
|
3834
5102
|
height: activeRect.height,
|
|
3835
5103
|
pointerEvents: confirmingRegion ? "auto" : "none",
|
|
3836
|
-
outline: `2px ${isRegion ? "dashed" : "solid"}
|
|
5104
|
+
outline: `2px ${isRegion ? "dashed" : "solid"} var(--qa-accent)`,
|
|
3837
5105
|
outlineOffset: "1px",
|
|
3838
|
-
background:
|
|
3839
|
-
boxShadow: phase === "annotating" ? "0 0 0 9999px
|
|
5106
|
+
background: "var(--qa-accent-tint)",
|
|
5107
|
+
boxShadow: phase === "annotating" ? "0 0 0 9999px var(--qa-scrim-spot)" : "none"
|
|
3840
5108
|
},
|
|
3841
5109
|
children: [
|
|
3842
5110
|
(phase === "selecting" || phase === "confirming") && hover?.selector && !drag && /* @__PURE__ */ jsx(
|
|
@@ -3847,7 +5115,7 @@ function CaptureMode() {
|
|
|
3847
5115
|
top: "-1.5rem",
|
|
3848
5116
|
left: 0,
|
|
3849
5117
|
maxWidth: "260px",
|
|
3850
|
-
background:
|
|
5118
|
+
background: "var(--qa-surface-3)"
|
|
3851
5119
|
},
|
|
3852
5120
|
children: hover.selector
|
|
3853
5121
|
}
|
|
@@ -3859,7 +5127,7 @@ function CaptureMode() {
|
|
|
3859
5127
|
style: {
|
|
3860
5128
|
bottom: "-1.5rem",
|
|
3861
5129
|
right: 0,
|
|
3862
|
-
background:
|
|
5130
|
+
background: "var(--qa-accent-active)"
|
|
3863
5131
|
},
|
|
3864
5132
|
children: [
|
|
3865
5133
|
Math.round(drag.rect.width),
|
|
@@ -3896,7 +5164,7 @@ function CaptureMode() {
|
|
|
3896
5164
|
transform: "translate(-50%, -50%)",
|
|
3897
5165
|
touchAction: "none",
|
|
3898
5166
|
cursor,
|
|
3899
|
-
background:
|
|
5167
|
+
background: "var(--qa-accent-tint)"
|
|
3900
5168
|
},
|
|
3901
5169
|
children: /* @__PURE__ */ jsx(
|
|
3902
5170
|
"span",
|
|
@@ -3905,7 +5173,7 @@ function CaptureMode() {
|
|
|
3905
5173
|
style: {
|
|
3906
5174
|
width: 16,
|
|
3907
5175
|
height: 16,
|
|
3908
|
-
background:
|
|
5176
|
+
background: "var(--qa-accent)",
|
|
3909
5177
|
border: "2px solid #fff",
|
|
3910
5178
|
boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
|
|
3911
5179
|
pointerEvents: "none"
|
|
@@ -3926,19 +5194,19 @@ function CaptureMode() {
|
|
|
3926
5194
|
dir,
|
|
3927
5195
|
role: "group",
|
|
3928
5196
|
"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-
|
|
5197
|
+
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
5198
|
style: {
|
|
3931
5199
|
...confirmPopStyle,
|
|
3932
|
-
background:
|
|
3933
|
-
borderColor:
|
|
5200
|
+
background: "var(--qa-surface-1)",
|
|
5201
|
+
borderColor: "var(--qa-border-subtle)"
|
|
3934
5202
|
},
|
|
3935
5203
|
children: [
|
|
3936
5204
|
/* @__PURE__ */ jsxs(
|
|
3937
5205
|
"button",
|
|
3938
5206
|
{
|
|
3939
5207
|
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: {
|
|
5208
|
+
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",
|
|
5209
|
+
style: { border: "none", cursor: "pointer" },
|
|
3942
5210
|
children: [
|
|
3943
5211
|
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
|
|
3944
5212
|
t("use_this")
|
|
@@ -3952,14 +5220,10 @@ function CaptureMode() {
|
|
|
3952
5220
|
setCandidate(null);
|
|
3953
5221
|
setHover(null);
|
|
3954
5222
|
setPhase("selecting");
|
|
5223
|
+
setTargetForensics(void 0);
|
|
3955
5224
|
},
|
|
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
|
-
},
|
|
5225
|
+
className: "qa-tap qa-rounded-full qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
5226
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
3963
5227
|
children: t("adjust")
|
|
3964
5228
|
}
|
|
3965
5229
|
)
|
|
@@ -3971,70 +5235,111 @@ function CaptureMode() {
|
|
|
3971
5235
|
{
|
|
3972
5236
|
"data-qa-overlay": "true",
|
|
3973
5237
|
dir,
|
|
3974
|
-
className: `qa-fixed qa-z-10096 qa-w-320 qa-overflow-hidden qa-rounded-xl qa-border qa-
|
|
5238
|
+
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
5239
|
style: {
|
|
3976
5240
|
...popStyle,
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
5241
|
+
// popStyle's maxHeight caps this to whatever room is actually
|
|
5242
|
+
// available above/below the target; overflowY lets the card
|
|
5243
|
+
// itself scroll internally rather than ever rendering content
|
|
5244
|
+
// (most importantly the Save button) somewhere the page's own
|
|
5245
|
+
// scroll — locked during capture — can't reach. overflowX stays
|
|
5246
|
+
// hidden so the qa-overflow-hidden class's rounded-corner
|
|
5247
|
+
// clipping is preserved on that axis.
|
|
5248
|
+
overflowY: "auto",
|
|
5249
|
+
overflowX: "hidden",
|
|
5250
|
+
background: "var(--qa-surface-1)"
|
|
3980
5251
|
},
|
|
3981
5252
|
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
|
-
),
|
|
5253
|
+
/* @__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: [
|
|
5254
|
+
/* @__PURE__ */ jsx(
|
|
5255
|
+
Icon,
|
|
5256
|
+
{
|
|
5257
|
+
name: selection.kind === "region" ? "Square" : "MousePointerClick",
|
|
5258
|
+
size: 16
|
|
5259
|
+
}
|
|
5260
|
+
),
|
|
5261
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-font-semibold", children: selection.kind === "region" ? t("sel_region") : t("sel_element") }),
|
|
5262
|
+
/* @__PURE__ */ jsx(
|
|
5263
|
+
"button",
|
|
5264
|
+
{
|
|
5265
|
+
onClick: () => endCapture(),
|
|
5266
|
+
className: "qa-tap-icon qa-ms-auto qa-opacity-80 qa-hover-opacity-100",
|
|
5267
|
+
style: { background: "transparent", border: "none", cursor: "pointer", color: "var(--qa-ink-hi)" },
|
|
5268
|
+
children: /* @__PURE__ */ jsx(Icon, { name: "X", size: 16 })
|
|
5269
|
+
}
|
|
5270
|
+
)
|
|
5271
|
+
] }),
|
|
4008
5272
|
/* @__PURE__ */ jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
|
|
4009
5273
|
/* @__PURE__ */ jsx(
|
|
4010
5274
|
"div",
|
|
4011
5275
|
{
|
|
4012
5276
|
className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
|
|
4013
5277
|
style: {
|
|
4014
|
-
borderColor:
|
|
4015
|
-
background:
|
|
5278
|
+
borderColor: "var(--qa-border-subtle)",
|
|
5279
|
+
background: "var(--qa-surface-0)"
|
|
4016
5280
|
},
|
|
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(
|
|
5281
|
+
children: capturing ? /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
|
|
5282
|
+
/* @__PURE__ */ jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
|
|
5283
|
+
t("capturing")
|
|
5284
|
+
] }) : shotUrl ? /* @__PURE__ */ jsx(
|
|
4028
5285
|
"img",
|
|
4029
5286
|
{
|
|
4030
5287
|
src: shotUrl,
|
|
4031
5288
|
alt: "capture",
|
|
4032
5289
|
className: "qa-max-h-32 qa-rounded-md"
|
|
4033
5290
|
}
|
|
5291
|
+
) : captureError ? (
|
|
5292
|
+
// The render broke rather than being skipped — offer a retry
|
|
5293
|
+
// against the same selection instead of a dead-end message.
|
|
5294
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
|
|
5295
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
|
|
5296
|
+
/* @__PURE__ */ jsxs(
|
|
5297
|
+
"button",
|
|
5298
|
+
{
|
|
5299
|
+
type: "button",
|
|
5300
|
+
onClick: () => selection && void runCapture(selection.rect),
|
|
5301
|
+
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",
|
|
5302
|
+
children: [
|
|
5303
|
+
/* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
5304
|
+
t("retry")
|
|
5305
|
+
]
|
|
5306
|
+
}
|
|
5307
|
+
)
|
|
5308
|
+
] })
|
|
4034
5309
|
) : /* @__PURE__ */ jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
|
|
4035
5310
|
}
|
|
4036
5311
|
),
|
|
4037
5312
|
/* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
5313
|
+
/* @__PURE__ */ jsxs(
|
|
5314
|
+
"div",
|
|
5315
|
+
{
|
|
5316
|
+
role: "group",
|
|
5317
|
+
"aria-label": t("severity_label"),
|
|
5318
|
+
className: "qa-flex qa-items-center qa-flex-wrap qa-gap-1.5",
|
|
5319
|
+
children: [
|
|
5320
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-text-mid qa-me-1", children: t("severity_label") }),
|
|
5321
|
+
SEVERITIES2.map((sev) => {
|
|
5322
|
+
const active = severity === sev;
|
|
5323
|
+
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";
|
|
5324
|
+
return /* @__PURE__ */ jsxs(
|
|
5325
|
+
"button",
|
|
5326
|
+
{
|
|
5327
|
+
type: "button",
|
|
5328
|
+
onClick: () => setSeverity(sev),
|
|
5329
|
+
"aria-pressed": active,
|
|
5330
|
+
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"}`,
|
|
5331
|
+
style: { cursor: "pointer" },
|
|
5332
|
+
children: [
|
|
5333
|
+
/* @__PURE__ */ jsx(Icon, { name: SEVERITY_ICON[sev], size: 12 }),
|
|
5334
|
+
t(SEVERITY_LABEL_KEY2[sev])
|
|
5335
|
+
]
|
|
5336
|
+
},
|
|
5337
|
+
sev
|
|
5338
|
+
);
|
|
5339
|
+
})
|
|
5340
|
+
]
|
|
5341
|
+
}
|
|
5342
|
+
),
|
|
4038
5343
|
/* @__PURE__ */ jsx(
|
|
4039
5344
|
"textarea",
|
|
4040
5345
|
{
|
|
@@ -4046,8 +5351,7 @@ function CaptureMode() {
|
|
|
4046
5351
|
},
|
|
4047
5352
|
rows: 3,
|
|
4048
5353
|
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" }
|
|
5354
|
+
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
5355
|
}
|
|
4052
5356
|
),
|
|
4053
5357
|
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
@@ -4056,8 +5360,8 @@ function CaptureMode() {
|
|
|
4056
5360
|
{
|
|
4057
5361
|
onClick: () => void save(),
|
|
4058
5362
|
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: {
|
|
5363
|
+
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",
|
|
5364
|
+
style: { border: "none", cursor: "pointer" },
|
|
4061
5365
|
children: [
|
|
4062
5366
|
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
|
|
4063
5367
|
t("save_point")
|
|
@@ -4072,14 +5376,11 @@ function CaptureMode() {
|
|
|
4072
5376
|
setSelection(null);
|
|
4073
5377
|
setShot(null);
|
|
4074
5378
|
setDescription("");
|
|
5379
|
+
setSeverity("bug");
|
|
5380
|
+
setTargetForensics(void 0);
|
|
4075
5381
|
},
|
|
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
|
-
},
|
|
5382
|
+
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
|
|
5383
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
4083
5384
|
children: t("reselect")
|
|
4084
5385
|
}
|
|
4085
5386
|
)
|
|
@@ -4133,8 +5434,9 @@ function CaptureGate() {
|
|
|
4133
5434
|
return captureActive ? /* @__PURE__ */ jsx(CaptureMode, {}) : null;
|
|
4134
5435
|
}
|
|
4135
5436
|
function QaRootInner({ config }) {
|
|
5437
|
+
const { isOpen, setIsOpen, testAlong } = useQa();
|
|
4136
5438
|
const shouldShowInitially = config.alwaysVisible === true || config.visible === true || config.visible === void 0 && !isProduction();
|
|
4137
|
-
const [
|
|
5439
|
+
const [widgetShown, setWidgetShown] = useState(shouldShowInitially);
|
|
4138
5440
|
useEffect(() => {
|
|
4139
5441
|
if (typeof document === "undefined") return;
|
|
4140
5442
|
const hk = parseHotkey(config.hotkey);
|
|
@@ -4142,16 +5444,22 @@ function QaRootInner({ config }) {
|
|
|
4142
5444
|
const handler = (e) => {
|
|
4143
5445
|
if (e.key.toLowerCase() === hk.key && !!e.shiftKey === hk.shift && !!e.altKey === hk.alt && !!e.ctrlKey === hk.ctrl && !!e.metaKey === hk.meta) {
|
|
4144
5446
|
e.preventDefault();
|
|
4145
|
-
|
|
5447
|
+
if (!widgetShown) {
|
|
5448
|
+
setWidgetShown(true);
|
|
5449
|
+
setIsOpen(true);
|
|
5450
|
+
} else {
|
|
5451
|
+
setIsOpen(!isOpen);
|
|
5452
|
+
}
|
|
4146
5453
|
}
|
|
4147
5454
|
};
|
|
4148
5455
|
document.addEventListener("keydown", handler);
|
|
4149
5456
|
return () => document.removeEventListener("keydown", handler);
|
|
4150
|
-
}, [config.hotkey]);
|
|
4151
|
-
if (!
|
|
5457
|
+
}, [config.hotkey, widgetShown, isOpen, setIsOpen]);
|
|
5458
|
+
if (!widgetShown) return null;
|
|
4152
5459
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4153
5460
|
/* @__PURE__ */ jsx(QaFab, {}),
|
|
4154
|
-
/* @__PURE__ */ jsx(QaPanel, {}),
|
|
5461
|
+
testAlong.active ? /* @__PURE__ */ jsx(TestAlongHud, {}) : /* @__PURE__ */ jsx(QaPanel, {}),
|
|
5462
|
+
/* @__PURE__ */ jsx(NoticeHost, {}),
|
|
4155
5463
|
/* @__PURE__ */ jsx(CaptureGate, {})
|
|
4156
5464
|
] });
|
|
4157
5465
|
}
|
|
@@ -4170,18 +5478,23 @@ function mountQaStudio(config) {
|
|
|
4170
5478
|
document.body.appendChild(host);
|
|
4171
5479
|
const shadow = host.attachShadow({ mode: "open" });
|
|
4172
5480
|
injectStyles(shadow);
|
|
4173
|
-
applyThemeVars(host, config.theme);
|
|
4174
5481
|
const root = ReactDOM.createRoot(shadow);
|
|
4175
5482
|
root.render(React.createElement(QaRoot, { config }));
|
|
5483
|
+
if (config.captureContext !== false) {
|
|
5484
|
+
installContextCapture();
|
|
5485
|
+
}
|
|
4176
5486
|
return {
|
|
4177
5487
|
destroy() {
|
|
5488
|
+
if (config.captureContext !== false) {
|
|
5489
|
+
uninstallContextCapture();
|
|
5490
|
+
}
|
|
4178
5491
|
try {
|
|
4179
5492
|
root.unmount();
|
|
4180
5493
|
} catch {
|
|
4181
5494
|
}
|
|
4182
5495
|
if (host.parentNode) host.remove();
|
|
4183
5496
|
if (typeof document !== "undefined") {
|
|
4184
|
-
document.body.querySelectorAll(":scope > [data-qa-overlay]").forEach((el) => el.remove());
|
|
5497
|
+
document.body.querySelectorAll(":scope > [data-qa-overlay]:not(qapture-overlay)").forEach((el) => el.remove());
|
|
4185
5498
|
}
|
|
4186
5499
|
}
|
|
4187
5500
|
};
|
|
@@ -4202,11 +5515,13 @@ function initQaStudio(config) {
|
|
|
4202
5515
|
function Qapture({ config }) {
|
|
4203
5516
|
useEffect(() => {
|
|
4204
5517
|
const instance = initQaStudio(config);
|
|
4205
|
-
return () =>
|
|
5518
|
+
return () => {
|
|
5519
|
+
queueMicrotask(() => instance.destroy());
|
|
5520
|
+
};
|
|
4206
5521
|
}, []);
|
|
4207
5522
|
return null;
|
|
4208
5523
|
}
|
|
4209
5524
|
|
|
4210
5525
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
4211
|
-
//# sourceMappingURL=chunk-
|
|
4212
|
-
//# sourceMappingURL=chunk-
|
|
5526
|
+
//# sourceMappingURL=chunk-2CAMCMJP.js.map
|
|
5527
|
+
//# sourceMappingURL=chunk-2CAMCMJP.js.map
|