qapture2 0.2.4 → 0.3.1

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