qapture2 0.2.4 → 0.3.1

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