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