qapture2 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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"];
@@ -316,7 +285,6 @@ function validateConfig(input) {
316
285
  return {
317
286
  config: {
318
287
  namespace: DEFAULTS.namespace,
319
- theme: { ...DEFAULT_THEME },
320
288
  brand: { label: DEFAULTS.brandLabel },
321
289
  loginField: { ...DEFAULTS.loginField },
322
290
  credentials: [],
@@ -325,7 +293,8 @@ function validateConfig(input) {
325
293
  rtl: DEFAULTS.rtl,
326
294
  visible: DEFAULTS.visible,
327
295
  alwaysVisible: DEFAULTS.alwaysVisible,
328
- hotkey: DEFAULTS.hotkey
296
+ hotkey: DEFAULTS.hotkey,
297
+ captureContext: DEFAULTS.captureContext
329
298
  },
330
299
  warnings
331
300
  };
@@ -335,7 +304,6 @@ function validateConfig(input) {
335
304
  return {
336
305
  config: {
337
306
  namespace: DEFAULTS.namespace,
338
- theme: { ...DEFAULT_THEME },
339
307
  brand: { label: DEFAULTS.brandLabel },
340
308
  loginField: { ...DEFAULTS.loginField },
341
309
  credentials: [],
@@ -344,14 +312,19 @@ function validateConfig(input) {
344
312
  rtl: DEFAULTS.rtl,
345
313
  visible: DEFAULTS.visible,
346
314
  alwaysVisible: DEFAULTS.alwaysVisible,
347
- hotkey: DEFAULTS.hotkey
315
+ hotkey: DEFAULTS.hotkey,
316
+ captureContext: DEFAULTS.captureContext
348
317
  },
349
318
  warnings
350
319
  };
351
320
  }
352
321
  const raw = input;
353
322
  const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
354
- const theme = coerceTheme(raw["theme"]);
323
+ if (raw["theme"] !== void 0) {
324
+ warnings.push(
325
+ '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.'
326
+ );
327
+ }
355
328
  let brandLabel = DEFAULTS.brandLabel;
356
329
  if (raw["brand"] !== void 0 && raw["brand"] !== null && typeof raw["brand"] === "object") {
357
330
  const b = raw["brand"];
@@ -373,6 +346,7 @@ function validateConfig(input) {
373
346
  const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
374
347
  const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
375
348
  const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
349
+ const captureContext = typeof raw["captureContext"] === "boolean" ? raw["captureContext"] : DEFAULTS.captureContext;
376
350
  let visible = DEFAULTS.visible;
377
351
  if (raw["visible"] !== void 0) {
378
352
  if (typeof raw["visible"] === "boolean") {
@@ -384,7 +358,6 @@ function validateConfig(input) {
384
358
  return {
385
359
  config: {
386
360
  namespace,
387
- theme,
388
361
  brand: { label: brandLabel },
389
362
  loginField,
390
363
  credentials,
@@ -393,7 +366,8 @@ function validateConfig(input) {
393
366
  rtl,
394
367
  visible,
395
368
  alwaysVisible,
396
- hotkey
369
+ hotkey,
370
+ captureContext
397
371
  },
398
372
  warnings
399
373
  };
@@ -404,6 +378,103 @@ var QA_CSS = `
404
378
  /* \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
379
  *, *::before, *::after { box-sizing: border-box; }
406
380
 
381
+ /* \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
382
+ Single source of truth for every colour, shadow, radius, font, motion
383
+ duration, and z-index the widget uses. Nothing here is themeable \u2014
384
+ qapture 0.3.0 ships one fixed, self-contained design. */
385
+ :host {
386
+ /* Surfaces */
387
+ --qa-surface-0: #101215;
388
+ --qa-surface-1: #181B20;
389
+ --qa-surface-2: #20242B;
390
+ --qa-surface-3: #2A2F37;
391
+
392
+ /* Ink */
393
+ --qa-ink-hi: #F4F5F7;
394
+ --qa-ink-mid: #A8AEB8;
395
+ --qa-ink-lo: #6B717C;
396
+ --qa-ink-faint: #4A4F58;
397
+
398
+ /* Accent */
399
+ --qa-accent: #4D9CFF;
400
+ --qa-accent-hover: #6FB0FF;
401
+ --qa-accent-active: #3B84E6;
402
+ --qa-on-accent: #0A0C10;
403
+ --qa-accent-tint: rgba(77,156,255,0.14);
404
+ --qa-accent-border: rgba(77,156,255,0.45);
405
+
406
+ /* Semantic */
407
+ --qa-danger: #FF6B6B;
408
+ --qa-danger-tint: rgba(255,107,107,0.14);
409
+ --qa-warn: #FBBF24;
410
+ --qa-warn-tint: rgba(251,191,36,0.14);
411
+ --qa-success: #34D399;
412
+ --qa-success-tint: rgba(52,211,153,0.14);
413
+ --qa-neutral: #5B616B;
414
+
415
+ /* Borders */
416
+ --qa-border-subtle: rgba(255,255,255,0.08);
417
+ --qa-border-strong: rgba(255,255,255,0.14);
418
+
419
+ /* Scrims */
420
+ --qa-scrim-dialog: rgba(8,9,12,0.50);
421
+ --qa-scrim-capture: rgba(8,9,12,0.32);
422
+ --qa-scrim-spot: rgba(8,9,12,0.55);
423
+
424
+ /* Elevation */
425
+ --qa-sheen: inset 0 1px 0 rgba(255,255,255,0.06);
426
+ --qa-elev-1: 0 1px 2px rgba(0,0,0,0.40);
427
+ --qa-elev-2: 0 8px 24px -8px rgba(0,0,0,0.55);
428
+ --qa-elev-3: 0 24px 60px -16px rgba(0,0,0,0.65);
429
+
430
+ /* Radius */
431
+ --qa-radius-sm: 6px;
432
+ --qa-radius-md: 10px;
433
+ --qa-radius-lg: 14px;
434
+
435
+ /* Fonts (same stack for Arabic \u2014 no separate Arabic typeface) */
436
+ --qa-font: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
437
+ --qa-font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
438
+
439
+ /* Motion */
440
+ --qa-dur-1: 120ms;
441
+ --qa-dur-2: 180ms;
442
+ --qa-dur-3: 240ms;
443
+ --qa-ease: cubic-bezier(0.4,0,0.2,1);
444
+ --qa-ease-out: cubic-bezier(0.16,1,0.3,1);
445
+
446
+ /* Z-index scale */
447
+ --qa-z-fab: 9990;
448
+ --qa-z-panel: 9995;
449
+ --qa-z-capture-dim: 10090;
450
+ --qa-z-capture-highlight: 10092;
451
+ --qa-z-capture-region-move: 10093;
452
+ --qa-z-capture-region-handle: 10094;
453
+ --qa-z-capture-hint: 10095;
454
+ --qa-z-capture-ui: 10096;
455
+ --qa-z-toast: 10097;
456
+
457
+ font-family: var(--qa-font);
458
+ color: var(--qa-ink-hi);
459
+ }
460
+
461
+ /* Respect the user's OS-level motion preference: kill durations everywhere,
462
+ including the token defaults so any var(--qa-dur-*)-based rule inherits
463
+ the kill for free. */
464
+ @media (prefers-reduced-motion: reduce) {
465
+ :host {
466
+ --qa-dur-1: 0ms;
467
+ --qa-dur-2: 0ms;
468
+ --qa-dur-3: 0ms;
469
+ }
470
+ *, *::before, *::after {
471
+ animation-duration: 0.01ms !important;
472
+ animation-iteration-count: 1 !important;
473
+ transition-duration: 0.01ms !important;
474
+ scroll-behavior: auto !important;
475
+ }
476
+ }
477
+
407
478
  /* \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
479
  .qa-fixed { position: fixed; }
409
480
  .qa-absolute { position: absolute; }
@@ -419,17 +490,18 @@ var QA_CSS = `
419
490
  .qa-left-half { left: 50%; }
420
491
  .qa-right-0 { right: 0; }
421
492
 
422
- /* z-index */
493
+ /* z-index \u2014 values mirror the --qa-z-* tokens above; class NAMES are kept
494
+ verbatim (scripts/browser-test.mjs string-matches .qa-z-10093/.qa-z-10094). */
423
495
  .qa-z-1 { z-index: 1; }
424
496
  .qa-z-50 { z-index: 50; }
425
497
  .qa-z-100 { z-index: 100; }
426
- .qa-z-10090 { z-index: 10090; }
427
- .qa-z-10092 { z-index: 10092; }
498
+ .qa-z-10090 { z-index: var(--qa-z-capture-dim); }
499
+ .qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
428
500
  /* 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; }
501
+ .qa-z-10093 { z-index: var(--qa-z-capture-region-move); }
502
+ .qa-z-10094 { z-index: var(--qa-z-capture-region-handle); }
503
+ .qa-z-10095 { z-index: var(--qa-z-capture-hint); }
504
+ .qa-z-10096 { z-index: var(--qa-z-capture-ui); }
433
505
 
434
506
  /* \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
507
  .qa-flex { display: flex; }
@@ -543,7 +615,12 @@ var QA_CSS = `
543
615
  .qa-border-t { border-top-width: 1px; border-top-style: solid; }
544
616
  .qa-border-b { border-bottom-width: 1px; border-bottom-style: solid; }
545
617
  .qa-border-white { border-color: #ffffff; }
546
- .qa-border-white-40 { border-color: rgba(255,255,255,0.40); }
618
+ .qa-border-white-40 { border-color: var(--qa-border-strong); }
619
+
620
+ /* Semantic border colour (combine with .qa-border for width+style) */
621
+ .qa-border-subtle { border-color: var(--qa-border-subtle); }
622
+ .qa-border-strong { border-color: var(--qa-border-strong); }
623
+ .qa-border-accent { border-color: var(--qa-accent-border); }
547
624
 
548
625
  /* \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
626
  .qa-rounded { border-radius: 0.25rem; }
@@ -557,6 +634,13 @@ var QA_CSS = `
557
634
  .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
635
  .qa-shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
559
636
 
637
+ /* Semantic elevation \u2014 each layers --qa-sheen (a 1px inner highlight) on top
638
+ of the matching --qa-elev-* drop shadow, so raised surfaces read as
639
+ subtly lit from above rather than flat dark rectangles. */
640
+ .qa-elev-1 { box-shadow: var(--qa-elev-1), var(--qa-sheen); }
641
+ .qa-elev-2 { box-shadow: var(--qa-elev-2), var(--qa-sheen); }
642
+ .qa-elev-3 { box-shadow: var(--qa-elev-3), var(--qa-sheen); }
643
+
560
644
  /* \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
645
  .qa-text-10 { font-size: 10px; }
562
646
  .qa-text-11 { font-size: 11px; }
@@ -567,7 +651,7 @@ var QA_CSS = `
567
651
  .qa-font-medium { font-weight: 500; }
568
652
  .qa-font-semibold { font-weight: 600; }
569
653
  .qa-font-bold { font-weight: 700; }
570
- .qa-font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
654
+ .qa-font-mono { font-family: var(--qa-font-mono); }
571
655
  .qa-leading-relaxed { line-height: 1.625; }
572
656
  .qa-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
573
657
  .qa-whitespace-pre-wrap { white-space: pre-wrap; }
@@ -590,19 +674,53 @@ var QA_CSS = `
590
674
  /* \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
675
  .qa-text-white { color: #ffffff; }
592
676
  .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; }
677
+ /* legacy slate scale, restyled onto the ink levels in place */
678
+ .qa-text-slate-300 { color: var(--qa-ink-faint); }
679
+ .qa-text-slate-400 { color: var(--qa-ink-lo); }
680
+ .qa-text-slate-500 { color: var(--qa-ink-mid); }
681
+ .qa-text-green-600 { color: var(--qa-success); }
682
+ .qa-text-red-500 { color: var(--qa-danger); }
683
+ .qa-text-red-600 { color: var(--qa-danger); }
684
+
685
+ /* Semantic text levels */
686
+ .qa-text-hi { color: var(--qa-ink-hi); }
687
+ .qa-text-mid { color: var(--qa-ink-mid); }
688
+ .qa-text-lo { color: var(--qa-ink-lo); }
689
+ .qa-text-faint { color: var(--qa-ink-faint); }
690
+ .qa-text-accent { color: var(--qa-accent); }
691
+ .qa-text-on-accent { color: var(--qa-on-accent); }
692
+ .qa-text-danger { color: var(--qa-danger); }
693
+ .qa-text-warn { color: var(--qa-warn); }
694
+ .qa-text-success { color: var(--qa-success); }
599
695
 
600
696
  /* \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); }
697
+ /* legacy names, restyled onto Graphite tokens in place \u2014 components keep
698
+ using these class names unchanged. */
699
+ .qa-bg-white { background-color: var(--qa-surface-1); }
700
+ .qa-bg-white-25 { background-color: var(--qa-surface-3); }
603
701
  .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); }
702
+ /* These two were 3%/5% black tints for a light theme, which is inert on a
703
+ dark surface. Restyled as low-alpha WHITE lifts of the same two
704
+ intensities \u2014 still legible as a step above the base surface. */
705
+ .qa-bg-black-3 { background-color: rgba(255,255,255,0.03); }
706
+ .qa-bg-black-5 { background-color: rgba(255,255,255,0.05); }
707
+
708
+ /* Semantic surfaces */
709
+ .qa-bg-0 { background-color: var(--qa-surface-0); }
710
+ .qa-bg-1 { background-color: var(--qa-surface-1); }
711
+ .qa-bg-2 { background-color: var(--qa-surface-2); }
712
+ .qa-bg-3 { background-color: var(--qa-surface-3); }
713
+
714
+ .qa-bg-accent {
715
+ background-color: var(--qa-accent);
716
+ color: var(--qa-on-accent);
717
+ }
718
+ .qa-bg-accent:hover { background-color: var(--qa-accent-hover); }
719
+
720
+ .qa-bg-accent-tint { background-color: var(--qa-accent-tint); }
721
+ .qa-bg-danger-tint { background-color: var(--qa-danger-tint); }
722
+ .qa-bg-warn-tint { background-color: var(--qa-warn-tint); }
723
+ .qa-bg-success-tint { background-color: var(--qa-success-tint); }
606
724
 
607
725
  /* \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
726
  .qa-opacity-0 { opacity: 0; }
@@ -621,8 +739,10 @@ var QA_CSS = `
621
739
  .qa-touch-none { touch-action: none; }
622
740
  .qa-touch-pan { touch-action: pan-x pan-y; }
623
741
 
624
- .qa-focus-ring:focus {
625
- outline: 2px solid var(--qa-primary, #4f46e5);
742
+ /* Restyled onto :focus-visible (was :focus) so a mouse click no longer
743
+ leaves a persistent ring \u2014 only keyboard/AT focus does. */
744
+ .qa-focus-ring:focus-visible {
745
+ outline: 2px solid var(--qa-accent);
626
746
  outline-offset: 2px;
627
747
  }
628
748
 
@@ -634,13 +754,14 @@ input:disabled,
634
754
  }
635
755
 
636
756
  /* 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); }
757
+ .qa-hover-bg-black-3:hover { background-color: var(--qa-surface-2); }
758
+ .qa-hover-bg-black-5:hover { background-color: var(--qa-surface-3); }
759
+ .qa-hover-bg-white-15:hover { background-color: var(--qa-surface-2); }
760
+ .qa-hover-bg-2:hover { background-color: var(--qa-surface-2); }
640
761
  .qa-hover-opacity-80:hover { opacity: 0.80; }
641
762
  .qa-hover-opacity-100:hover { opacity: 1; }
642
- .qa-hover-text-red:hover { color: #ef4444; }
643
- .qa-hover-text-slate-600:hover { color: #475569; }
763
+ .qa-hover-text-red:hover { color: var(--qa-danger); }
764
+ .qa-hover-text-slate-600:hover { color: var(--qa-ink-hi); }
644
765
 
645
766
  /* Group-hover (child uses .qa-group-hover-opacity-80 inside a .qa-group parent) */
646
767
  .qa-group .qa-group-hover-opacity-80 { opacity: 0.40; }
@@ -664,13 +785,25 @@ input:disabled,
664
785
  50% { opacity: 0.5; box-shadow: 0 0 0 8px transparent; }
665
786
  }
666
787
 
788
+ @keyframes qaShimmer {
789
+ 0%, 100% { opacity: 0.55; }
790
+ 50% { opacity: 1; }
791
+ }
792
+
667
793
  .qa-animate-spin {
668
794
  animation: qaSpin 1s linear infinite;
669
795
  }
670
796
 
671
797
  .qa-animate-pulse-accent {
672
798
  animation: qaPulse 2s ease-in-out infinite;
673
- color: var(--qa-accent, #7c3aed);
799
+ color: var(--qa-accent);
800
+ }
801
+
802
+ /* Loading placeholder rows (NoteList while notesLoading && !notes.length) */
803
+ .qa-skeleton {
804
+ background-color: var(--qa-surface-2);
805
+ border-radius: var(--qa-radius-sm);
806
+ animation: qaShimmer 1.4s ease-in-out infinite;
674
807
  }
675
808
 
676
809
  /* \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 +890,41 @@ input:disabled,
757
890
 
758
891
  /* \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
892
  .qa-space-y-1\\.5 > * + * { margin-top: 0.375rem; }
893
+
894
+ /* \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 */
895
+ .qa-toast-viewport {
896
+ position: fixed;
897
+ inset-inline: 0;
898
+ bottom: 1rem;
899
+ z-index: var(--qa-z-toast);
900
+ display: flex;
901
+ flex-direction: column;
902
+ align-items: center;
903
+ gap: 0.5rem;
904
+ pointer-events: none;
905
+ }
906
+ .qa-toast {
907
+ pointer-events: auto;
908
+ display: flex;
909
+ align-items: center;
910
+ gap: 0.5rem;
911
+ max-width: min(92vw, 360px);
912
+ padding: 0.625rem 0.75rem;
913
+ background-color: var(--qa-surface-2);
914
+ border: 1px solid var(--qa-border-subtle);
915
+ border-radius: var(--qa-radius-md);
916
+ box-shadow: var(--qa-elev-2), var(--qa-sheen);
917
+ color: var(--qa-ink-hi);
918
+ font-size: 13px;
919
+ opacity: 0;
920
+ transform: translateY(8px);
921
+ transition: opacity var(--qa-dur-2) var(--qa-ease-out),
922
+ transform var(--qa-dur-2) var(--qa-ease-out);
923
+ }
924
+ .qa-toast.qa-toast-in {
925
+ opacity: 1;
926
+ transform: translateY(0);
927
+ }
760
928
  `;
761
929
  function injectStyles(root) {
762
930
  if (typeof CSSStyleSheet !== "undefined" && "adoptedStyleSheets" in Document.prototype) {
@@ -772,16 +940,295 @@ function injectStyles(root) {
772
940
  style.textContent = QA_CSS;
773
941
  root.appendChild(style);
774
942
  }
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);
943
+
944
+ // src/lib/contextBuffer.ts
945
+ var RING_CAP = 75;
946
+ var MAX_MESSAGE_CHARS = 600;
947
+ var MAX_HTML_CHARS = 600;
948
+ var ring = [];
949
+ var installed = false;
950
+ var refCount = 0;
951
+ var drainedUpTo = 0;
952
+ var original = {};
953
+ function push(ev) {
954
+ ring.push(ev);
955
+ if (ring.length > RING_CAP) {
956
+ const overflow = ring.length - RING_CAP;
957
+ ring = ring.slice(overflow);
958
+ drainedUpTo = Math.max(0, drainedUpTo - overflow);
959
+ }
960
+ }
961
+ function clip(s, max = MAX_MESSAGE_CHARS) {
962
+ const str = typeof s === "string" ? s : safeStringify(s);
963
+ return str.length > max ? `${str.slice(0, max)}\u2026` : str;
964
+ }
965
+ function safeStringify(v) {
966
+ if (v === null) return "null";
967
+ if (v === void 0) return "undefined";
968
+ if (typeof v === "string") return v;
969
+ if (v instanceof Error) return `${v.name}: ${v.message}`;
970
+ try {
971
+ return JSON.stringify(v) ?? String(v);
972
+ } catch {
973
+ return String(v);
974
+ }
975
+ }
976
+ function now() {
977
+ return Date.now();
978
+ }
979
+ function redactUrl(raw) {
980
+ const s = String(raw ?? "");
981
+ try {
982
+ const u = new URL(s, typeof location !== "undefined" ? location.href : "http://localhost");
983
+ const redacted = u.search ? "?\u2026" : "";
984
+ return `${u.origin}${u.pathname}${redacted}`;
985
+ } catch {
986
+ const cut = s.split(/[?#]/)[0];
987
+ return s.length > cut.length ? `${cut}?\u2026` : cut;
988
+ }
989
+ }
990
+ function installContextCapture() {
991
+ refCount += 1;
992
+ if (installed) return;
993
+ if (typeof window === "undefined" || typeof document === "undefined") return;
994
+ installed = true;
995
+ original.consoleError = console.error.bind(console);
996
+ original.consoleWarn = console.warn.bind(console);
997
+ console.error = (...args) => {
998
+ push({ t: now(), kind: "console", level: "error", message: clip(args.map(safeStringify).join(" ")) });
999
+ original.consoleError?.(...args);
1000
+ };
1001
+ console.warn = (...args) => {
1002
+ push({ t: now(), kind: "console", level: "warn", message: clip(args.map(safeStringify).join(" ")) });
1003
+ original.consoleWarn?.(...args);
1004
+ };
1005
+ original.onError = (e) => {
1006
+ const ev = { t: now(), kind: "error", message: clip(e.message) };
1007
+ if (e.error?.stack) ev.stack = clip(e.error.stack);
1008
+ push(ev);
1009
+ };
1010
+ original.onRejection = (e) => {
1011
+ const reason = e.reason;
1012
+ const ev = {
1013
+ t: now(),
1014
+ kind: "error",
1015
+ message: clip(reason instanceof Error ? `${reason.name}: ${reason.message}` : safeStringify(reason))
1016
+ };
1017
+ if (reason instanceof Error && reason.stack) ev.stack = clip(reason.stack);
1018
+ push(ev);
1019
+ };
1020
+ window.addEventListener("error", original.onError);
1021
+ window.addEventListener("unhandledrejection", original.onRejection);
1022
+ if (typeof window.fetch === "function") {
1023
+ original.fetch = window.fetch.bind(window);
1024
+ window.fetch = async (input, init) => {
1025
+ const started = now();
1026
+ const method = (init?.method || (typeof input === "object" && "method" in input ? input.method : "GET") || "GET").toUpperCase();
1027
+ const url = redactUrl(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
1028
+ try {
1029
+ const res = await original.fetch(input, init);
1030
+ push({ t: started, kind: "network", method, url, status: res.status, durationMs: now() - started });
1031
+ return res;
1032
+ } catch (err) {
1033
+ push({
1034
+ t: started,
1035
+ kind: "network",
1036
+ method,
1037
+ url,
1038
+ status: null,
1039
+ durationMs: now() - started,
1040
+ error: clip(err instanceof Error ? err.message : safeStringify(err))
1041
+ });
1042
+ throw err;
1043
+ }
1044
+ };
1045
+ }
1046
+ if (typeof XMLHttpRequest !== "undefined") {
1047
+ original.xhrOpen = XMLHttpRequest.prototype.open;
1048
+ original.xhrSend = XMLHttpRequest.prototype.send;
1049
+ XMLHttpRequest.prototype.open = function(method, url, ...rest) {
1050
+ this.__qaMethod = String(method || "GET").toUpperCase();
1051
+ this.__qaUrl = redactUrl(typeof url === "string" ? url : url.href);
1052
+ return original.xhrOpen.call(this, method, url, ...rest);
1053
+ };
1054
+ XMLHttpRequest.prototype.send = function(...args) {
1055
+ this.__qaStart = now();
1056
+ const record = (error) => {
1057
+ const ev = {
1058
+ t: this.__qaStart ?? now(),
1059
+ kind: "network",
1060
+ method: this.__qaMethod ?? "GET",
1061
+ url: this.__qaUrl ?? "",
1062
+ status: error ? null : this.status,
1063
+ durationMs: now() - (this.__qaStart ?? now())
1064
+ };
1065
+ if (error) ev.error = error;
1066
+ push(ev);
1067
+ };
1068
+ this.addEventListener("load", () => record());
1069
+ this.addEventListener("error", () => record("network error"));
1070
+ this.addEventListener("timeout", () => record("timeout"));
1071
+ return original.xhrSend.apply(this, args);
1072
+ };
1073
+ }
1074
+ }
1075
+ function uninstallContextCapture() {
1076
+ if (refCount > 0) refCount -= 1;
1077
+ if (!installed || refCount > 0) return;
1078
+ installed = false;
1079
+ if (original.consoleError) console.error = original.consoleError;
1080
+ if (original.consoleWarn) console.warn = original.consoleWarn;
1081
+ if (original.fetch) window.fetch = original.fetch;
1082
+ if (original.xhrOpen) XMLHttpRequest.prototype.open = original.xhrOpen;
1083
+ if (original.xhrSend) XMLHttpRequest.prototype.send = original.xhrSend;
1084
+ if (original.onError) window.removeEventListener("error", original.onError);
1085
+ if (original.onRejection) {
1086
+ window.removeEventListener("unhandledrejection", original.onRejection);
1087
+ }
1088
+ ring = [];
1089
+ drainedUpTo = 0;
1090
+ }
1091
+ function drainSinceLastNote() {
1092
+ if (!installed) return [];
1093
+ const slice = ring.slice(drainedUpTo);
1094
+ drainedUpTo = ring.length;
1095
+ return slice;
1096
+ }
1097
+ function collectEnvSnapshot(route) {
1098
+ const snap = {
1099
+ url: typeof location !== "undefined" ? redactUrl(location.href) : "",
1100
+ route,
1101
+ viewportW: typeof window !== "undefined" ? window.innerWidth : 0,
1102
+ viewportH: typeof window !== "undefined" ? window.innerHeight : 0,
1103
+ dpr: typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1,
1104
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
1105
+ language: typeof navigator !== "undefined" ? navigator.language : "",
1106
+ online: typeof navigator !== "undefined" ? navigator.onLine !== false : true,
1107
+ timezone: ""
1108
+ };
1109
+ try {
1110
+ snap.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
1111
+ } catch {
1112
+ snap.timezone = "";
1113
+ }
1114
+ try {
1115
+ const nav = performance?.getEntriesByType?.("navigation")?.[0];
1116
+ if (nav && Number.isFinite(nav.duration) && nav.duration > 0) {
1117
+ snap.pageLoadMs = Math.round(nav.duration);
1118
+ }
1119
+ } catch {
1120
+ }
1121
+ try {
1122
+ const mem = performance.memory;
1123
+ if (mem?.usedJSHeapSize) snap.memoryUsedMB = Math.round(mem.usedJSHeapSize / 1048576);
1124
+ } catch {
1125
+ }
1126
+ return snap;
1127
+ }
1128
+ function luminance(rgb) {
1129
+ const [r, g, b] = rgb.map((c) => {
1130
+ const v = c / 255;
1131
+ return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
1132
+ });
1133
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
1134
+ }
1135
+ function parseRgb(color) {
1136
+ const m = (color || "").match(/^rgba?\(([^)]+)\)$/i);
1137
+ if (!m) return null;
1138
+ const parts = m[1].split(/[,/\s]+/).filter(Boolean).map(parseFloat);
1139
+ if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return null;
1140
+ return [parts[0], parts[1], parts[2]];
1141
+ }
1142
+ var SENSITIVE_FORENSICS_ATTRS = ["value", "checked", "selected"];
1143
+ function sanitizeForForensics(el) {
1144
+ const clone = el.cloneNode(true);
1145
+ const nodes = [clone, ...Array.from(clone.querySelectorAll("*"))];
1146
+ for (const node of nodes) {
1147
+ for (const attr of SENSITIVE_FORENSICS_ATTRS) {
1148
+ if (node.hasAttribute(attr)) node.removeAttribute(attr);
1149
+ }
1150
+ if (node.tagName === "TEXTAREA") node.textContent = "";
1151
+ }
1152
+ return clone;
1153
+ }
1154
+ function collectTargetForensics(el) {
1155
+ const out = {};
1156
+ if (!el || typeof window === "undefined") return out;
1157
+ try {
1158
+ out.html = clip(sanitizeForForensics(el).outerHTML, MAX_HTML_CHARS);
1159
+ } catch {
1160
+ }
1161
+ try {
1162
+ const cs = getComputedStyle(el);
1163
+ out.styles = {
1164
+ display: cs.display,
1165
+ position: cs.position,
1166
+ overflow: cs.overflow,
1167
+ "z-index": cs.zIndex,
1168
+ "font-size": cs.fontSize,
1169
+ color: cs.color,
1170
+ "background-color": cs.backgroundColor
1171
+ };
1172
+ const fg = parseRgb(cs.color);
1173
+ const bg = parseRgb(cs.backgroundColor);
1174
+ let contrastFlag = "unknown";
1175
+ if (fg && bg) {
1176
+ const l1 = luminance(fg);
1177
+ const l2 = luminance(bg);
1178
+ const ratio = (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
1179
+ contrastFlag = ratio < 4.5 ? "low" : "ok";
1180
+ }
1181
+ const name = el.getAttribute("aria-label") || el.getAttribute("title") || el.innerText || el.textContent || "";
1182
+ const tabIndexAttr = el.getAttribute("tabindex");
1183
+ const nativelyFocusable = /^(a|button|input|select|textarea)$/i.test(el.tagName) && !el.disabled;
1184
+ out.a11y = {
1185
+ hasAccessibleName: name.trim().length > 0,
1186
+ tabReachable: nativelyFocusable || tabIndexAttr !== null && tabIndexAttr !== "-1",
1187
+ contrastFlag
1188
+ };
1189
+ const role = el.getAttribute("role");
1190
+ if (role) out.a11y.role = role;
1191
+ } catch {
1192
+ }
1193
+ return out;
1194
+ }
1195
+
1196
+ // src/lib/journeyMatch.ts
1197
+ function normalizeRoute(route) {
1198
+ const path = String(route ?? "").split(/[?#]/)[0];
1199
+ if (path.length > 1 && path.endsWith("/")) return path.slice(0, -1);
1200
+ return path || "/";
1201
+ }
1202
+ function segments(path) {
1203
+ return normalizeRoute(path).split("/").filter(Boolean);
1204
+ }
1205
+ function matchesWithParams(stepPath, route) {
1206
+ const stepSegs = segments(stepPath);
1207
+ const routeSegs = segments(route);
1208
+ if (stepSegs.length !== routeSegs.length) return false;
1209
+ return stepSegs.every((seg, i) => {
1210
+ const isParam = seg.startsWith(":") || seg.startsWith("[") && seg.endsWith("]");
1211
+ return isParam || seg.toLowerCase() === routeSegs[i].toLowerCase();
1212
+ });
1213
+ }
1214
+ function matchRouteToSteps(journey, route) {
1215
+ if (!Array.isArray(journey) || !journey.length) return [];
1216
+ const target = normalizeRoute(route);
1217
+ const exact = [];
1218
+ const param = [];
1219
+ for (const lane of journey) {
1220
+ if (!lane || !Array.isArray(lane.steps)) continue;
1221
+ for (const step of lane.steps) {
1222
+ if (!step || typeof step.path !== "string") continue;
1223
+ const ref = { laneId: lane.id, path: step.path };
1224
+ if (normalizeRoute(step.path).toLowerCase() === target.toLowerCase()) {
1225
+ exact.push(ref);
1226
+ } else if (matchesWithParams(step.path, target)) {
1227
+ param.push(ref);
1228
+ }
1229
+ }
1230
+ }
1231
+ return [...exact, ...param];
785
1232
  }
786
1233
 
787
1234
  // src/lib/storage.ts
@@ -893,7 +1340,36 @@ var STR = {
893
1340
  use_this: "Use this",
894
1341
  adjust: "Adjust",
895
1342
  resize: "Resize",
896
- confirm_region: "Confirm region"
1343
+ confirm_region: "Confirm region",
1344
+ capture_failed: "Screenshot failed",
1345
+ retry: "Retry",
1346
+ persist_failed: "Storage full \u2014 this note may not survive a reload",
1347
+ note_deleted: "Note deleted",
1348
+ notes_cleared: "All notes cleared",
1349
+ undo: "Undo",
1350
+ export_done: "Export downloaded",
1351
+ export_failed: "Export failed",
1352
+ copied: "Copied",
1353
+ copy_failed: "Copy failed",
1354
+ copy_prompt: "Copy as agent prompt",
1355
+ severity_label: "Severity",
1356
+ sev_bug: "Bug",
1357
+ sev_question: "Question",
1358
+ sev_polish: "Polish",
1359
+ status_open: "Open",
1360
+ status_verified: "Verified",
1361
+ context_attached: "{n} runtime events attached",
1362
+ start_walkthrough: "Start walkthrough",
1363
+ step_of: "Step {n} of {m}",
1364
+ next_step: "Next",
1365
+ prev_step: "Back",
1366
+ mark_pass: "Pass",
1367
+ mark_fail: "Fail",
1368
+ capture_here: "Capture here",
1369
+ exit_walkthrough: "Exit",
1370
+ evidence_n: "{n} attached",
1371
+ no_evidence: "ticked, no capture",
1372
+ expected_label: "Expected"
897
1373
  },
898
1374
  ar: {
899
1375
  tab_notes: "\u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
@@ -944,7 +1420,36 @@ var STR = {
944
1420
  use_this: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0627",
945
1421
  adjust: "\u062A\u0639\u062F\u064A\u0644",
946
1422
  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"
1423
+ confirm_region: "\u062A\u0623\u0643\u064A\u062F \u0627\u0644\u0645\u0646\u0637\u0642\u0629",
1424
+ capture_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629",
1425
+ retry: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629",
1426
+ 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",
1427
+ note_deleted: "\u062A\u0645 \u062D\u0630\u0641 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0629",
1428
+ notes_cleared: "\u062A\u0645 \u0645\u0633\u062D \u062C\u0645\u064A\u0639 \u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
1429
+ undo: "\u062A\u0631\u0627\u062C\u0639",
1430
+ export_done: "\u062A\u0645 \u062A\u0646\u0632\u064A\u0644 \u0627\u0644\u0645\u0644\u0641",
1431
+ export_failed: "\u0641\u0634\u0644 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
1432
+ copied: "\u062A\u0645 \u0627\u0644\u0646\u0633\u062E",
1433
+ copy_failed: "\u0641\u0634\u0644 \u0627\u0644\u0646\u0633\u062E",
1434
+ copy_prompt: "\u0646\u0633\u062E \u0643\u0645\u0648\u062C\u0651\u0647 \u0644\u0644\u0648\u0643\u064A\u0644",
1435
+ severity_label: "\u0627\u0644\u0623\u0647\u0645\u064A\u0629",
1436
+ sev_bug: "\u062E\u0644\u0644",
1437
+ sev_question: "\u0633\u0624\u0627\u0644",
1438
+ sev_polish: "\u062A\u062D\u0633\u064A\u0646",
1439
+ status_open: "\u0645\u0641\u062A\u0648\u062D",
1440
+ status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
1441
+ context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
1442
+ start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
1443
+ step_of: "\u0627\u0644\u062E\u0637\u0648\u0629 {n} \u0645\u0646 {m}",
1444
+ next_step: "\u0627\u0644\u062A\u0627\u0644\u064A",
1445
+ prev_step: "\u0627\u0644\u0633\u0627\u0628\u0642",
1446
+ mark_pass: "\u0646\u062C\u0627\u062D",
1447
+ mark_fail: "\u0641\u0634\u0644",
1448
+ capture_here: "\u0627\u0644\u062A\u0642\u0637 \u0647\u0646\u0627",
1449
+ exit_walkthrough: "\u062E\u0631\u0648\u062C",
1450
+ evidence_n: "{n} \u0645\u0631\u0641\u0642",
1451
+ no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
1452
+ expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639"
948
1453
  }
949
1454
  };
950
1455
  function translate(lang, key, vars) {
@@ -964,10 +1469,14 @@ function pick(value, lang) {
964
1469
 
965
1470
  // src/lib/coverage.ts
966
1471
  var RISK_COLORS = {
967
- red: "#EF4444",
968
- amber: "#F59E0B",
969
- green: "#22C55E",
970
- none: "#CBD5E1"
1472
+ red: "#FF6B6B",
1473
+ // --qa-danger
1474
+ amber: "#FBBF24",
1475
+ // --qa-warn
1476
+ green: "#34D399",
1477
+ // --qa-success
1478
+ none: "#5B616B"
1479
+ // --qa-neutral
971
1480
  };
972
1481
  function computeCoverage(journey, guideChecked) {
973
1482
  const red = { total: 0, covered: 0 };
@@ -1027,33 +1536,103 @@ function computeCoverage(journey, guideChecked) {
1027
1536
  };
1028
1537
  }
1029
1538
 
1030
- // src/lib/exportZip.ts
1031
- function fmtTarget(t) {
1539
+ // src/lib/noteMarkdown.ts
1540
+ function oneLine(s) {
1541
+ return String(s ?? "").replace(/\r?\n|\r/g, " ").trim();
1542
+ }
1543
+ function formatEvent(ev, t0) {
1544
+ const rel = `${((ev.t - t0) / 1e3).toFixed(1)}s`;
1545
+ if (ev.kind === "network") {
1546
+ const status = ev.status === null ? ev.error ?? "failed" : String(ev.status);
1547
+ return `[${rel}] ${ev.method} ${ev.url} \u2192 ${status} (${ev.durationMs}ms)`;
1548
+ }
1549
+ if (ev.kind === "console") {
1550
+ return `[${rel}] console.${ev.level}: ${oneLine(ev.message)}`;
1551
+ }
1552
+ return `[${rel}] uncaught: ${oneLine(ev.message)}`;
1553
+ }
1554
+ function noteToMarkdown(note, opts) {
1555
+ const brand = opts?.brand ?? "Qapture";
1556
+ const idx = opts?.index;
1032
1557
  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
- );
1558
+ lines.push(idx != null ? `## Point ${idx}` : `## ${brand} point`);
1559
+ lines.push("");
1560
+ lines.push(`- **Page:** ${oneLine(note.route) || "/"}`);
1561
+ if (note.url) lines.push(`- **Full URL:** ${oneLine(note.url)}`);
1562
+ lines.push(`- **When:** ${oneLine(note.timestamp)}`);
1563
+ if (note.severity) lines.push(`- **Severity:** ${note.severity}`);
1564
+ if (note.status) lines.push(`- **Status:** ${note.status}`);
1565
+ if (note.journeyRef) {
1566
+ lines.push(`- **Journey step:** ${oneLine(note.journeyRef.laneId)} \u2192 ${oneLine(note.journeyRef.path)}`);
1567
+ }
1568
+ const target = note.target;
1569
+ if (target) {
1570
+ lines.push(`- **Target:** ${target.kind}`);
1571
+ if (target.selector) lines.push(`- **Selector:** \`${oneLine(target.selector)}\``);
1572
+ if (target.tagName) lines.push(`- **Tag:** \`<${oneLine(target.tagName)}>\``);
1573
+ if (target.text) lines.push(`- **Text:** ${oneLine(target.text)}`);
1574
+ const r = target.rect;
1575
+ if (r) {
1576
+ lines.push(
1577
+ `- **Position:** top ${Math.round(r.top)}, left ${Math.round(r.left)}, ${Math.round(r.width)}\xD7${Math.round(r.height)}`
1578
+ );
1579
+ }
1041
1580
  }
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));
1581
+ if (idx != null && note.screenshot) {
1582
+ lines.push(`- **Screenshot:** screenshots/point-${idx}.png`);
1583
+ }
1584
+ lines.push("");
1585
+ lines.push(oneLine(note.description) ? note.description.trim() : "_(no description)_");
1586
+ const ctx = note.context;
1587
+ if (ctx) {
1588
+ const env = ctx.env;
1589
+ const events = Array.isArray(ctx.events) ? ctx.events : [];
1590
+ lines.push("");
1591
+ lines.push("<details><summary>Runtime context at capture</summary>");
1592
+ lines.push("");
1593
+ lines.push("```");
1594
+ if (env) {
1595
+ lines.push(`viewport ${env.viewportW}\xD7${env.viewportH} @${env.dpr}x`);
1596
+ if (env.language) lines.push(`language ${env.language}`);
1597
+ if (env.timezone) lines.push(`timezone ${env.timezone}`);
1598
+ lines.push(`online ${env.online}`);
1599
+ if (env.pageLoadMs != null) lines.push(`pageLoad ${env.pageLoadMs}ms`);
1600
+ if (env.memoryUsedMB != null) lines.push(`jsHeap ${env.memoryUsedMB}MB`);
1601
+ if (env.userAgent) lines.push(`userAgent ${env.userAgent}`);
1602
+ }
1603
+ if (events.length) {
1604
+ const t0 = Date.parse(note.timestamp) || (events[events.length - 1]?.t ?? 0);
1605
+ lines.push("");
1606
+ lines.push(`events (${events.length}, most recent last):`);
1607
+ for (const ev of events) lines.push(` ${formatEvent(ev, t0)}`);
1608
+ } else {
1609
+ lines.push("");
1610
+ lines.push("events (none recorded)");
1611
+ }
1612
+ lines.push("```");
1613
+ const f = ctx.forensics;
1614
+ if (f && (f.html || f.styles || f.a11y)) {
1615
+ lines.push("");
1616
+ lines.push("**Element forensics**");
1617
+ lines.push("");
1618
+ lines.push("```");
1619
+ if (f.html) lines.push(`html ${oneLine(f.html)}`);
1620
+ if (f.styles) {
1621
+ for (const [k, v] of Object.entries(f.styles)) lines.push(`${k.padEnd(7)} ${v}`);
1622
+ }
1623
+ if (f.a11y) {
1624
+ if (f.a11y.role) lines.push(`role ${f.a11y.role}`);
1625
+ lines.push(`a11y accessibleName=${f.a11y.hasAccessibleName} tabReachable=${f.a11y.tabReachable}` + (f.a11y.contrastFlag ? ` contrast=${f.a11y.contrastFlag}` : ""));
1626
+ }
1627
+ lines.push("```");
1628
+ }
1629
+ lines.push("");
1630
+ lines.push("</details>");
1052
1631
  }
1053
- if (note.screenshot) lines.push(`- **Screenshot:** screenshots/point-${num}.png`);
1054
- lines.push("", note.description || "(no description)", "", "---", "");
1055
1632
  return lines.join("\n");
1056
1633
  }
1634
+
1635
+ // src/lib/exportZip.ts
1057
1636
  function safeName(name, stamp) {
1058
1637
  const fallback = `qa-notes-${stamp.slice(0, 10)}`;
1059
1638
  let base = (name ?? "").trim().replace(/\.zip$/i, "");
@@ -1133,12 +1712,13 @@ ${list}`);
1133
1712
  sections.push("## Conventions\n\n(not provided)");
1134
1713
  }
1135
1714
  const creds = config.credentials ?? [];
1715
+ const redactedCount = creds.filter((c) => !c.seeded).length;
1136
1716
  let credBlock;
1137
1717
  if (creds.length > 0) {
1138
1718
  const credRows = creds.map((c) => [
1139
1719
  c.role,
1140
1720
  c.login,
1141
- c.password || "(none)",
1721
+ c.seeded ? c.password || "(none)" : "(redacted \u2014 not marked seeded)",
1142
1722
  c.seeded ? "seeded" : "manual",
1143
1723
  c.hint?.en ?? "\u2014"
1144
1724
  ]);
@@ -1149,12 +1729,13 @@ ${list}`);
1149
1729
  } else {
1150
1730
  credBlock = "(not provided)";
1151
1731
  }
1732
+ 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
1733
  sections.push(
1153
1734
  `## Login Context
1154
1735
 
1155
1736
  ${credBlock}
1156
1737
 
1157
- > **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production.`
1738
+ > **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production.${redactionNote}`
1158
1739
  );
1159
1740
  const cov = computeCoverage(journey, guideChecked);
1160
1741
  const covTableRows = [
@@ -1231,7 +1812,14 @@ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked)
1231
1812
  "---",
1232
1813
  ""
1233
1814
  ].join("\n");
1234
- const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notes.map((n, i) => fmt(n, i)).join("\n");
1815
+ const noteBlocks = notes.map(
1816
+ (n, i) => noteToMarkdown(n, { brand: brandLabel, index: i + 1 })
1817
+ );
1818
+ const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
1819
+
1820
+ ---
1821
+ ` : "";
1822
+ const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
1235
1823
  zip.file("notes.md", notesMd);
1236
1824
  notes.forEach((n, i) => {
1237
1825
  if (n.screenshot && shots) {
@@ -1266,7 +1854,13 @@ function safeLocation() {
1266
1854
  var QaContext = React.createContext(null);
1267
1855
  var LANG_KEY = "lang";
1268
1856
  var GUIDE_KEY = "guide";
1857
+ var GUIDE_FAILED_KEY = "guideFailed";
1269
1858
  var LOGIN_KEY = "logins";
1859
+ var PENDING_DELETE_KEY = "pendingDeleteIds";
1860
+ var NOTICE_QUEUE_CAP = 3;
1861
+ var NOTICE_DURATION_INFO = 4e3;
1862
+ var NOTICE_DURATION_ERROR = 6e3;
1863
+ var SOFT_DELETE_MS = 5e3;
1270
1864
  function QaProvider({
1271
1865
  config,
1272
1866
  children
@@ -1274,6 +1868,7 @@ function QaProvider({
1274
1868
  const [storage] = React.useState(() => createStorage(config.namespace));
1275
1869
  const [idb] = React.useState(() => createIdb(config.namespace));
1276
1870
  const [notes, setNotes] = React.useState([]);
1871
+ const [notesLoading, setNotesLoading] = React.useState(true);
1277
1872
  const [isOpen, setIsOpen] = React.useState(false);
1278
1873
  const [activeTab, setActiveTab] = React.useState("notes");
1279
1874
  const [captureActive, setCaptureActive] = React.useState(false);
@@ -1286,23 +1881,64 @@ function QaProvider({
1286
1881
  const [guideChecked, setGuideChecked] = React.useState(
1287
1882
  () => new Set(storage.getJSON(GUIDE_KEY, []))
1288
1883
  );
1884
+ const [guideFailed, setGuideFailed] = React.useState(
1885
+ () => new Set(storage.getJSON(GUIDE_FAILED_KEY, []))
1886
+ );
1289
1887
  const [loginsUsed, setLoginsUsed] = React.useState(
1290
1888
  () => new Set(storage.getJSON(LOGIN_KEY, []))
1291
1889
  );
1890
+ const [notices, setNotices] = React.useState([]);
1891
+ const noticeTimers = React.useRef(/* @__PURE__ */ new Map());
1892
+ const [testAlong, setTestAlong] = React.useState({
1893
+ active: false,
1894
+ index: 0
1895
+ });
1896
+ const pendingDeletes = React.useRef(
1897
+ /* @__PURE__ */ new Map()
1898
+ );
1899
+ const pendingClear = React.useRef(null);
1900
+ const readPendingDeleteIds = React.useCallback(() => {
1901
+ return new Set(storage.getJSON(PENDING_DELETE_KEY, []));
1902
+ }, [storage]);
1903
+ const addPendingDeleteIds = React.useCallback((ids) => {
1904
+ if (ids.length === 0) return;
1905
+ const current = readPendingDeleteIds();
1906
+ for (const id of ids) current.add(id);
1907
+ storage.setJSON(PENDING_DELETE_KEY, [...current]);
1908
+ }, [storage, readPendingDeleteIds]);
1909
+ const removePendingDeleteIds = React.useCallback((ids) => {
1910
+ if (ids.length === 0) return;
1911
+ const current = readPendingDeleteIds();
1912
+ let changed = false;
1913
+ for (const id of ids) {
1914
+ if (current.delete(id)) changed = true;
1915
+ }
1916
+ if (changed) storage.setJSON(PENDING_DELETE_KEY, [...current]);
1917
+ }, [storage, readPendingDeleteIds]);
1292
1918
  React.useEffect(() => {
1293
1919
  let alive = true;
1294
1920
  idb.getAll().then((rows) => {
1295
1921
  if (!alive) return;
1296
- const sorted = rows.slice().sort(
1922
+ let live = rows;
1923
+ const pendingIds = storage.getJSON(PENDING_DELETE_KEY, []);
1924
+ if (pendingIds.length > 0) {
1925
+ const pendingSet = new Set(pendingIds);
1926
+ live = live.filter((n) => !pendingSet.has(n.id));
1927
+ for (const id of pendingIds) void idb.delete(id);
1928
+ storage.setJSON(PENDING_DELETE_KEY, []);
1929
+ }
1930
+ const sorted = live.slice().sort(
1297
1931
  (a, b) => a.timestamp < b.timestamp ? 1 : -1
1298
1932
  );
1299
1933
  setNotes(sorted);
1300
1934
  }).catch(() => {
1935
+ }).finally(() => {
1936
+ if (alive) setNotesLoading(false);
1301
1937
  });
1302
1938
  return () => {
1303
1939
  alive = false;
1304
1940
  };
1305
- }, [idb]);
1941
+ }, [idb, storage]);
1306
1942
  const setLang = React.useCallback((l) => {
1307
1943
  setLangState(l);
1308
1944
  storage.setItem(LANG_KEY, l);
@@ -1315,27 +1951,160 @@ function QaProvider({
1315
1951
  (value2) => pick(value2, lang),
1316
1952
  [lang]
1317
1953
  );
1954
+ const dismissNotice = React.useCallback((id) => {
1955
+ const timer = noticeTimers.current.get(id);
1956
+ if (timer) {
1957
+ clearTimeout(timer);
1958
+ noticeTimers.current.delete(id);
1959
+ }
1960
+ setNotices((prev) => prev.filter((n) => n.id !== id));
1961
+ }, []);
1962
+ const notify = React.useCallback((message, opts) => {
1963
+ const tone = opts?.tone ?? "info";
1964
+ const id = opts?.id ?? uid();
1965
+ const duration = opts?.duration ?? (tone === "error" ? NOTICE_DURATION_ERROR : NOTICE_DURATION_INFO);
1966
+ const existingTimer = noticeTimers.current.get(id);
1967
+ if (existingTimer) clearTimeout(existingTimer);
1968
+ const notice = { id, message, tone, action: opts?.action, duration };
1969
+ setNotices((prev) => {
1970
+ const deduped = prev.filter((n) => n.id !== id);
1971
+ const next = [...deduped, notice];
1972
+ if (next.length <= NOTICE_QUEUE_CAP) return next;
1973
+ const overflow = next.length - NOTICE_QUEUE_CAP;
1974
+ for (const dropped of next.slice(0, overflow)) {
1975
+ const droppedTimer = noticeTimers.current.get(dropped.id);
1976
+ if (droppedTimer) {
1977
+ clearTimeout(droppedTimer);
1978
+ noticeTimers.current.delete(dropped.id);
1979
+ }
1980
+ }
1981
+ return next.slice(overflow);
1982
+ });
1983
+ const timer = setTimeout(() => {
1984
+ noticeTimers.current.delete(id);
1985
+ setNotices((prev) => prev.filter((n) => n.id !== id));
1986
+ }, duration);
1987
+ noticeTimers.current.set(id, timer);
1988
+ return id;
1989
+ }, []);
1990
+ const testAlongSteps = React.useMemo(() => {
1991
+ const out = [];
1992
+ for (const lane of journeyOrEmpty(config.journey)) {
1993
+ const laneRole = pick2(lane.role);
1994
+ const color = lane.color ?? "var(--qa-accent)";
1995
+ for (const step of lane.steps ?? []) {
1996
+ out.push({
1997
+ key: `${lane.id}::${step.path}`,
1998
+ laneId: lane.id,
1999
+ laneRole,
2000
+ color,
2001
+ path: step.path,
2002
+ what: step.what,
2003
+ expect: step.expect,
2004
+ risk: step.risk ?? "green"
2005
+ });
2006
+ }
2007
+ }
2008
+ return out;
2009
+ }, [config.journey, pick2]);
2010
+ const startTestAlong = React.useCallback(() => {
2011
+ setTestAlong({ active: true, index: 0 });
2012
+ setIsOpen(false);
2013
+ }, []);
2014
+ const exitTestAlong = React.useCallback(() => {
2015
+ setTestAlong({ active: false, index: 0 });
2016
+ }, []);
2017
+ const gotoStep = React.useCallback((index) => {
2018
+ setTestAlong((prev) => {
2019
+ if (!prev.active) return prev;
2020
+ const maxIndex = Math.max(0, testAlongSteps.length - 1);
2021
+ const clamped = Math.max(0, Math.min(index, maxIndex));
2022
+ if (clamped === prev.index) return prev;
2023
+ return { ...prev, index: clamped };
2024
+ });
2025
+ }, [testAlongSteps.length]);
2026
+ const gradeStep = React.useCallback((key, grade) => {
2027
+ if (grade === "pass") {
2028
+ setGuideChecked((prev) => {
2029
+ const next = new Set(prev);
2030
+ next.add(key);
2031
+ storage.setJSON(GUIDE_KEY, [...next]);
2032
+ return next;
2033
+ });
2034
+ setGuideFailed((prev) => {
2035
+ const next = new Set(prev);
2036
+ next.delete(key);
2037
+ storage.setJSON(GUIDE_FAILED_KEY, [...next]);
2038
+ return next;
2039
+ });
2040
+ } else {
2041
+ setGuideFailed((prev) => {
2042
+ const next = new Set(prev);
2043
+ next.add(key);
2044
+ storage.setJSON(GUIDE_FAILED_KEY, [...next]);
2045
+ return next;
2046
+ });
2047
+ setGuideChecked((prev) => {
2048
+ const next = new Set(prev);
2049
+ next.delete(key);
2050
+ storage.setJSON(GUIDE_KEY, [...next]);
2051
+ return next;
2052
+ });
2053
+ }
2054
+ }, [storage]);
2055
+ const evidenceByStep = React.useMemo(() => {
2056
+ const map = /* @__PURE__ */ new Map();
2057
+ for (let i = notes.length - 1; i >= 0; i--) {
2058
+ const note = notes[i];
2059
+ const ref = note.journeyRef;
2060
+ if (!ref) continue;
2061
+ const key = `${ref.laneId}::${ref.path}`;
2062
+ const arr = map.get(key);
2063
+ if (arr) arr.push(note);
2064
+ else map.set(key, [note]);
2065
+ }
2066
+ return map;
2067
+ }, [notes]);
1318
2068
  const addNote = React.useCallback(
1319
- async ({
1320
- description,
1321
- screenshot,
1322
- target
1323
- }) => {
2069
+ async (input) => {
1324
2070
  const loc = safeLocation();
2071
+ const route = loc.pathname + (loc.search ? "?\u2026" : "");
2072
+ let journeyRef;
2073
+ if (testAlong.active) {
2074
+ const step = testAlongSteps[testAlong.index];
2075
+ if (step) journeyRef = { laneId: step.laneId, path: step.path };
2076
+ } else {
2077
+ const hits = matchRouteToSteps(config.journey, route);
2078
+ if (hits.length) journeyRef = hits[0];
2079
+ }
2080
+ let context;
2081
+ if (config.captureContext !== false) {
2082
+ context = {
2083
+ events: drainSinceLastNote(),
2084
+ env: collectEnvSnapshot(route),
2085
+ forensics: input.forensics
2086
+ };
2087
+ }
1325
2088
  const note = {
1326
2089
  id: uid(),
1327
- url: loc.href,
1328
- route: loc.pathname + loc.search,
2090
+ url: redactUrl(loc.href),
2091
+ route,
1329
2092
  timestamp: nowIso(),
1330
- description: (description || "").trim(),
1331
- screenshot: screenshot ?? void 0,
1332
- target: target ?? void 0
2093
+ description: (input.description || "").trim(),
2094
+ screenshot: input.screenshot ?? void 0,
2095
+ target: input.target ?? void 0,
2096
+ severity: input.severity,
2097
+ status: input.status,
2098
+ journeyRef,
2099
+ context
1333
2100
  };
1334
2101
  setNotes((prev) => [note, ...prev]);
1335
- await idb.put(note);
1336
- return note;
2102
+ const persisted = await idb.put(note);
2103
+ if (!persisted) {
2104
+ notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
2105
+ }
1337
2106
  },
1338
- [idb]
2107
+ [idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t]
1339
2108
  );
1340
2109
  const updateNote = React.useCallback(
1341
2110
  async (id, patch) => {
@@ -1350,27 +2119,132 @@ function QaProvider({
1350
2119
  } else if (patch.screenshot !== void 0) {
1351
2120
  next.screenshot = patch.screenshot;
1352
2121
  }
2122
+ if (patch.severity !== void 0) next.severity = patch.severity;
2123
+ if (patch.status !== void 0) next.status = patch.status;
1353
2124
  updated = next;
1354
2125
  return next;
1355
2126
  })
1356
2127
  );
1357
2128
  if (updated) {
1358
- await idb.put(updated);
2129
+ const persisted = await idb.put(updated);
2130
+ if (!persisted) {
2131
+ notify(t("persist_failed"), { tone: "error", id: "persist_failed" });
2132
+ }
1359
2133
  }
1360
2134
  },
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]
2135
+ [idb, notify, t]
1369
2136
  );
1370
- const clearAll = React.useCallback(async () => {
1371
- setNotes([]);
1372
- await idb.clear();
1373
- }, [idb]);
2137
+ const deleteNote = React.useCallback(async (id) => {
2138
+ let removedNote = null;
2139
+ let removedAfterId = null;
2140
+ let found = false;
2141
+ setNotes((prev) => {
2142
+ const idx = prev.findIndex((n) => n.id === id);
2143
+ if (idx === -1) return prev;
2144
+ found = true;
2145
+ removedNote = prev[idx];
2146
+ removedAfterId = prev[idx + 1]?.id ?? null;
2147
+ return prev.filter((n) => n.id !== id);
2148
+ });
2149
+ if (!removedNote || !found) return;
2150
+ const noteToRestore = removedNote;
2151
+ const afterIdToRestore = removedAfterId;
2152
+ const existingPending = pendingDeletes.current.get(id);
2153
+ if (existingPending) clearTimeout(existingPending.timer);
2154
+ addPendingDeleteIds([id]);
2155
+ const timer = setTimeout(() => {
2156
+ pendingDeletes.current.delete(id);
2157
+ void idb.delete(id).then(() => removePendingDeleteIds([id]));
2158
+ }, SOFT_DELETE_MS);
2159
+ pendingDeletes.current.set(id, { note: noteToRestore, afterId: afterIdToRestore, timer });
2160
+ notify(t("note_deleted"), {
2161
+ duration: SOFT_DELETE_MS,
2162
+ id: `delete-${id}`,
2163
+ action: {
2164
+ label: t("undo"),
2165
+ onAction: () => {
2166
+ const pending = pendingDeletes.current.get(id);
2167
+ if (!pending) return;
2168
+ clearTimeout(pending.timer);
2169
+ pendingDeletes.current.delete(id);
2170
+ removePendingDeleteIds([id]);
2171
+ setNotes((prev) => {
2172
+ if (prev.some((n) => n.id === id)) return prev;
2173
+ const next = prev.slice();
2174
+ const anchorIndex = pending.afterId != null ? next.findIndex((n) => n.id === pending.afterId) : -1;
2175
+ const insertAt = anchorIndex === -1 ? next.length : anchorIndex;
2176
+ next.splice(insertAt, 0, pending.note);
2177
+ return next;
2178
+ });
2179
+ }
2180
+ }
2181
+ });
2182
+ }, [idb, notify, t, addPendingDeleteIds, removePendingDeleteIds]);
2183
+ const clearNotes = React.useCallback(async () => {
2184
+ let snapshot = [];
2185
+ setNotes((prev) => {
2186
+ snapshot = prev;
2187
+ return [];
2188
+ });
2189
+ if (pendingClear.current) clearTimeout(pendingClear.current.timer);
2190
+ for (const [pendingId, pending] of pendingDeletes.current) {
2191
+ clearTimeout(pending.timer);
2192
+ dismissNotice(`delete-${pendingId}`);
2193
+ void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
2194
+ }
2195
+ pendingDeletes.current.clear();
2196
+ const snapshotIds = snapshot.map((n) => n.id);
2197
+ addPendingDeleteIds(snapshotIds);
2198
+ const timer = setTimeout(() => {
2199
+ pendingClear.current = null;
2200
+ void Promise.all(snapshot.map((n) => idb.delete(n.id))).then(
2201
+ () => removePendingDeleteIds(snapshotIds)
2202
+ );
2203
+ }, SOFT_DELETE_MS);
2204
+ pendingClear.current = { notes: snapshot, timer };
2205
+ notify(t("notes_cleared"), {
2206
+ duration: SOFT_DELETE_MS,
2207
+ id: "clear-all",
2208
+ action: {
2209
+ label: t("undo"),
2210
+ onAction: () => {
2211
+ const pending = pendingClear.current;
2212
+ if (!pending) return;
2213
+ clearTimeout(pending.timer);
2214
+ pendingClear.current = null;
2215
+ removePendingDeleteIds(pending.notes.map((n) => n.id));
2216
+ setNotes(pending.notes);
2217
+ }
2218
+ }
2219
+ });
2220
+ }, [idb, notify, dismissNotice, t, addPendingDeleteIds, removePendingDeleteIds]);
2221
+ const flushPendingDeletes = React.useCallback(() => {
2222
+ for (const [pendingId, pending] of pendingDeletes.current) {
2223
+ clearTimeout(pending.timer);
2224
+ void idb.delete(pendingId).then(() => removePendingDeleteIds([pendingId]));
2225
+ }
2226
+ pendingDeletes.current.clear();
2227
+ if (pendingClear.current) {
2228
+ const { notes: clearedNotes } = pendingClear.current;
2229
+ clearTimeout(pendingClear.current.timer);
2230
+ pendingClear.current = null;
2231
+ const clearedIds = clearedNotes.map((n) => n.id);
2232
+ void Promise.all(clearedNotes.map((n) => idb.delete(n.id))).then(
2233
+ () => removePendingDeleteIds(clearedIds)
2234
+ );
2235
+ }
2236
+ }, [idb, removePendingDeleteIds]);
2237
+ React.useEffect(() => {
2238
+ if (typeof window === "undefined") return void 0;
2239
+ const onBeforeUnload = () => flushPendingDeletes();
2240
+ window.addEventListener("beforeunload", onBeforeUnload);
2241
+ return () => {
2242
+ window.removeEventListener("beforeunload", onBeforeUnload);
2243
+ flushPendingDeletes();
2244
+ for (const timer of noticeTimers.current.values()) clearTimeout(timer);
2245
+ noticeTimers.current.clear();
2246
+ };
2247
+ }, [flushPendingDeletes]);
1374
2248
  const startCapture = React.useCallback(() => {
1375
2249
  setIsOpen(false);
1376
2250
  setCaptureActive(true);
@@ -1415,17 +2289,19 @@ function QaProvider({
1415
2289
  // Data
1416
2290
  notes,
1417
2291
  guideChecked,
2292
+ guideFailed,
1418
2293
  loginsUsed,
1419
2294
  // UI state
1420
2295
  isOpen,
1421
2296
  activeTab,
1422
2297
  captureActive,
1423
2298
  isExporting,
2299
+ notesLoading,
1424
2300
  // i18n
1425
2301
  lang,
1426
2302
  dir: lang === "ar" ? "rtl" : "ltr",
1427
2303
  // Config passthrough
1428
- theme: config.theme,
2304
+ namespace: config.namespace,
1429
2305
  brand: config.brand,
1430
2306
  loginField: config.loginField,
1431
2307
  credentials: config.credentials,
@@ -1434,6 +2310,10 @@ function QaProvider({
1434
2310
  // i18n helpers
1435
2311
  t,
1436
2312
  pick: pick2,
2313
+ // Notices
2314
+ notices,
2315
+ notify,
2316
+ dismissNotice,
1437
2317
  // Actions
1438
2318
  setIsOpen,
1439
2319
  setActiveTab,
@@ -1441,15 +2321,26 @@ function QaProvider({
1441
2321
  addNote,
1442
2322
  updateNote,
1443
2323
  deleteNote,
1444
- clearAll,
2324
+ clearNotes,
1445
2325
  startCapture,
1446
2326
  endCapture,
1447
2327
  toggleGuide,
1448
2328
  toggleLogin,
2329
+ // Test-along
2330
+ testAlong,
2331
+ testAlongSteps,
2332
+ startTestAlong,
2333
+ exitTestAlong,
2334
+ gotoStep,
2335
+ gradeStep,
2336
+ evidenceByStep,
1449
2337
  exportZip: exportZipFn
1450
2338
  };
1451
2339
  return /* @__PURE__ */ jsxRuntime.jsx(QaContext.Provider, { value, children });
1452
2340
  }
2341
+ function journeyOrEmpty(journey) {
2342
+ return Array.isArray(journey) ? journey : [];
2343
+ }
1453
2344
  function useQa() {
1454
2345
  const ctx = React.useContext(QaContext);
1455
2346
  if (!ctx) throw new Error("useQa must be used inside <QaProvider>");
@@ -1459,6 +2350,37 @@ var ICONS = {
1459
2350
  Check: [
1460
2351
  ["path", { d: "M20 6 9 17l-5-5" }]
1461
2352
  ],
2353
+ Bug: [
2354
+ ["path", { d: "m8 2 1.88 1.88" }],
2355
+ ["path", { d: "M14.12 3.88 16 2" }],
2356
+ ["path", { d: "M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" }],
2357
+ ["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" }],
2358
+ ["path", { d: "M12 20v-9" }],
2359
+ ["path", { d: "M6.53 9C4.6 8.8 3 7.1 3 5" }],
2360
+ ["path", { d: "M6 13H2" }],
2361
+ ["path", { d: "M3 21c0-2.1 1.7-3.9 3.8-4" }],
2362
+ ["path", { d: "M20.97 5c0 2.1-1.6 3.8-3.5 4" }],
2363
+ ["path", { d: "M22 13h-4" }],
2364
+ ["path", { d: "M17.2 17c2.1.1 3.8 1.9 3.8 4" }]
2365
+ ],
2366
+ AlertTriangle: [
2367
+ ["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" }],
2368
+ ["path", { d: "M12 9v4" }],
2369
+ ["path", { d: "M12 17h.01" }]
2370
+ ],
2371
+ RotateCcw: [
2372
+ ["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }],
2373
+ ["path", { d: "M3 3v5h5" }]
2374
+ ],
2375
+ ChevronLeft: [
2376
+ ["path", { d: "m15 18-6-6 6-6" }]
2377
+ ],
2378
+ ChevronRight: [
2379
+ ["path", { d: "m9 18 6-6-6-6" }]
2380
+ ],
2381
+ Play: [
2382
+ ["polygon", { points: "6 3 20 12 6 21 6 3" }]
2383
+ ],
1462
2384
  X: [
1463
2385
  ["path", { d: "M18 6 6 18" }],
1464
2386
  ["path", { d: "m6 6 12 12" }]
@@ -1632,27 +2554,41 @@ var DEFAULT_BOTTOM = "calc(5rem + env(safe-area-inset-bottom))";
1632
2554
  var FAB_SIZE_PX = 56;
1633
2555
  var EDGE_MARGIN = 12;
1634
2556
  var DRAG_THRESHOLD = 8;
1635
- var FAB_POS_KEY = "qapture:fabpos";
2557
+ var LEGACY_FAB_POS_KEY = "qapture:fabpos";
2558
+ function fabPosKey(namespace) {
2559
+ return `${namespace}:fabpos`;
2560
+ }
1636
2561
  function isFabPos(v) {
1637
2562
  if (!v || typeof v !== "object") return false;
1638
2563
  const o = v;
1639
2564
  return typeof o.left === "number" && Number.isFinite(o.left) && typeof o.bottom === "number" && Number.isFinite(o.bottom);
1640
2565
  }
1641
- function loadFabPos() {
2566
+ function loadFabPos(namespace) {
1642
2567
  if (typeof window === "undefined") return null;
2568
+ const key = fabPosKey(namespace);
1643
2569
  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;
2570
+ const raw = window.localStorage.getItem(key);
2571
+ if (raw) {
2572
+ const parsed = JSON.parse(raw);
2573
+ if (isFabPos(parsed)) return parsed;
2574
+ }
2575
+ const legacyRaw = window.localStorage.getItem(LEGACY_FAB_POS_KEY);
2576
+ if (!legacyRaw) return null;
2577
+ const legacyParsed = JSON.parse(legacyRaw);
2578
+ if (!isFabPos(legacyParsed)) return null;
2579
+ try {
2580
+ window.localStorage.setItem(key, JSON.stringify(legacyParsed));
2581
+ } catch {
2582
+ }
2583
+ return legacyParsed;
1648
2584
  } catch {
1649
2585
  return null;
1650
2586
  }
1651
2587
  }
1652
- function saveFabPos(pos) {
2588
+ function saveFabPos(namespace, pos) {
1653
2589
  if (typeof window === "undefined") return;
1654
2590
  try {
1655
- window.localStorage.setItem(FAB_POS_KEY, JSON.stringify(pos));
2591
+ window.localStorage.setItem(fabPosKey(namespace), JSON.stringify(pos));
1656
2592
  } catch {
1657
2593
  }
1658
2594
  }
@@ -1669,9 +2605,9 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
1669
2605
  };
1670
2606
  }
1671
2607
  function QaFab() {
1672
- const { isOpen, setIsOpen, notes, captureActive, theme } = useQa();
2608
+ const { isOpen, setIsOpen, notes, captureActive, namespace } = useQa();
1673
2609
  const coarse = useCoarsePointer();
1674
- const [pos, setPos] = React.useState(() => loadFabPos());
2610
+ const [pos, setPos] = React.useState(() => loadFabPos(namespace));
1675
2611
  const dragRef = React.useRef(null);
1676
2612
  const didDragRef = React.useRef(false);
1677
2613
  const [, setViewportTick] = React.useState(0);
@@ -1735,7 +2671,7 @@ function QaFab() {
1735
2671
  const dy = e.clientY - d.startY;
1736
2672
  const next = clampFabPos({ left: d.startLeft + dx, bottom: d.startBottom - dy }, d.width, d.height);
1737
2673
  setPos(next);
1738
- saveFabPos(next);
2674
+ saveFabPos(namespace, next);
1739
2675
  didDragRef.current = true;
1740
2676
  }
1741
2677
  };
@@ -1755,9 +2691,7 @@ function QaFab() {
1755
2691
  bottom: applied ? `${applied.bottom}px` : DEFAULT_BOTTOM,
1756
2692
  width: "3.5rem",
1757
2693
  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
2694
+ zIndex: "var(--qa-z-fab)"
1761
2695
  };
1762
2696
  return /* @__PURE__ */ jsxRuntime.jsxs(
1763
2697
  "button",
@@ -1772,7 +2706,7 @@ function QaFab() {
1772
2706
  onPointerCancel: coarse ? onPointerCancel : void 0,
1773
2707
  "aria-label": "Qapture \u2014 testing notes",
1774
2708
  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" : ""}`,
2709
+ 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
2710
  style: fabStyle,
1777
2711
  children: [
1778
2712
  !isOpen && /* @__PURE__ */ jsxRuntime.jsx(
@@ -1787,17 +2721,14 @@ function QaFab() {
1787
2721
  !isOpen && notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
1788
2722
  "span",
1789
2723
  {
1790
- className: "qa-absolute qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-xs qa-font-bold",
2724
+ 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
2725
  "aria-label": `${notes.length} notes`,
1792
2726
  style: {
1793
2727
  top: "-4px",
1794
2728
  right: "-4px",
1795
2729
  minWidth: "1.5rem",
1796
2730
  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)"
2731
+ padding: "0 4px"
1801
2732
  },
1802
2733
  children: notes.length
1803
2734
  }
@@ -1806,13 +2737,47 @@ function QaFab() {
1806
2737
  }
1807
2738
  );
1808
2739
  }
2740
+ var SEVERITIES = [
2741
+ { value: "bug", labelKey: "sev_bug", icon: "Bug" },
2742
+ { value: "question", labelKey: "sev_question" },
2743
+ { value: "polish", labelKey: "sev_polish" }
2744
+ ];
2745
+ function SeverityChipRow({
2746
+ value,
2747
+ onChange,
2748
+ t
2749
+ }) {
2750
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
2751
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mb-1 qa-text-11 qa-text-lo", children: t("severity_label") }),
2752
+ /* @__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) => {
2753
+ const active = value === s.value;
2754
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2755
+ "button",
2756
+ {
2757
+ type: "button",
2758
+ role: "radio",
2759
+ "aria-checked": active,
2760
+ onClick: () => onChange(s.value),
2761
+ 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"}`,
2762
+ style: { border: "none", cursor: "pointer" },
2763
+ children: [
2764
+ s.icon && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: s.icon, size: 12 }),
2765
+ t(s.labelKey)
2766
+ ]
2767
+ },
2768
+ s.value
2769
+ );
2770
+ }) })
2771
+ ] });
2772
+ }
1809
2773
  function NoteEditor() {
1810
- const { addNote, startCapture, t, theme } = useQa();
2774
+ const { addNote, startCapture, t } = useQa();
1811
2775
  const [open, setOpen] = React.useState(false);
1812
2776
  const [description, setDescription] = React.useState("");
1813
2777
  const [screenshot, setScreenshot] = React.useState(null);
1814
2778
  const [previewUrl, setPreviewUrl] = React.useState(null);
1815
2779
  const [dragOver, setDragOver] = React.useState(false);
2780
+ const [severity, setSeverity] = React.useState("bug");
1816
2781
  const fileRef = React.useRef(null);
1817
2782
  const previewUrlRef = React.useRef(null);
1818
2783
  React.useEffect(() => {
@@ -1863,11 +2828,18 @@ function NoteEditor() {
1863
2828
  if (f?.type.startsWith("image/")) setImage(f);
1864
2829
  e.target.value = "";
1865
2830
  };
2831
+ const resetForm = () => {
2832
+ setOpen(false);
2833
+ clearImage();
2834
+ setDescription("");
2835
+ setSeverity("bug");
2836
+ };
1866
2837
  const save = async () => {
1867
2838
  if (!description.trim()) return;
1868
- await addNote({ description, screenshot: screenshot ?? void 0 });
2839
+ await addNote({ description, screenshot: screenshot ?? void 0, severity });
1869
2840
  setDescription("");
1870
2841
  clearImage();
2842
+ setSeverity("bug");
1871
2843
  setOpen(false);
1872
2844
  };
1873
2845
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", children: [
@@ -1875,12 +2847,8 @@ function NoteEditor() {
1875
2847
  "button",
1876
2848
  {
1877
2849
  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
- },
2850
+ 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",
2851
+ style: { border: "none", cursor: "pointer" },
1884
2852
  children: [
1885
2853
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 16 }),
1886
2854
  t("capture_cta")
@@ -1891,13 +2859,8 @@ function NoteEditor() {
1891
2859
  "button",
1892
2860
  {
1893
2861
  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
- },
2862
+ 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",
2863
+ style: { background: "transparent", cursor: "pointer" },
1901
2864
  children: [
1902
2865
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 14 }),
1903
2866
  t("quick_note")
@@ -1907,8 +2870,7 @@ function NoteEditor() {
1907
2870
  "div",
1908
2871
  {
1909
2872
  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 },
2873
+ className: "qa-space-y-2 qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-2.5",
1912
2874
  children: [
1913
2875
  /* @__PURE__ */ jsxRuntime.jsx(
1914
2876
  "textarea",
@@ -1918,10 +2880,10 @@ function NoteEditor() {
1918
2880
  onChange: (e) => setDescription(e.target.value),
1919
2881
  rows: 3,
1920
2882
  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" }
2883
+ 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
2884
  }
1924
2885
  ),
2886
+ /* @__PURE__ */ jsxRuntime.jsx(SeverityChipRow, { value: severity, onChange: setSeverity, t }),
1925
2887
  /* @__PURE__ */ jsxRuntime.jsxs(
1926
2888
  "div",
1927
2889
  {
@@ -1931,11 +2893,7 @@ function NoteEditor() {
1931
2893
  },
1932
2894
  onDragLeave: () => setDragOver(false),
1933
2895
  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
- },
2896
+ 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
2897
  children: [
1940
2898
  previewUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-relative qa-inline-block", children: [
1941
2899
  /* @__PURE__ */ jsxRuntime.jsx("img", { src: previewUrl, alt: "preview", style: { maxHeight: "7rem", borderRadius: "0.25rem" } }),
@@ -1943,11 +2901,10 @@ function NoteEditor() {
1943
2901
  "button",
1944
2902
  {
1945
2903
  onClick: clearImage,
1946
- className: "qa-absolute qa-rounded-full qa-p-1 qa-text-white qa-tap-icon",
2904
+ className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
1947
2905
  style: {
1948
2906
  top: "-8px",
1949
2907
  insetInlineEnd: "-8px",
1950
- background: theme.primary,
1951
2908
  border: "none",
1952
2909
  cursor: "pointer"
1953
2910
  },
@@ -1958,8 +2915,8 @@ function NoteEditor() {
1958
2915
  "button",
1959
2916
  {
1960
2917
  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" },
2918
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
2919
+ style: { background: "transparent", border: "none", cursor: "pointer" },
1963
2920
  children: [
1964
2921
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ImagePlus", size: 16 }),
1965
2922
  t("image_hint")
@@ -1983,28 +2940,19 @@ function NoteEditor() {
1983
2940
  /* @__PURE__ */ jsxRuntime.jsx(
1984
2941
  "button",
1985
2942
  {
1986
- onClick: save,
2943
+ onClick: () => void save(),
1987
2944
  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" },
2945
+ 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",
2946
+ style: { border: "none", cursor: "pointer" },
1990
2947
  children: t("add_point")
1991
2948
  }
1992
2949
  ),
1993
2950
  /* @__PURE__ */ jsxRuntime.jsx(
1994
2951
  "button",
1995
2952
  {
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
- },
2953
+ onClick: resetForm,
2954
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
2955
+ style: { background: "transparent", cursor: "pointer" },
2008
2956
  children: t("cancel")
2009
2957
  }
2010
2958
  )
@@ -2017,16 +2965,11 @@ function NoteEditor() {
2017
2965
 
2018
2966
  // src/lib/highlight.ts
2019
2967
  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) {
2968
+ var ACCENT = "#4D9CFF";
2969
+ var DANGER = "#FF6B6B";
2970
+ function paint(rect) {
2026
2971
  if (typeof document === "undefined") return;
2027
2972
  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
2973
  const box = document.createElement("div");
2031
2974
  box.setAttribute("data-qa-overlay", "true");
2032
2975
  Object.assign(box.style, {
@@ -2038,9 +2981,9 @@ function paint(rect, colors) {
2038
2981
  zIndex: "10098",
2039
2982
  pointerEvents: "none",
2040
2983
  borderRadius: "3px",
2041
- outline: `3px solid ${accent}`,
2042
- background: `${accent}22`,
2043
- boxShadow: `0 0 0 4px ${primary}55`,
2984
+ outline: `3px solid ${ACCENT}`,
2985
+ background: `${ACCENT}22`,
2986
+ boxShadow: `0 0 0 4px ${DANGER}55`,
2044
2987
  transition: "opacity 0.45s ease",
2045
2988
  opacity: "1"
2046
2989
  });
@@ -2052,9 +2995,9 @@ function paint(rect, colors) {
2052
2995
  if (box.parentNode) box.remove();
2053
2996
  }, 1500);
2054
2997
  }
2055
- function settleThenPaint(el, colors) {
2056
- const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
2057
- const start = now();
2998
+ function settleThenPaint(el) {
2999
+ const now2 = () => typeof performance !== "undefined" ? performance.now() : Date.now();
3000
+ const start = now2();
2058
3001
  let last = null;
2059
3002
  let stableFrames = 0;
2060
3003
  const tick = () => {
@@ -2062,15 +3005,15 @@ function settleThenPaint(el, colors) {
2062
3005
  const unchanged = !!last && r.top === last.top && r.left === last.left && r.width === last.width && r.height === last.height;
2063
3006
  stableFrames = unchanged ? stableFrames + 1 : 0;
2064
3007
  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);
3008
+ if (stableFrames >= 2 || now2() - start >= SETTLE_TIMEOUT_MS) {
3009
+ paint({ top: r.top, left: r.left, width: r.width, height: r.height });
2067
3010
  return;
2068
3011
  }
2069
3012
  requestAnimationFrame(tick);
2070
3013
  };
2071
3014
  requestAnimationFrame(tick);
2072
3015
  }
2073
- function flashLocate(target, colors) {
3016
+ function flashLocate(target) {
2074
3017
  if (typeof document === "undefined" || !target) return;
2075
3018
  let el = null;
2076
3019
  if (target.selector) {
@@ -2082,7 +3025,7 @@ function flashLocate(target, colors) {
2082
3025
  }
2083
3026
  if (el) {
2084
3027
  el.scrollIntoView({ block: "center", inline: "center" });
2085
- settleThenPaint(el, colors);
3028
+ settleThenPaint(el);
2086
3029
  } else if (target.rect) {
2087
3030
  let rect = target.rect;
2088
3031
  const snap = target.scroll;
@@ -2093,106 +3036,92 @@ function flashLocate(target, colors) {
2093
3036
  rect = { ...rect, left: rect.left - dx, top: rect.top - dy };
2094
3037
  }
2095
3038
  }
2096
- paint(rect, colors);
3039
+ paint(rect);
2097
3040
  }
2098
3041
  }
2099
3042
  function LocationReveal({ target }) {
2100
- const { t, theme } = useQa();
3043
+ const { t } = useQa();
2101
3044
  const [open, setOpen] = React.useState(false);
2102
3045
  if (!target) return null;
2103
3046
  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
- ]
3047
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-lg qa-border qa-border-subtle qa-bg-2", children: [
3048
+ /* @__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: [
3049
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 14, className: "qa-text-success" }),
3050
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-font-medium qa-text-hi", children: t("loc_captured") }),
3051
+ /* @__PURE__ */ jsxRuntime.jsxs(
3052
+ "button",
3053
+ {
3054
+ onClick: () => setOpen((o) => !o),
3055
+ className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-font-medium qa-tap qa-text-accent",
3056
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3057
+ children: [
3058
+ open ? t("loc_hide") : t("loc_show"),
3059
+ /* @__PURE__ */ jsxRuntime.jsx(
3060
+ Icon,
3061
+ {
3062
+ name: "ChevronDown",
3063
+ size: 14,
3064
+ style: {
3065
+ transition: "transform 150ms",
3066
+ transform: open ? "rotate(180deg)" : "rotate(0deg)"
2188
3067
  }
2189
- )
2190
- ]
3068
+ }
3069
+ )
3070
+ ]
3071
+ }
3072
+ )
3073
+ ] }),
3074
+ 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: [
3075
+ target.selector && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-1", children: [
3076
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "selector" }),
3077
+ /* @__PURE__ */ jsxRuntime.jsx(
3078
+ "code",
3079
+ {
3080
+ className: "qa-min-w-0 qa-flex-1 qa-truncate qa-rounded qa-bg-3 qa-px-1",
3081
+ title: target.selector,
3082
+ children: target.selector
2191
3083
  }
2192
3084
  )
2193
- ]
2194
- }
2195
- );
3085
+ ] }),
3086
+ target.tagName && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3087
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "tag " }),
3088
+ /* @__PURE__ */ jsxRuntime.jsxs("code", { className: "qa-rounded qa-bg-3 qa-px-1", children: [
3089
+ "<",
3090
+ target.tagName,
3091
+ ">"
3092
+ ] })
3093
+ ] }),
3094
+ target.text && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-truncate", children: [
3095
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "text " }),
3096
+ '"',
3097
+ target.text,
3098
+ '"'
3099
+ ] }),
3100
+ r && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
3101
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "pos " }),
3102
+ Math.round(r.left),
3103
+ ", ",
3104
+ Math.round(r.top),
3105
+ " \xB7 ",
3106
+ Math.round(r.width),
3107
+ "\xD7",
3108
+ Math.round(r.height)
3109
+ ] }),
3110
+ /* @__PURE__ */ jsxRuntime.jsxs(
3111
+ "button",
3112
+ {
3113
+ onClick: () => flashLocate(target),
3114
+ 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",
3115
+ style: { border: "none", cursor: "pointer" },
3116
+ children: [
3117
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 12 }),
3118
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPinned", size: 12 }),
3119
+ t("loc_locate")
3120
+ ]
3121
+ }
3122
+ )
3123
+ ] })
3124
+ ] });
2196
3125
  }
2197
3126
  function useObjectUrl(blob) {
2198
3127
  const [url, setUrl] = React.useState(null);
@@ -2209,11 +3138,10 @@ function useObjectUrl(blob) {
2209
3138
  }
2210
3139
  function KindBadge({
2211
3140
  target,
2212
- t,
2213
- theme
3141
+ t
2214
3142
  }) {
2215
3143
  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: [
3144
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-lo", children: [
2217
3145
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "FileText", size: 12 }),
2218
3146
  t("kind_note")
2219
3147
  ] });
@@ -2222,8 +3150,7 @@ function KindBadge({
2222
3150
  return /* @__PURE__ */ jsxRuntime.jsxs(
2223
3151
  "span",
2224
3152
  {
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 },
3153
+ 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
3154
  children: [
2228
3155
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: region ? "Square" : "MousePointerClick", size: 10 }),
2229
3156
  region ? t("kind_region") : t("kind_element")
@@ -2231,13 +3158,59 @@ function KindBadge({
2231
3158
  }
2232
3159
  );
2233
3160
  }
3161
+ var SEVERITY_CLASS = {
3162
+ bug: "qa-bg-danger-tint qa-text-danger",
3163
+ question: "qa-bg-warn-tint qa-text-warn",
3164
+ polish: "qa-bg-accent-tint qa-text-accent"
3165
+ };
3166
+ var SEVERITY_LABEL_KEY = {
3167
+ bug: "sev_bug",
3168
+ question: "sev_question",
3169
+ polish: "sev_polish"
3170
+ };
3171
+ function SeverityChip({ severity, t }) {
3172
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3173
+ "span",
3174
+ {
3175
+ 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]}`,
3176
+ children: [
3177
+ severity === "bug" && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Bug", size: 10 }),
3178
+ t(SEVERITY_LABEL_KEY[severity])
3179
+ ]
3180
+ }
3181
+ );
3182
+ }
3183
+ function StatusPill({
3184
+ status,
3185
+ onToggle,
3186
+ t
3187
+ }) {
3188
+ const verified = status === "verified";
3189
+ return /* @__PURE__ */ jsxRuntime.jsxs(
3190
+ "button",
3191
+ {
3192
+ type: "button",
3193
+ onClick: onToggle,
3194
+ "aria-label": t(verified ? "status_verified" : "status_open"),
3195
+ 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"}`,
3196
+ style: { border: "none", cursor: "pointer" },
3197
+ children: [
3198
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: verified ? "CheckCircle2" : "Circle", size: 10 }),
3199
+ t(verified ? "status_verified" : "status_open")
3200
+ ]
3201
+ }
3202
+ );
3203
+ }
2234
3204
  function NoteItem({ note, index }) {
2235
- const { deleteNote, updateNote, t, theme } = useQa();
3205
+ const { deleteNote, updateNote, notify, t } = useQa();
2236
3206
  const [editing, setEditing] = React.useState(false);
2237
3207
  const [desc, setDesc] = React.useState(note.description);
2238
3208
  const [img, setImg] = React.useState(note.screenshot ?? null);
2239
3209
  const fileRef = React.useRef(null);
2240
3210
  const thumbUrl = useObjectUrl(editing ? img ?? void 0 : note.screenshot);
3211
+ const severity = note.severity ?? "bug";
3212
+ const status = note.status ?? "open";
3213
+ const contextEventCount = note.context?.events.length ?? 0;
2241
3214
  const startEdit = () => {
2242
3215
  setDesc(note.description);
2243
3216
  setImg(note.screenshot ?? null);
@@ -2266,188 +3239,178 @@ function NoteItem({ note, index }) {
2266
3239
  updateNote(note.id, patch);
2267
3240
  setEditing(false);
2268
3241
  };
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: [
3242
+ const toggleStatus = () => {
3243
+ updateNote(note.id, { status: status === "open" ? "verified" : "open" });
3244
+ };
3245
+ const copyPrompt = async () => {
3246
+ try {
3247
+ if (!navigator.clipboard?.writeText) throw new Error("clipboard unavailable");
3248
+ await navigator.clipboard.writeText(noteToMarkdown(note));
3249
+ notify(t("copied"));
3250
+ } catch {
3251
+ notify(t("copy_failed"), { tone: "error" });
3252
+ }
3253
+ };
3254
+ 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: [
3255
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-1 qa-flex qa-flex-wrap qa-items-center qa-gap-1.5", children: [
3256
+ /* @__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 }),
3257
+ /* @__PURE__ */ jsxRuntime.jsx(KindBadge, { target: note.target, t }),
3258
+ /* @__PURE__ */ jsxRuntime.jsx(SeverityChip, { severity, t }),
3259
+ /* @__PURE__ */ jsxRuntime.jsx(StatusPill, { status, onToggle: toggleStatus, t }),
3260
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-ms-auto qa-flex qa-items-center qa-gap-1.5", children: [
3261
+ /* @__PURE__ */ jsxRuntime.jsx(
3262
+ "button",
3263
+ {
3264
+ onClick: () => void copyPrompt(),
3265
+ className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
3266
+ title: t("copy_prompt"),
3267
+ "aria-label": t("copy_prompt"),
3268
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3269
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Copy", size: 14 })
3270
+ }
3271
+ ),
3272
+ !editing && /* @__PURE__ */ jsxRuntime.jsx(
3273
+ "button",
3274
+ {
3275
+ onClick: startEdit,
3276
+ className: "qa-tap-icon qa-text-mid qa-hover-text-slate-600",
3277
+ title: t("edit"),
3278
+ "aria-label": t("edit"),
3279
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3280
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 14 })
3281
+ }
3282
+ ),
3283
+ /* @__PURE__ */ jsxRuntime.jsx(
3284
+ "button",
3285
+ {
3286
+ onClick: () => deleteNote(note.id),
3287
+ className: "qa-tap-icon qa-text-mid qa-hover-text-red",
3288
+ "aria-label": "delete",
3289
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3290
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash2", size: 16 })
3291
+ }
3292
+ )
3293
+ ] })
3294
+ ] }),
3295
+ editing ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2", onPaste, children: [
3296
+ /* @__PURE__ */ jsxRuntime.jsx(
3297
+ "textarea",
3298
+ {
3299
+ autoFocus: true,
3300
+ value: desc,
3301
+ onChange: (e) => setDesc(e.target.value),
3302
+ rows: 3,
3303
+ 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"
3304
+ }
3305
+ ),
3306
+ /* @__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: [
3307
+ thumbUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-relative qa-inline-block", children: [
2310
3308
  /* @__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",
3309
+ "img",
2323
3310
  {
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
- ]
3311
+ src: thumbUrl,
3312
+ alt: "screenshot",
3313
+ style: { maxHeight: "7rem", borderRadius: "0.25rem" }
2375
3314
  }
2376
3315
  ),
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
3316
  /* @__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",
3317
+ "button",
2424
3318
  {
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` }
3319
+ onClick: () => setImg(null),
3320
+ className: "qa-tap-icon qa-absolute qa-rounded-full qa-bg-danger-tint qa-text-danger",
3321
+ title: t("remove_image"),
3322
+ style: {
3323
+ top: "-8px",
3324
+ insetInlineEnd: "-8px",
3325
+ border: "none",
3326
+ cursor: "pointer"
3327
+ },
3328
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 12 })
2429
3329
  }
2430
3330
  )
2431
- ] })
2432
- ]
2433
- }
2434
- );
3331
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
3332
+ "button",
3333
+ {
3334
+ onClick: () => fileRef.current?.click(),
3335
+ className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-accent",
3336
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3337
+ children: [
3338
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ImagePlus", size: 16 }),
3339
+ t("image_hint")
3340
+ ]
3341
+ }
3342
+ ),
3343
+ /* @__PURE__ */ jsxRuntime.jsx(
3344
+ "input",
3345
+ {
3346
+ ref: fileRef,
3347
+ type: "file",
3348
+ accept: "image/*",
3349
+ onChange: onFile,
3350
+ className: "qa-hidden"
3351
+ }
3352
+ )
3353
+ ] }),
3354
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-gap-2", children: [
3355
+ /* @__PURE__ */ jsxRuntime.jsxs(
3356
+ "button",
3357
+ {
3358
+ onClick: save,
3359
+ disabled: !desc.trim(),
3360
+ 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",
3361
+ style: { border: "none", cursor: "pointer" },
3362
+ children: [
3363
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
3364
+ t("save")
3365
+ ]
3366
+ }
3367
+ ),
3368
+ /* @__PURE__ */ jsxRuntime.jsx(
3369
+ "button",
3370
+ {
3371
+ onClick: () => setEditing(false),
3372
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-text-sm qa-text-mid",
3373
+ style: { background: "transparent", cursor: "pointer" },
3374
+ children: t("cancel")
3375
+ }
3376
+ )
3377
+ ] })
3378
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3379
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-whitespace-pre-wrap qa-break-words qa-text-hi", children: note.description }),
3380
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-space-y-1.5 qa-text-11 qa-text-lo", children: [
3381
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
3382
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
3383
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
3384
+ ] }),
3385
+ note.target && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: note.target }),
3386
+ contextEventCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { children: t("context_attached", { n: contextEventCount }) })
3387
+ ] }),
3388
+ thumbUrl && /* @__PURE__ */ jsxRuntime.jsx(
3389
+ "img",
3390
+ {
3391
+ src: thumbUrl,
3392
+ alt: "screenshot",
3393
+ className: "qa-mt-2 qa-w-full qa-rounded-lg qa-border qa-border-subtle"
3394
+ }
3395
+ )
3396
+ ] })
3397
+ ] });
2435
3398
  }
2436
3399
  function NoteList() {
2437
- const { notes, t, theme } = useQa();
3400
+ const { notes, notesLoading, t } = useQa();
3401
+ if (notesLoading && !notes.length) {
3402
+ return /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "qa-space-y-2", "aria-hidden": "true", children: [
3403
+ /* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
3404
+ /* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } }),
3405
+ /* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-skeleton qa-rounded-xl", style: { height: "4.5rem" } })
3406
+ ] });
3407
+ }
2438
3408
  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
- );
3409
+ 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: [
3410
+ t("no_points"),
3411
+ /* @__PURE__ */ jsxRuntime.jsx("br", {}),
3412
+ t("no_points_hint", { cta: t("capture_cta") })
3413
+ ] });
2451
3414
  }
2452
3415
  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
3416
  }
@@ -2479,17 +3442,27 @@ function EyeIcon({ open, size = 12, className }) {
2479
3442
  );
2480
3443
  }
2481
3444
  var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2482
- function CopyField({ value, ink, maskable = false }) {
3445
+ function CopyField({
3446
+ value,
3447
+ maskable = false,
3448
+ notify,
3449
+ t
3450
+ }) {
2483
3451
  const [done, setDone] = React.useState(false);
2484
3452
  const [revealed, setRevealed] = React.useState(true);
2485
3453
  const copy = async () => {
2486
3454
  if (value === "\u2014") return;
2487
- if (typeof navigator === "undefined" || !navigator.clipboard) return;
3455
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
3456
+ notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
3457
+ return;
3458
+ }
2488
3459
  try {
2489
3460
  await navigator.clipboard.writeText(value);
2490
3461
  setDone(true);
2491
3462
  setTimeout(() => setDone(false), 1100);
3463
+ notify(t("copied"), { tone: "success", id: "credentials-copy" });
2492
3464
  } catch {
3465
+ notify(t("copy_failed"), { tone: "error", id: "credentials-copy-failed" });
2493
3466
  }
2494
3467
  };
2495
3468
  const hidden = maskable && !revealed && value !== "\u2014";
@@ -2498,14 +3471,14 @@ function CopyField({ value, ink, maskable = false }) {
2498
3471
  /* @__PURE__ */ jsxRuntime.jsxs(
2499
3472
  "button",
2500
3473
  {
2501
- onClick: copy,
3474
+ onClick: () => void copy(),
2502
3475
  disabled: value === "\u2014",
2503
3476
  dir: "ltr",
2504
3477
  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
3478
  style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2506
3479
  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" }))
3480
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-hi", children: displayValue }),
3481
+ 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
3482
  ]
2510
3483
  }
2511
3484
  ),
@@ -2524,7 +3497,7 @@ function CopyField({ value, ink, maskable = false }) {
2524
3497
  ] });
2525
3498
  }
2526
3499
  function CredentialsSection() {
2527
- const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, theme } = useQa();
3500
+ const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, notify } = useQa();
2528
3501
  const usedCount = credentials.filter((c) => loginsUsed.has(c.role)).length;
2529
3502
  const field = pick2(loginField);
2530
3503
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2.5", children: [
@@ -2533,8 +3506,8 @@ function CredentialsSection() {
2533
3506
  /* @__PURE__ */ jsxRuntime.jsx(
2534
3507
  "span",
2535
3508
  {
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 },
3509
+ className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium",
3510
+ style: { background: "var(--qa-success)", color: "var(--qa-on-accent)" },
2538
3511
  children: t("used_count", { n: usedCount, m: credentials.length })
2539
3512
  }
2540
3513
  )
@@ -2545,24 +3518,20 @@ function CredentialsSection() {
2545
3518
  return /* @__PURE__ */ jsxRuntime.jsxs(
2546
3519
  "div",
2547
3520
  {
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
- },
3521
+ className: `qa-rounded-xl qa-border qa-p-2.5 qa-elev-1 qa-transition ${used ? "qa-bg-success-tint" : "qa-bg-1"}`,
3522
+ style: { borderColor: used ? "var(--qa-success)" : "var(--qa-border-subtle)" },
2553
3523
  children: [
2554
3524
  /* @__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 }),
3525
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CircleUser", size: 16, className: "qa-shrink-0 qa-text-accent" }),
3526
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-semibold qa-text-hi", children: label }),
2557
3527
  c.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-slate-400", children: pick2(c.hint) }),
2558
3528
  /* @__PURE__ */ jsxRuntime.jsxs(
2559
3529
  "button",
2560
3530
  {
2561
3531
  onClick: () => toggleLogin(c.role),
2562
3532
  disabled: !c.seeded,
2563
- className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-text-xs",
3533
+ className: `qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-text-xs ${used ? "qa-text-success" : "qa-text-lo"}`,
2564
3534
  style: {
2565
- color: used ? theme.sage : "#94a3b8",
2566
3535
  background: "transparent",
2567
3536
  border: "none",
2568
3537
  cursor: c.seeded ? "pointer" : "default"
@@ -2575,9 +3544,9 @@ function CredentialsSection() {
2575
3544
  )
2576
3545
  ] }),
2577
3546
  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 }),
3547
+ /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.login, notify, t }),
2579
3548
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
2580
- /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password, ink: theme.ink, maskable: true })
3549
+ /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password, notify, t, maskable: true })
2581
3550
  ] })
2582
3551
  ]
2583
3552
  },
@@ -2587,193 +3556,192 @@ function CredentialsSection() {
2587
3556
  ] });
2588
3557
  }
2589
3558
  var keyOf = (id, path) => `${id}::${path}`;
3559
+ var DEFAULT_LANE_COLOR = "#4D9CFF";
2590
3560
  function Lane({
2591
3561
  group,
2592
3562
  checked,
2593
3563
  toggle,
2594
3564
  pick: pick2
2595
3565
  }) {
2596
- const { theme, lang } = useQa();
2597
- const { id, color = theme.primary, steps } = group;
3566
+ const { lang, t, guideFailed, evidenceByStep } = useQa();
3567
+ const { id, color = DEFAULT_LANE_COLOR, steps } = group;
2598
3568
  const done = steps.filter((s) => checked.has(keyOf(id, s.path))).length;
2599
3569
  const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
2600
3570
  const uncoveredRedCount = steps.filter(
2601
3571
  (s) => s.risk === "red" && !checked.has(keyOf(id, s.path))
2602
3572
  ).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(
3573
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-3 qa-elev-1", children: [
3574
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mb-2 qa-flex qa-items-center qa-gap-2", children: [
3575
+ /* @__PURE__ */ jsxRuntime.jsx(
3576
+ "span",
3577
+ {
3578
+ className: "qa-h-2.5 qa-w-2.5 qa-rounded-full",
3579
+ style: { background: color }
3580
+ }
3581
+ ),
3582
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-sm qa-font-bold qa-text-hi", children: pick2(group.role) }),
3583
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-ms-auto qa-text-11 qa-font-medium qa-text-slate-400", children: [
3584
+ done,
3585
+ "/",
3586
+ steps.length
3587
+ ] }),
3588
+ uncoveredRedCount > 0 && /* @__PURE__ */ jsxRuntime.jsx(
3589
+ "span",
3590
+ {
3591
+ className: "qa-bg-danger-tint qa-text-danger qa-rounded qa-px-1 qa-text-10 qa-font-medium",
3592
+ 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)`,
3593
+ children: lang === "ar" ? `\u0623\u062D\u0645\u0631: ${uncoveredRedCount}` : `red: ${uncoveredRedCount}`
3594
+ }
3595
+ )
3596
+ ] }),
3597
+ /* @__PURE__ */ jsxRuntime.jsx(
3598
+ "div",
3599
+ {
3600
+ className: "qa-mb-3 qa-h-1.5 qa-overflow-hidden qa-rounded-full",
3601
+ style: { background: `${color}22` },
3602
+ children: /* @__PURE__ */ jsxRuntime.jsx(
2634
3603
  "div",
2635
3604
  {
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
- )
3605
+ className: "qa-h-full qa-rounded-full qa-transition-all",
3606
+ style: { width: `${pct}%`, background: color }
2645
3607
  }
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: [
3608
+ )
3609
+ }
3610
+ ),
3611
+ /* @__PURE__ */ jsxRuntime.jsxs("ol", { className: "qa-relative qa-ms-1.5", children: [
3612
+ /* @__PURE__ */ jsxRuntime.jsx(
3613
+ "span",
3614
+ {
3615
+ className: "qa-absolute qa-top-1 qa-bottom-0 qa-w-px",
3616
+ style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
3617
+ }
3618
+ ),
3619
+ steps.map((s, i) => {
3620
+ const k = keyOf(id, s.path);
3621
+ const on = checked.has(k);
3622
+ const failed = guideFailed.has(k);
3623
+ const evidence = evidenceByStep.get(k);
3624
+ const evidenceCount = evidence ? evidence.length : 0;
3625
+ const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
3626
+ 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;
3627
+ return /* @__PURE__ */ jsxRuntime.jsx("li", { className: "qa-relative qa-mb-2 qa-last-mb-0", children: /* @__PURE__ */ jsxRuntime.jsxs(
3628
+ "button",
3629
+ {
3630
+ onClick: () => toggle(k),
3631
+ 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" : ""}`,
3632
+ style: { background: failed ? void 0 : "transparent", border: "none", cursor: "pointer" },
3633
+ children: [
3634
+ /* @__PURE__ */ jsxRuntime.jsxs(
3635
+ "span",
3636
+ {
3637
+ 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",
3638
+ style: {
3639
+ borderColor: failed ? "var(--qa-danger)" : color,
3640
+ background: on ? color : failed ? "var(--qa-danger-tint)" : "var(--qa-surface-1)",
3641
+ zIndex: 1
3642
+ },
3643
+ children: [
3644
+ on && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" }),
3645
+ !on && failed && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "AlertTriangle", size: 9, strokeWidth: 2.5, className: "qa-text-danger" })
3646
+ ]
3647
+ }
3648
+ ),
3649
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-min-w-0", children: [
3650
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1", children: [
2667
3651
  /* @__PURE__ */ jsxRuntime.jsx(
2668
- "span",
3652
+ "code",
2669
3653
  {
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",
3654
+ className: "qa-rounded qa-px-1 qa-text-11 qa-font-semibold qa-dir-ltr qa-text-hi",
2671
3655
  style: {
2672
- borderColor: color,
2673
- background: on ? color : "#fff",
2674
- zIndex: 1
3656
+ background: failed ? "var(--qa-danger-tint)" : `${color}14`,
3657
+ textDecoration: on ? "line-through" : "none",
3658
+ opacity: on ? 0.55 : 1
2675
3659
  },
2676
- children: on && /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" })
3660
+ children: s.path
2677
3661
  }
2678
3662
  ),
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
- );
3663
+ /* @__PURE__ */ jsxRuntime.jsx(
3664
+ "span",
3665
+ {
3666
+ className: "qa-inline-block qa-rounded-full qa-shrink-0",
3667
+ style: {
3668
+ width: "6px",
3669
+ height: "6px",
3670
+ background: riskColor,
3671
+ flexShrink: 0
3672
+ },
3673
+ title: dotTitle
3674
+ }
3675
+ )
3676
+ ] }),
3677
+ /* @__PURE__ */ jsxRuntime.jsx(
3678
+ "span",
3679
+ {
3680
+ className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
3681
+ style: { opacity: on ? 0.5 : 1 },
3682
+ children: pick2(s.what)
3683
+ }
3684
+ ),
3685
+ 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 }) }),
3686
+ 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") })
3687
+ ] })
3688
+ ]
3689
+ }
3690
+ ) }, `${k}-${i}`);
3691
+ })
3692
+ ] })
3693
+ ] });
2725
3694
  }
2726
3695
  function GuideSection() {
2727
- const { guideChecked, toggleGuide, t, journey, pick: pick2, theme, lang } = useQa();
3696
+ const { guideChecked, toggleGuide, t, journey, pick: pick2, lang, startTestAlong } = useQa();
2728
3697
  const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
2729
3698
  const done = all.filter((k) => guideChecked.has(k)).length;
2730
3699
  const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
2731
3700
  const coverage = computeCoverage(journey, guideChecked);
2732
3701
  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
- ),
3702
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-xl qa-border qa-border-accent qa-bg-accent-tint qa-p-3 qa-elev-1", children: [
3703
+ 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: [
3704
+ /* @__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" }),
3705
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr qa-font-semibold qa-text-hi", children: [
3706
+ coverage.red.covered,
3707
+ "/",
3708
+ coverage.red.total,
3709
+ " ",
3710
+ lang === "ar" ? "\u0645\u063A\u0637\u0649" : "covered"
3711
+ ] })
3712
+ ] }),
3713
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-text-sm qa-font-semibold qa-text-hi", children: [
3714
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("journey_title") }),
3715
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-dir-ltr", children: [
3716
+ done,
3717
+ "/",
3718
+ all.length,
3719
+ " \xB7 ",
3720
+ pct,
3721
+ "%"
3722
+ ] })
3723
+ ] }),
3724
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-h-2 qa-overflow-hidden qa-rounded-full qa-bg-3", children: /* @__PURE__ */ jsxRuntime.jsx(
3725
+ "div",
3726
+ {
3727
+ className: "qa-h-full qa-rounded-full qa-bg-accent qa-transition-all",
3728
+ style: { width: `${pct}%` }
3729
+ }
3730
+ ) }),
3731
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-mt-2 qa-flex qa-justify-end", children: /* @__PURE__ */ jsxRuntime.jsxs(
3732
+ "button",
3733
+ {
3734
+ type: "button",
3735
+ onClick: startTestAlong,
3736
+ disabled: all.length === 0,
3737
+ 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",
3738
+ children: [
3739
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Play", size: 13 }),
3740
+ t("start_walkthrough")
3741
+ ]
3742
+ }
3743
+ ) })
3744
+ ] }),
2777
3745
  journey.map((g) => /* @__PURE__ */ jsxRuntime.jsx(
2778
3746
  Lane,
2779
3747
  {
@@ -2831,13 +3799,13 @@ function QaPanel() {
2831
3799
  notes,
2832
3800
  exportZip,
2833
3801
  isExporting,
2834
- clearAll,
3802
+ clearNotes,
3803
+ startCapture,
2835
3804
  t,
2836
3805
  lang,
2837
3806
  setLang,
2838
3807
  dir,
2839
3808
  brand,
2840
- theme,
2841
3809
  journey,
2842
3810
  guideChecked
2843
3811
  } = useQa();
@@ -2943,6 +3911,14 @@ function QaPanel() {
2943
3911
  React.useEffect(() => {
2944
3912
  if (!isOpen) setKeyboardLift(0);
2945
3913
  }, [isOpen]);
3914
+ React.useEffect(() => {
3915
+ if (!naming) return void 0;
3916
+ const onKeyDown = (e) => {
3917
+ if (e.key === "Escape") setNaming(false);
3918
+ };
3919
+ document.addEventListener("keydown", onKeyDown);
3920
+ return () => document.removeEventListener("keydown", onKeyDown);
3921
+ }, [naming]);
2946
3922
  const keyboardLiftActive = coarse && !isIpadLandscape;
2947
3923
  const appliedKeyboardLift = keyboardLiftActive ? keyboardLift : 0;
2948
3924
  if (phase === "hidden") return null;
@@ -2964,7 +3940,7 @@ function QaPanel() {
2964
3940
  "data-qa-overlay": "true",
2965
3941
  dir,
2966
3942
  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" : ""}`,
3943
+ 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
3944
  style: {
2969
3945
  // Floating popover position (default). Fully overridden below when
2970
3946
  // docked as an iPad-landscape side-sheet.
@@ -2978,10 +3954,7 @@ function QaPanel() {
2978
3954
  // full height — neutralize it only in the docked sheet variant.
2979
3955
  maxHeight: isIpadLandscape ? "none" : void 0,
2980
3956
  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,
3957
+ zIndex: "var(--qa-z-panel)",
2985
3958
  // Keyboard-avoidance lift (coarse/touch only — see effect above).
2986
3959
  // undefined ⇒ !keyboardLiftActive, so desktop and the iPad-landscape
2987
3960
  // side-sheet render this property exactly as before (the class's own
@@ -2989,156 +3962,141 @@ function QaPanel() {
2989
3962
  transition: keyboardLiftActive ? PANEL_TRANSITION_WITH_LIFT : void 0
2990
3963
  },
2991
3964
  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(
3965
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-px-4 qa-py-3 qa-bg-1", children: [
3966
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5 qa-dir-ltr", dir: "ltr", children: [
3967
+ /* @__PURE__ */ jsxRuntime.jsx(
3968
+ "span",
3969
+ {
3970
+ "aria-hidden": "true",
3971
+ className: "qa-shrink-0",
3972
+ style: { width: 6, height: 6, background: "var(--qa-accent)" }
3973
+ }
3974
+ ),
3975
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-hi", style: { fontSize: 13, fontWeight: 600 }, children: brand.label })
3976
+ ] }),
3977
+ /* @__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 }),
3978
+ /* @__PURE__ */ jsxRuntime.jsx(
3979
+ "div",
3980
+ {
3981
+ className: "qa-ms-auto qa-flex qa-items-center qa-overflow-hidden qa-rounded-lg qa-text-11 qa-font-semibold qa-bg-2",
3982
+ dir: "ltr",
3983
+ children: ["en", "ar"].map((l) => /* @__PURE__ */ jsxRuntime.jsx(
3032
3984
  "button",
3033
3985
  {
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",
3986
+ onClick: () => setLang(l),
3987
+ className: `qa-px-2 qa-py-1 qa-transition qa-tap ${lang === l ? "qa-bg-accent" : "qa-bg-transparent qa-text-mid"}`,
3988
+ style: { border: "none", cursor: "pointer" },
3989
+ children: l === "en" ? "EN" : "\u0639"
3990
+ },
3991
+ l
3992
+ ))
3993
+ }
3994
+ ),
3995
+ /* @__PURE__ */ jsxRuntime.jsx(
3996
+ "button",
3997
+ {
3998
+ onClick: startCapture,
3999
+ title: t("capture_cta"),
4000
+ "aria-label": t("capture_cta"),
4001
+ className: "qa-tap-icon qa-rounded-lg qa-border qa-border-subtle qa-bg-transparent qa-text-hi qa-hover-bg-2 qa-transition",
4002
+ style: { cursor: "pointer" },
4003
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 16 })
4004
+ }
4005
+ ),
4006
+ /* @__PURE__ */ jsxRuntime.jsxs(
4007
+ "button",
4008
+ {
4009
+ onClick: openNaming,
4010
+ disabled: !notes.length || isExporting,
4011
+ title: t("export"),
4012
+ 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",
4013
+ style: { cursor: "pointer" },
4014
+ children: [
4015
+ /* @__PURE__ */ jsxRuntime.jsx(
4016
+ Icon,
3102
4017
  {
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
- ]
4018
+ name: isExporting ? "Loader2" : "Download",
4019
+ size: 14,
4020
+ className: isExporting ? "qa-animate-spin" : void 0
3110
4021
  }
3111
- ) })
3112
- ] }),
3113
- activeTab === "logins" && /* @__PURE__ */ jsxRuntime.jsx(CredentialsSection, {}),
3114
- activeTab === "guide" && /* @__PURE__ */ jsxRuntime.jsx(GuideSection, {})
3115
- ]
4022
+ ),
4023
+ t("export")
4024
+ ]
4025
+ }
4026
+ )
4027
+ ] }),
4028
+ /* @__PURE__ */ jsxRuntime.jsx(
4029
+ TabsBar,
4030
+ {
4031
+ activeTab,
4032
+ setActiveTab,
4033
+ t,
4034
+ lang
3116
4035
  }
3117
4036
  ),
4037
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3" }),
4038
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex-1 qa-space-y-3 qa-overflow-y-auto qa-p-3 qa-bg-0", children: [
4039
+ activeTab === "notes" && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4040
+ /* @__PURE__ */ jsxRuntime.jsx(NoteEditor, {}),
4041
+ /* @__PURE__ */ jsxRuntime.jsx(NoteList, {}),
4042
+ 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: [
4043
+ t("delete_all_q", { n: notes.length }),
4044
+ " ",
4045
+ /* @__PURE__ */ jsxRuntime.jsx(
4046
+ "button",
4047
+ {
4048
+ onClick: () => {
4049
+ void clearNotes();
4050
+ setConfirmClear(false);
4051
+ },
4052
+ className: "qa-font-semibold qa-text-danger qa-tap",
4053
+ style: { background: "transparent", border: "none", cursor: "pointer" },
4054
+ children: t("yes")
4055
+ }
4056
+ ),
4057
+ " / ",
4058
+ /* @__PURE__ */ jsxRuntime.jsx(
4059
+ "button",
4060
+ {
4061
+ onClick: () => setConfirmClear(false),
4062
+ className: "qa-text-accent qa-tap",
4063
+ style: { background: "transparent", border: "none", cursor: "pointer" },
4064
+ children: t("no")
4065
+ }
4066
+ )
4067
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
4068
+ "button",
4069
+ {
4070
+ onClick: () => setConfirmClear(true),
4071
+ className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-xs qa-text-lo qa-hover-text-red",
4072
+ style: { background: "transparent", border: "none", cursor: "pointer" },
4073
+ children: [
4074
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Trash", size: 12 }),
4075
+ t("clear_all")
4076
+ ]
4077
+ }
4078
+ ) })
4079
+ ] }),
4080
+ activeTab === "logins" && /* @__PURE__ */ jsxRuntime.jsx(CredentialsSection, {}),
4081
+ activeTab === "guide" && /* @__PURE__ */ jsxRuntime.jsx(GuideSection, {})
4082
+ ] }),
3118
4083
  naming && /* @__PURE__ */ jsxRuntime.jsx(
3119
4084
  "div",
3120
4085
  {
3121
4086
  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)" },
4087
+ style: { background: "var(--qa-scrim-dialog)" },
4088
+ onClick: () => setNaming(false),
3123
4089
  children: /* @__PURE__ */ jsxRuntime.jsxs(
3124
4090
  "div",
3125
4091
  {
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` },
4092
+ className: "qa-w-full qa-rounded-xl qa-border qa-border-subtle qa-bg-2 qa-p-4 qa-elev-3",
4093
+ onClick: (e) => e.stopPropagation(),
3128
4094
  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
- ),
4095
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mb-2 qa-text-sm qa-font-semibold qa-text-hi", children: t("export_name_title") }),
3137
4096
  /* @__PURE__ */ jsxRuntime.jsxs(
3138
4097
  "div",
3139
4098
  {
3140
- className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-dir-ltr",
3141
- style: { borderColor: `${theme.primary}33` },
4099
+ className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-border-subtle qa-dir-ltr",
3142
4100
  children: [
3143
4101
  /* @__PURE__ */ jsxRuntime.jsx(
3144
4102
  "input",
@@ -3148,32 +4106,24 @@ function QaPanel() {
3148
4106
  onChange: (e) => setFilename(e.target.value),
3149
4107
  onKeyDown: (e) => {
3150
4108
  if (e.key === "Enter") doExport();
3151
- if (e.key === "Escape") setNaming(false);
3152
4109
  },
3153
4110
  placeholder: t("export_name_placeholder"),
3154
4111
  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
4112
  style: { outline: "none", background: "transparent", color: "inherit" }
3156
4113
  }
3157
4114
  ),
3158
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-px-2 qa-text-xs qa-text-slate-400", children: ".zip" })
4115
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-px-2 qa-text-xs qa-text-lo", children: ".zip" })
3159
4116
  ]
3160
4117
  }
3161
4118
  ),
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
- ),
4119
+ 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
4120
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-3 qa-flex qa-gap-2", children: [
3171
4121
  /* @__PURE__ */ jsxRuntime.jsxs(
3172
4122
  "button",
3173
4123
  {
3174
4124
  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" },
4125
+ 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",
4126
+ style: { border: "none", cursor: "pointer" },
3177
4127
  children: [
3178
4128
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
3179
4129
  t("export")
@@ -3184,13 +4134,8 @@ function QaPanel() {
3184
4134
  "button",
3185
4135
  {
3186
4136
  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
- },
4137
+ 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",
4138
+ style: { background: "transparent", cursor: "pointer" },
3194
4139
  children: [
3195
4140
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 16 }),
3196
4141
  t("cancel")
@@ -3211,7 +4156,6 @@ function TabsBar({
3211
4156
  activeTab,
3212
4157
  setActiveTab,
3213
4158
  t,
3214
- theme,
3215
4159
  lang
3216
4160
  }) {
3217
4161
  const tabRefs = React.useRef([]);
@@ -3235,7 +4179,7 @@ function TabsBar({
3235
4179
  ro.observe(container);
3236
4180
  return () => ro.disconnect();
3237
4181
  }, [reposition]);
3238
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative", children: [
4182
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative qa-bg-1", children: [
3239
4183
  TABS.map((tab, i) => {
3240
4184
  const on = activeTab === tab.key;
3241
4185
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -3245,13 +4189,8 @@ function TabsBar({
3245
4189
  tabRefs.current[i] = el;
3246
4190
  },
3247
4191
  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
- },
4192
+ 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"}`,
4193
+ style: { background: "transparent", border: "none", cursor: "pointer" },
3255
4194
  children: [
3256
4195
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: tab.icon, size: 16 }),
3257
4196
  t(tab.labelKey)
@@ -3265,20 +4204,265 @@ function TabsBar({
3265
4204
  {
3266
4205
  ref: barRef,
3267
4206
  className: "qa-tab-indicator",
3268
- style: { background: theme.accent },
4207
+ style: { background: "var(--qa-accent)" },
3269
4208
  "aria-hidden": "true"
3270
4209
  }
3271
4210
  )
3272
4211
  ] });
3273
4212
  }
4213
+ function toneIcon(tone) {
4214
+ switch (tone) {
4215
+ case "success":
4216
+ return { name: "Check", colorClass: "qa-text-success" };
4217
+ case "error":
4218
+ return { name: "AlertTriangle", colorClass: "qa-text-danger" };
4219
+ default:
4220
+ return { name: "X", colorClass: "qa-text-accent" };
4221
+ }
4222
+ }
4223
+ function Toast({ notice }) {
4224
+ const { dismissNotice } = useQa();
4225
+ const icon = toneIcon(notice.tone);
4226
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4227
+ "div",
4228
+ {
4229
+ 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",
4230
+ style: { fontSize: 13 },
4231
+ children: [
4232
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: icon.name, size: 16, className: `qa-shrink-0 ${icon.colorClass}` }),
4233
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-min-w-0 qa-flex-1", children: notice.message }),
4234
+ notice.action && /* @__PURE__ */ jsxRuntime.jsx(
4235
+ "button",
4236
+ {
4237
+ type: "button",
4238
+ onClick: () => {
4239
+ notice.action?.onAction();
4240
+ dismissNotice(notice.id);
4241
+ },
4242
+ className: "qa-tap qa-text-accent qa-focus-ring qa-shrink-0 qa-rounded qa-px-2 qa-py-1 qa-font-semibold",
4243
+ style: { background: "transparent", border: "none", cursor: "pointer", fontSize: 13, pointerEvents: "auto" },
4244
+ children: notice.action.label
4245
+ }
4246
+ )
4247
+ ]
4248
+ }
4249
+ );
4250
+ }
4251
+ function NoticeHost() {
4252
+ const { notices, dir } = useQa();
4253
+ const politeNotices = notices.filter((n) => n.tone !== "error");
4254
+ const errorNotices = notices.filter((n) => n.tone === "error");
4255
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4256
+ "div",
4257
+ {
4258
+ "data-qa-overlay": "true",
4259
+ dir,
4260
+ className: "qa-toast-viewport qa-print-hidden qa-fixed",
4261
+ style: { zIndex: "var(--qa-z-toast)", pointerEvents: "none" },
4262
+ children: [
4263
+ /* @__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)) }),
4264
+ /* @__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)) })
4265
+ ]
4266
+ }
4267
+ );
4268
+ }
4269
+ function TestAlongHud() {
4270
+ const {
4271
+ dir,
4272
+ t,
4273
+ pick: pick2,
4274
+ testAlong,
4275
+ testAlongSteps,
4276
+ gotoStep,
4277
+ gradeStep,
4278
+ exitTestAlong,
4279
+ startCapture,
4280
+ evidenceByStep
4281
+ } = useQa();
4282
+ const index = testAlong.index;
4283
+ const steps = testAlongSteps;
4284
+ const currentStep = steps[index];
4285
+ const backIcon = dir === "rtl" ? "ChevronRight" : "ChevronLeft";
4286
+ const nextIcon = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
4287
+ React.useEffect(() => {
4288
+ const step = steps[index];
4289
+ if (!step) return;
4290
+ const notesForStep = evidenceByStep.get(step.key);
4291
+ if (!notesForStep || notesForStep.length === 0) return;
4292
+ const latest = notesForStep[notesForStep.length - 1];
4293
+ if (latest.target) flashLocate(latest.target);
4294
+ }, [testAlong.index]);
4295
+ if (!testAlong.active) return null;
4296
+ const atFirst = index <= 0;
4297
+ const atLast = index >= steps.length - 1;
4298
+ const riskColor = RISK_COLORS[currentStep?.risk ?? "green"];
4299
+ return /* @__PURE__ */ jsxRuntime.jsx(
4300
+ "div",
4301
+ {
4302
+ "data-qa-overlay": "true",
4303
+ dir,
4304
+ className: "qa-fixed qa-print-hidden",
4305
+ style: {
4306
+ left: "env(safe-area-inset-left)",
4307
+ right: "env(safe-area-inset-right)",
4308
+ bottom: "env(safe-area-inset-bottom)",
4309
+ zIndex: "var(--qa-z-panel)",
4310
+ padding: "0.75rem",
4311
+ pointerEvents: "none"
4312
+ },
4313
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
4314
+ "div",
4315
+ {
4316
+ role: "region",
4317
+ "aria-label": t("journey_title"),
4318
+ 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",
4319
+ style: { maxWidth: "32rem", marginInline: "auto", pointerEvents: "auto" },
4320
+ children: [
4321
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
4322
+ /* @__PURE__ */ jsxRuntime.jsx(
4323
+ "span",
4324
+ {
4325
+ "aria-hidden": "true",
4326
+ className: "qa-shrink-0 qa-rounded-full",
4327
+ style: { width: 8, height: 8, backgroundColor: riskColor }
4328
+ }
4329
+ ),
4330
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-mid", children: t("step_of", { n: index + 1, m: steps.length }) }),
4331
+ /* @__PURE__ */ jsxRuntime.jsxs(
4332
+ "button",
4333
+ {
4334
+ type: "button",
4335
+ onClick: exitTestAlong,
4336
+ 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",
4337
+ style: { background: "transparent", border: "none", cursor: "pointer" },
4338
+ children: [
4339
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 14 }),
4340
+ t("exit_walkthrough")
4341
+ ]
4342
+ }
4343
+ )
4344
+ ] }),
4345
+ currentStep && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-min-w-0", children: [
4346
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-text-sm qa-font-medium qa-text-hi qa-break-words", children: pick2(currentStep.what) }),
4347
+ currentStep.expect && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-11 qa-text-mid qa-mt-1 qa-break-words", children: [
4348
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-font-semibold", children: [
4349
+ t("expected_label"),
4350
+ ": "
4351
+ ] }),
4352
+ pick2(currentStep.expect)
4353
+ ] })
4354
+ ] }),
4355
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-items-center qa-gap-2", children: [
4356
+ /* @__PURE__ */ jsxRuntime.jsxs(
4357
+ "button",
4358
+ {
4359
+ type: "button",
4360
+ onClick: () => gotoStep(index - 1),
4361
+ disabled: atFirst,
4362
+ 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",
4363
+ style: { background: "transparent", cursor: "pointer" },
4364
+ children: [
4365
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: backIcon, size: 14 }),
4366
+ t("prev_step")
4367
+ ]
4368
+ }
4369
+ ),
4370
+ /* @__PURE__ */ jsxRuntime.jsxs(
4371
+ "button",
4372
+ {
4373
+ type: "button",
4374
+ onClick: () => currentStep && gradeStep(currentStep.key, "fail"),
4375
+ disabled: !currentStep,
4376
+ 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",
4377
+ style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
4378
+ children: [
4379
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "AlertTriangle", size: 14 }),
4380
+ t("mark_fail")
4381
+ ]
4382
+ }
4383
+ ),
4384
+ /* @__PURE__ */ jsxRuntime.jsxs(
4385
+ "button",
4386
+ {
4387
+ type: "button",
4388
+ onClick: () => currentStep && gradeStep(currentStep.key, "pass"),
4389
+ disabled: !currentStep,
4390
+ 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",
4391
+ style: { border: "none", cursor: "pointer", minWidth: "4.5rem" },
4392
+ children: [
4393
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 14 }),
4394
+ t("mark_pass")
4395
+ ]
4396
+ }
4397
+ ),
4398
+ /* @__PURE__ */ jsxRuntime.jsxs(
4399
+ "button",
4400
+ {
4401
+ type: "button",
4402
+ onClick: () => startCapture(),
4403
+ 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",
4404
+ style: { border: "none", cursor: "pointer", minWidth: "6rem" },
4405
+ children: [
4406
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 14 }),
4407
+ t("capture_here")
4408
+ ]
4409
+ }
4410
+ ),
4411
+ /* @__PURE__ */ jsxRuntime.jsxs(
4412
+ "button",
4413
+ {
4414
+ type: "button",
4415
+ onClick: () => gotoStep(index + 1),
4416
+ disabled: atLast,
4417
+ 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",
4418
+ style: { background: "transparent", cursor: "pointer" },
4419
+ children: [
4420
+ t("next_step"),
4421
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: nextIcon, size: 14 })
4422
+ ]
4423
+ }
4424
+ )
4425
+ ] })
4426
+ ]
4427
+ }
4428
+ )
4429
+ }
4430
+ );
4431
+ }
3274
4432
 
3275
4433
  // src/lib/capture.ts
3276
4434
  var HTML2CANVAS_TIMEOUT_MS = 1e4;
4435
+ var FALLBACK_PAGE_BACKGROUND = "#ffffff";
3277
4436
  function withTimeout(promise, ms) {
4437
+ let timer;
3278
4438
  return Promise.race([
3279
4439
  promise,
3280
- new Promise((resolve) => setTimeout(() => resolve(null), ms))
3281
- ]);
4440
+ new Promise((resolve) => {
4441
+ timer = setTimeout(() => resolve(null), ms);
4442
+ })
4443
+ ]).finally(() => {
4444
+ if (timer !== void 0) clearTimeout(timer);
4445
+ });
4446
+ }
4447
+ function isTransparent(color) {
4448
+ const c = (color || "").trim().toLowerCase();
4449
+ if (!c || c === "transparent") return true;
4450
+ const m = c.match(/^rgba?\(([^)]+)\)$/);
4451
+ if (!m) return false;
4452
+ const parts = m[1].split(/[,/\s]+/).filter(Boolean);
4453
+ return parts.length >= 4 && parseFloat(parts[3]) === 0;
4454
+ }
4455
+ function resolvePageBackground() {
4456
+ if (typeof getComputedStyle !== "function") return FALLBACK_PAGE_BACKGROUND;
4457
+ for (const el of [document.body, document.documentElement]) {
4458
+ if (!el) continue;
4459
+ try {
4460
+ const bg = getComputedStyle(el).backgroundColor;
4461
+ if (!isTransparent(bg)) return bg;
4462
+ } catch {
4463
+ }
4464
+ }
4465
+ return FALLBACK_PAGE_BACKGROUND;
3282
4466
  }
3283
4467
  function toBlob(canvas) {
3284
4468
  return new Promise((resolve) => {
@@ -3294,8 +4478,10 @@ function toBlob(canvas) {
3294
4478
  });
3295
4479
  }
3296
4480
  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;
4481
+ if (typeof document === "undefined" || typeof window === "undefined") {
4482
+ return { status: "empty" };
4483
+ }
4484
+ if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
3299
4485
  const sx = scroll?.x ?? window.scrollX;
3300
4486
  const sy = scroll?.y ?? window.scrollY;
3301
4487
  try {
@@ -3310,7 +4496,8 @@ async function captureRegion(rect, scroll) {
3310
4496
  scale,
3311
4497
  useCORS: true,
3312
4498
  allowTaint: true,
3313
- backgroundColor: null,
4499
+ // The page's own background, never null — see resolvePageBackground().
4500
+ backgroundColor: resolvePageBackground(),
3314
4501
  logging: false,
3315
4502
  scrollX: sx,
3316
4503
  scrollY: sy,
@@ -3322,11 +4509,12 @@ async function captureRegion(rect, scroll) {
3322
4509
  }),
3323
4510
  HTML2CANVAS_TIMEOUT_MS
3324
4511
  );
3325
- if (!canvas) return null;
3326
- return await toBlob(canvas);
4512
+ if (!canvas) return { status: "failed" };
4513
+ const blob = await toBlob(canvas);
4514
+ return blob ? { status: "ok", blob } : { status: "failed" };
3327
4515
  } catch (err) {
3328
4516
  console.warn("[QA] region capture failed:", err);
3329
- return null;
4517
+ return { status: "failed" };
3330
4518
  }
3331
4519
  }
3332
4520
 
@@ -3430,6 +4618,26 @@ function unlockPageScroll() {
3430
4618
  var DRAG_THRESHOLD2 = 6;
3431
4619
  var TOUCH_DRAG_THRESHOLD = 12;
3432
4620
  var MIN_REGION_SIZE = 8;
4621
+ var SEVERITIES2 = ["bug", "question", "polish"];
4622
+ var SEVERITY_ICON = {
4623
+ bug: "Bug",
4624
+ question: "AlertTriangle",
4625
+ polish: "Pencil"
4626
+ };
4627
+ var SEVERITY_LABEL_KEY2 = {
4628
+ bug: "sev_bug",
4629
+ question: "sev_question",
4630
+ polish: "sev_polish"
4631
+ };
4632
+ function clampRegionRect(rect) {
4633
+ const vw = typeof window !== "undefined" ? window.innerWidth : rect.left + rect.width;
4634
+ const vh = typeof window !== "undefined" ? window.innerHeight : rect.top + rect.height;
4635
+ const width = Math.min(Math.max(MIN_REGION_SIZE, rect.width), Math.max(MIN_REGION_SIZE, vw));
4636
+ const height = Math.min(Math.max(MIN_REGION_SIZE, rect.height), Math.max(MIN_REGION_SIZE, vh));
4637
+ const left = Math.min(Math.max(0, rect.left), Math.max(0, vw - width));
4638
+ const top = Math.min(Math.max(0, rect.top), Math.max(0, vh - height));
4639
+ return { top, left, width, height };
4640
+ }
3433
4641
  var REGION_HANDLES = [
3434
4642
  { edge: "nw", top: "0%", left: "0%", cursor: "nwse-resize" },
3435
4643
  { edge: "n", top: "0%", left: "50%", cursor: "ns-resize" },
@@ -3441,7 +4649,7 @@ var REGION_HANDLES = [
3441
4649
  { edge: "se", top: "100%", left: "100%", cursor: "nwse-resize" }
3442
4650
  ];
3443
4651
  function CaptureMode() {
3444
- const { addNote, endCapture, t, dir, theme } = useQa();
4652
+ const { addNote, endCapture, t, dir } = useQa();
3445
4653
  const coarse = useCoarsePointer();
3446
4654
  const layerRef = React.useRef(null);
3447
4655
  const overlayRootRef = React.useRef(null);
@@ -3455,7 +4663,10 @@ function CaptureMode() {
3455
4663
  const [shot, setShot] = React.useState(null);
3456
4664
  const [shotUrl, setShotUrl] = React.useState(null);
3457
4665
  const [capturing, setCapturing] = React.useState(false);
4666
+ const [captureError, setCaptureError] = React.useState(false);
3458
4667
  const [description, setDescription] = React.useState("");
4668
+ const [severity, setSeverity] = React.useState("bug");
4669
+ const [targetForensics, setTargetForensics] = React.useState(void 0);
3459
4670
  const taRef = React.useRef(null);
3460
4671
  const activePointerId = React.useRef(null);
3461
4672
  const pointerKind = React.useRef("mouse");
@@ -3476,31 +4687,39 @@ function CaptureMode() {
3476
4687
  if (!el || el.closest?.("[data-qa-overlay]")) return null;
3477
4688
  return el;
3478
4689
  }, []);
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);
4690
+ const runCapture = React.useCallback(async (rect) => {
3486
4691
  setCapturing(true);
4692
+ setCaptureError(false);
3487
4693
  lockPageScroll();
3488
4694
  try {
3489
- const blob = await captureRegion(sel.rect, scrollSnap.current);
4695
+ const outcome = await captureRegion(rect, scrollSnap.current);
4696
+ const blob = outcome.status === "ok" ? outcome.blob : null;
4697
+ const url = blob ? URL.createObjectURL(blob) : null;
3490
4698
  if (!mountedRef.current) {
3491
- if (blob) URL.revokeObjectURL(URL.createObjectURL(blob));
4699
+ if (url) URL.revokeObjectURL(url);
3492
4700
  return;
3493
4701
  }
4702
+ setCaptureError(outcome.status === "failed");
3494
4703
  setShot(blob);
3495
4704
  setShotUrl((old) => {
3496
4705
  if (old) URL.revokeObjectURL(old);
3497
- return blob ? URL.createObjectURL(blob) : null;
4706
+ return url;
3498
4707
  });
3499
4708
  } finally {
3500
4709
  unlockPageScroll();
3501
4710
  if (mountedRef.current) setCapturing(false);
3502
4711
  }
3503
4712
  }, []);
4713
+ const beginAnnotation = React.useCallback(async (sel) => {
4714
+ setSelection(sel);
4715
+ setCandidate(null);
4716
+ setHover(null);
4717
+ setRegionMode(false);
4718
+ setSeverity("bug");
4719
+ setPhase("annotating");
4720
+ setCardIn(false);
4721
+ await runCapture(sel.rect);
4722
+ }, [runCapture]);
3504
4723
  React.useEffect(() => {
3505
4724
  if (phase !== "annotating") {
3506
4725
  setCardIn(false);
@@ -3559,42 +4778,50 @@ function CaptureMode() {
3559
4778
  activePointerId.current = null;
3560
4779
  const d = dragRef.current;
3561
4780
  dragRef.current = null;
4781
+ setDrag(null);
3562
4782
  const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD2 : TOUCH_DRAG_THRESHOLD;
3563
4783
  const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
3564
4784
  scrollSnap.current = { x: window.scrollX, y: window.scrollY };
4785
+ let regionRect = null;
3565
4786
  if (moved && d) {
3566
- const rect = {
4787
+ const rawRect = {
3567
4788
  left: Math.min(d.x0, e.clientX),
3568
4789
  top: Math.min(d.y0, e.clientY),
3569
4790
  width: Math.abs(e.clientX - d.x0),
3570
4791
  height: Math.abs(e.clientY - d.y0)
3571
4792
  };
3572
- setDrag(null);
3573
- const sel = { kind: "region", rect };
3574
- if (coarse) {
3575
- setCandidate(sel);
3576
- setPhase("confirming");
3577
- } else {
3578
- void beginAnnotation(sel);
4793
+ if (rawRect.width >= MIN_REGION_SIZE || rawRect.height >= MIN_REGION_SIZE) {
4794
+ regionRect = clampRegionRect(rawRect);
3579
4795
  }
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
- };
4796
+ }
4797
+ if (regionRect) {
4798
+ const sel2 = { kind: "region", rect: regionRect };
4799
+ setTargetForensics(void 0);
3591
4800
  if (coarse) {
3592
- setCandidate(sel);
3593
- setHover({ rect: sel.rect, selector: sel.selector || "" });
4801
+ setCandidate(sel2);
3594
4802
  setPhase("confirming");
3595
4803
  } else {
3596
- void beginAnnotation(sel);
4804
+ void beginAnnotation(sel2);
3597
4805
  }
4806
+ return;
4807
+ }
4808
+ const el = elementUnder(e.clientX, e.clientY);
4809
+ if (!el) return;
4810
+ const r = el.getBoundingClientRect();
4811
+ const sel = {
4812
+ kind: "element",
4813
+ rect: { top: r.top, left: r.left, width: r.width, height: r.height },
4814
+ selector: getStableSelector(el),
4815
+ text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
4816
+ tagName: el.tagName.toLowerCase()
4817
+ };
4818
+ setTargetForensics(collectTargetForensics(el));
4819
+ if (coarse) {
4820
+ setCandidate(sel);
4821
+ setHover({ rect: sel.rect, selector: sel.selector || "" });
4822
+ setPhase("confirming");
4823
+ } else {
4824
+ void beginAnnotation(sel);
3598
4825
  }
3599
4826
  };
3600
4827
  const onPointerCancel = (e) => {
@@ -3629,23 +4856,29 @@ function CaptureMode() {
3629
4856
  const dx = e.clientX - hd.startX;
3630
4857
  const dy = e.clientY - hd.startY;
3631
4858
  const { startRect, edge } = hd;
3632
- let { top, left, width, height } = startRect;
4859
+ let top = startRect.top;
4860
+ let left = startRect.left;
4861
+ let right = startRect.left + startRect.width;
4862
+ let bottom = startRect.top + startRect.height;
3633
4863
  if (edge === "move") {
3634
4864
  left = startRect.left + dx;
3635
4865
  top = startRect.top + dy;
4866
+ right = left + startRect.width;
4867
+ bottom = top + startRect.height;
3636
4868
  } 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
- }
4869
+ if (edge.includes("e")) right += dx;
4870
+ if (edge.includes("w")) left += dx;
4871
+ if (edge.includes("s")) bottom += dy;
4872
+ if (edge.includes("n")) top += dy;
3647
4873
  }
3648
- setCandidate((prev) => prev ? { ...prev, rect: { top, left, width, height } } : prev);
4874
+ const rawRect = {
4875
+ left: Math.min(left, right),
4876
+ top: Math.min(top, bottom),
4877
+ width: Math.abs(right - left),
4878
+ height: Math.abs(bottom - top)
4879
+ };
4880
+ const rect = clampRegionRect(rawRect);
4881
+ setCandidate((prev) => prev ? { ...prev, rect } : prev);
3649
4882
  }, []);
3650
4883
  const onHandlePointerUp = React.useCallback((e) => {
3651
4884
  const hd = handleDragRef.current;
@@ -3682,7 +4915,8 @@ function CaptureMode() {
3682
4915
  }
3683
4916
  const first = focusable[0];
3684
4917
  const last = focusable[focusable.length - 1];
3685
- const active = document.activeElement;
4918
+ const rootNode = root.getRootNode();
4919
+ const active = rootNode.activeElement;
3686
4920
  const activeInside = !!active && root.contains(active);
3687
4921
  if (e.shiftKey) {
3688
4922
  if (!activeInside || active === first) {
@@ -3723,7 +4957,13 @@ function CaptureMode() {
3723
4957
  },
3724
4958
  scroll: { ...scrollSnap.current }
3725
4959
  };
3726
- await addNote({ description, screenshot: shot ?? void 0, target });
4960
+ await addNote({
4961
+ description,
4962
+ screenshot: shot ?? void 0,
4963
+ target,
4964
+ severity,
4965
+ forensics: selection.kind === "element" ? targetForensics : void 0
4966
+ });
3727
4967
  endCapture();
3728
4968
  };
3729
4969
  const popStyleFor = React.useCallback((r) => {
@@ -3745,7 +4985,7 @@ function CaptureMode() {
3745
4985
  const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
3746
4986
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
3747
4987
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
3748
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", ref: overlayRootRef, children: [
4988
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
3749
4989
  /* @__PURE__ */ jsxRuntime.jsx(
3750
4990
  "div",
3751
4991
  {
@@ -3758,15 +4998,14 @@ function CaptureMode() {
3758
4998
  style: {
3759
4999
  cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
3760
5000
  touchAction: coarse ? "none" : "auto",
3761
- background: "rgba(58,42,46,0.18)"
5001
+ background: "var(--qa-scrim-capture)"
3762
5002
  }
3763
5003
  }
3764
5004
  ),
3765
5005
  phase === "selecting" && !coarse && /* @__PURE__ */ jsxRuntime.jsxs(
3766
5006
  "div",
3767
5007
  {
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 },
5008
+ 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
5009
  children: [
3771
5010
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
3772
5011
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
@@ -3781,8 +5020,8 @@ function CaptureMode() {
3781
5020
  "button",
3782
5021
  {
3783
5022
  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" },
5023
+ 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",
5024
+ style: { background: "transparent", cursor: "pointer" },
3786
5025
  children: "Esc"
3787
5026
  }
3788
5027
  )
@@ -3792,8 +5031,7 @@ function CaptureMode() {
3792
5031
  phase === "selecting" && coarse && /* @__PURE__ */ jsxRuntime.jsxs(
3793
5032
  "div",
3794
5033
  {
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 },
5034
+ 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
5035
  children: [
3798
5036
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
3799
5037
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
@@ -3822,8 +5060,8 @@ function CaptureMode() {
3822
5060
  "button",
3823
5061
  {
3824
5062
  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" },
5063
+ 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",
5064
+ style: { background: "transparent", cursor: "pointer" },
3827
5065
  children: "Esc"
3828
5066
  }
3829
5067
  )
@@ -3840,10 +5078,10 @@ function CaptureMode() {
3840
5078
  width: activeRect.width,
3841
5079
  height: activeRect.height,
3842
5080
  pointerEvents: confirmingRegion ? "auto" : "none",
3843
- outline: `2px ${isRegion ? "dashed" : "solid"} ${theme.accent}`,
5081
+ outline: `2px ${isRegion ? "dashed" : "solid"} var(--qa-accent)`,
3844
5082
  outlineOffset: "1px",
3845
- background: `${theme.accent}1f`,
3846
- boxShadow: phase === "annotating" ? "0 0 0 9999px rgba(58,42,46,0.28)" : "none"
5083
+ background: "var(--qa-accent-tint)",
5084
+ boxShadow: phase === "annotating" ? "0 0 0 9999px var(--qa-scrim-spot)" : "none"
3847
5085
  },
3848
5086
  children: [
3849
5087
  (phase === "selecting" || phase === "confirming") && hover?.selector && !drag && /* @__PURE__ */ jsxRuntime.jsx(
@@ -3854,7 +5092,7 @@ function CaptureMode() {
3854
5092
  top: "-1.5rem",
3855
5093
  left: 0,
3856
5094
  maxWidth: "260px",
3857
- background: theme.primary
5095
+ background: "var(--qa-surface-3)"
3858
5096
  },
3859
5097
  children: hover.selector
3860
5098
  }
@@ -3866,7 +5104,7 @@ function CaptureMode() {
3866
5104
  style: {
3867
5105
  bottom: "-1.5rem",
3868
5106
  right: 0,
3869
- background: theme.accentDark
5107
+ background: "var(--qa-accent-active)"
3870
5108
  },
3871
5109
  children: [
3872
5110
  Math.round(drag.rect.width),
@@ -3903,7 +5141,7 @@ function CaptureMode() {
3903
5141
  transform: "translate(-50%, -50%)",
3904
5142
  touchAction: "none",
3905
5143
  cursor,
3906
- background: `${theme.accent}33`
5144
+ background: "var(--qa-accent-tint)"
3907
5145
  },
3908
5146
  children: /* @__PURE__ */ jsxRuntime.jsx(
3909
5147
  "span",
@@ -3912,7 +5150,7 @@ function CaptureMode() {
3912
5150
  style: {
3913
5151
  width: 16,
3914
5152
  height: 16,
3915
- background: theme.accent,
5153
+ background: "var(--qa-accent)",
3916
5154
  border: "2px solid #fff",
3917
5155
  boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
3918
5156
  pointerEvents: "none"
@@ -3933,19 +5171,19 @@ function CaptureMode() {
3933
5171
  dir,
3934
5172
  role: "group",
3935
5173
  "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",
5174
+ 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
5175
  style: {
3938
5176
  ...confirmPopStyle,
3939
- background: theme.surface,
3940
- borderColor: `${theme.primary}22`
5177
+ background: "var(--qa-surface-1)",
5178
+ borderColor: "var(--qa-border-subtle)"
3941
5179
  },
3942
5180
  children: [
3943
5181
  /* @__PURE__ */ jsxRuntime.jsxs(
3944
5182
  "button",
3945
5183
  {
3946
5184
  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" },
5185
+ 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",
5186
+ style: { border: "none", cursor: "pointer" },
3949
5187
  children: [
3950
5188
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
3951
5189
  t("use_this")
@@ -3959,14 +5197,10 @@ function CaptureMode() {
3959
5197
  setCandidate(null);
3960
5198
  setHover(null);
3961
5199
  setPhase("selecting");
5200
+ setTargetForensics(void 0);
3962
5201
  },
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
- },
5202
+ className: "qa-tap qa-rounded-full qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
5203
+ style: { background: "transparent", cursor: "pointer" },
3970
5204
  children: t("adjust")
3971
5205
  }
3972
5206
  )
@@ -3978,70 +5212,102 @@ function CaptureMode() {
3978
5212
  {
3979
5213
  "data-qa-overlay": "true",
3980
5214
  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" : ""}`,
5215
+ 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
5216
  style: {
3983
5217
  ...popStyle,
3984
- background: theme.surface,
3985
- borderColor: `${theme.primary}22`,
3986
- fontFamily: dir === "rtl" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif"
5218
+ background: "var(--qa-surface-1)"
3987
5219
  },
3988
5220
  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
- ),
5221
+ /* @__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: [
5222
+ /* @__PURE__ */ jsxRuntime.jsx(
5223
+ Icon,
5224
+ {
5225
+ name: selection.kind === "region" ? "Square" : "MousePointerClick",
5226
+ size: 16
5227
+ }
5228
+ ),
5229
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-font-semibold", children: selection.kind === "region" ? t("sel_region") : t("sel_element") }),
5230
+ /* @__PURE__ */ jsxRuntime.jsx(
5231
+ "button",
5232
+ {
5233
+ onClick: () => endCapture(),
5234
+ className: "qa-tap-icon qa-ms-auto qa-opacity-80 qa-hover-opacity-100",
5235
+ style: { background: "transparent", border: "none", cursor: "pointer", color: "var(--qa-ink-hi)" },
5236
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 16 })
5237
+ }
5238
+ )
5239
+ ] }),
4015
5240
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
4016
5241
  /* @__PURE__ */ jsxRuntime.jsx(
4017
5242
  "div",
4018
5243
  {
4019
5244
  className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
4020
5245
  style: {
4021
- borderColor: `${theme.primary}1a`,
4022
- background: theme.cream
5246
+ borderColor: "var(--qa-border-subtle)",
5247
+ background: "var(--qa-surface-0)"
4023
5248
  },
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(
5249
+ 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: [
5250
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
5251
+ t("capturing")
5252
+ ] }) : shotUrl ? /* @__PURE__ */ jsxRuntime.jsx(
4035
5253
  "img",
4036
5254
  {
4037
5255
  src: shotUrl,
4038
5256
  alt: "capture",
4039
5257
  className: "qa-max-h-32 qa-rounded-md"
4040
5258
  }
5259
+ ) : captureError ? (
5260
+ // The render broke rather than being skipped — offer a retry
5261
+ // against the same selection instead of a dead-end message.
5262
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
5263
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
5264
+ /* @__PURE__ */ jsxRuntime.jsxs(
5265
+ "button",
5266
+ {
5267
+ type: "button",
5268
+ onClick: () => selection && void runCapture(selection.rect),
5269
+ 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",
5270
+ children: [
5271
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
5272
+ t("retry")
5273
+ ]
5274
+ }
5275
+ )
5276
+ ] })
4041
5277
  ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
4042
5278
  }
4043
5279
  ),
4044
5280
  /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
5281
+ /* @__PURE__ */ jsxRuntime.jsxs(
5282
+ "div",
5283
+ {
5284
+ role: "group",
5285
+ "aria-label": t("severity_label"),
5286
+ className: "qa-flex qa-items-center qa-flex-wrap qa-gap-1.5",
5287
+ children: [
5288
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid qa-me-1", children: t("severity_label") }),
5289
+ SEVERITIES2.map((sev) => {
5290
+ const active = severity === sev;
5291
+ 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";
5292
+ return /* @__PURE__ */ jsxRuntime.jsxs(
5293
+ "button",
5294
+ {
5295
+ type: "button",
5296
+ onClick: () => setSeverity(sev),
5297
+ "aria-pressed": active,
5298
+ 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"}`,
5299
+ style: { cursor: "pointer" },
5300
+ children: [
5301
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: SEVERITY_ICON[sev], size: 12 }),
5302
+ t(SEVERITY_LABEL_KEY2[sev])
5303
+ ]
5304
+ },
5305
+ sev
5306
+ );
5307
+ })
5308
+ ]
5309
+ }
5310
+ ),
4045
5311
  /* @__PURE__ */ jsxRuntime.jsx(
4046
5312
  "textarea",
4047
5313
  {
@@ -4053,8 +5319,7 @@ function CaptureMode() {
4053
5319
  },
4054
5320
  rows: 3,
4055
5321
  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" }
5322
+ 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
5323
  }
4059
5324
  ),
4060
5325
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
@@ -4063,8 +5328,8 @@ function CaptureMode() {
4063
5328
  {
4064
5329
  onClick: () => void save(),
4065
5330
  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" },
5331
+ 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",
5332
+ style: { border: "none", cursor: "pointer" },
4068
5333
  children: [
4069
5334
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 16 }),
4070
5335
  t("save_point")
@@ -4079,14 +5344,11 @@ function CaptureMode() {
4079
5344
  setSelection(null);
4080
5345
  setShot(null);
4081
5346
  setDescription("");
5347
+ setSeverity("bug");
5348
+ setTargetForensics(void 0);
4082
5349
  },
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
- },
5350
+ className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
5351
+ style: { background: "transparent", cursor: "pointer" },
4090
5352
  children: t("reselect")
4091
5353
  }
4092
5354
  )
@@ -4140,8 +5402,9 @@ function CaptureGate() {
4140
5402
  return captureActive ? /* @__PURE__ */ jsxRuntime.jsx(CaptureMode, {}) : null;
4141
5403
  }
4142
5404
  function QaRootInner({ config }) {
5405
+ const { isOpen, setIsOpen, testAlong } = useQa();
4143
5406
  const shouldShowInitially = config.alwaysVisible === true || config.visible === true || config.visible === void 0 && !isProduction();
4144
- const [visible, setVisible] = React.useState(shouldShowInitially);
5407
+ const [widgetShown, setWidgetShown] = React.useState(shouldShowInitially);
4145
5408
  React.useEffect(() => {
4146
5409
  if (typeof document === "undefined") return;
4147
5410
  const hk = parseHotkey(config.hotkey);
@@ -4149,16 +5412,22 @@ function QaRootInner({ config }) {
4149
5412
  const handler = (e) => {
4150
5413
  if (e.key.toLowerCase() === hk.key && !!e.shiftKey === hk.shift && !!e.altKey === hk.alt && !!e.ctrlKey === hk.ctrl && !!e.metaKey === hk.meta) {
4151
5414
  e.preventDefault();
4152
- setVisible((v) => !v);
5415
+ if (!widgetShown) {
5416
+ setWidgetShown(true);
5417
+ setIsOpen(true);
5418
+ } else {
5419
+ setIsOpen(!isOpen);
5420
+ }
4153
5421
  }
4154
5422
  };
4155
5423
  document.addEventListener("keydown", handler);
4156
5424
  return () => document.removeEventListener("keydown", handler);
4157
- }, [config.hotkey]);
4158
- if (!visible) return null;
5425
+ }, [config.hotkey, widgetShown, isOpen, setIsOpen]);
5426
+ if (!widgetShown) return null;
4159
5427
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
4160
5428
  /* @__PURE__ */ jsxRuntime.jsx(QaFab, {}),
4161
- /* @__PURE__ */ jsxRuntime.jsx(QaPanel, {}),
5429
+ testAlong.active ? /* @__PURE__ */ jsxRuntime.jsx(TestAlongHud, {}) : /* @__PURE__ */ jsxRuntime.jsx(QaPanel, {}),
5430
+ /* @__PURE__ */ jsxRuntime.jsx(NoticeHost, {}),
4162
5431
  /* @__PURE__ */ jsxRuntime.jsx(CaptureGate, {})
4163
5432
  ] });
4164
5433
  }
@@ -4177,18 +5446,23 @@ function mountQaStudio(config) {
4177
5446
  document.body.appendChild(host);
4178
5447
  const shadow = host.attachShadow({ mode: "open" });
4179
5448
  injectStyles(shadow);
4180
- applyThemeVars(host, config.theme);
4181
5449
  const root = ReactDOM__default.default.createRoot(shadow);
4182
5450
  root.render(React__default.default.createElement(QaRoot, { config }));
5451
+ if (config.captureContext !== false) {
5452
+ installContextCapture();
5453
+ }
4183
5454
  return {
4184
5455
  destroy() {
5456
+ if (config.captureContext !== false) {
5457
+ uninstallContextCapture();
5458
+ }
4185
5459
  try {
4186
5460
  root.unmount();
4187
5461
  } catch {
4188
5462
  }
4189
5463
  if (host.parentNode) host.remove();
4190
5464
  if (typeof document !== "undefined") {
4191
- document.body.querySelectorAll(":scope > [data-qa-overlay]").forEach((el) => el.remove());
5465
+ document.body.querySelectorAll(":scope > [data-qa-overlay]:not(qapture-overlay)").forEach((el) => el.remove());
4192
5466
  }
4193
5467
  }
4194
5468
  };
@@ -4209,7 +5483,9 @@ function initQaStudio(config) {
4209
5483
  function Qapture({ config }) {
4210
5484
  React.useEffect(() => {
4211
5485
  const instance = initQaStudio(config);
4212
- return () => instance.destroy();
5486
+ return () => {
5487
+ queueMicrotask(() => instance.destroy());
5488
+ };
4213
5489
  }, []);
4214
5490
  return null;
4215
5491
  }
@@ -4217,5 +5493,5 @@ function Qapture({ config }) {
4217
5493
  exports.Qapture = Qapture;
4218
5494
  exports.deleteQaDatabase = deleteQaDatabase;
4219
5495
  exports.initQaStudio = initQaStudio;
4220
- //# sourceMappingURL=chunk-DPJW626S.cjs.map
4221
- //# sourceMappingURL=chunk-DPJW626S.cjs.map
5496
+ //# sourceMappingURL=chunk-6KAXMN77.cjs.map
5497
+ //# sourceMappingURL=chunk-6KAXMN77.cjs.map