qapture2 0.2.4 → 0.3.0

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