qapture2 0.2.2 → 0.2.4

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.
@@ -2,7 +2,134 @@ import React, { createContext, useEffect, Component, useState, useCallback, useR
2
2
  import ReactDOM from 'react-dom/client';
3
3
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
4
 
5
- // src/index.ts
5
+ // src/lib/idb.ts
6
+ var DB_VERSION = 2;
7
+ var NOTES_STORE = "notes";
8
+ var META_STORE = "meta";
9
+ var dbCache = /* @__PURE__ */ new Map();
10
+ function isIdbAvailable() {
11
+ return typeof indexedDB !== "undefined";
12
+ }
13
+ function openDB(dbName) {
14
+ const cached = dbCache.get(dbName);
15
+ if (cached) return cached;
16
+ const promise = new Promise((resolve, reject) => {
17
+ let req;
18
+ try {
19
+ req = indexedDB.open(dbName, DB_VERSION);
20
+ } catch (err) {
21
+ dbCache.delete(dbName);
22
+ reject(err);
23
+ return;
24
+ }
25
+ req.onupgradeneeded = (e) => {
26
+ const db = e.target.result;
27
+ const oldVersion = e.oldVersion;
28
+ switch (true) {
29
+ case oldVersion < 1:
30
+ if (!db.objectStoreNames.contains(NOTES_STORE)) {
31
+ db.createObjectStore(NOTES_STORE, { keyPath: "id" });
32
+ }
33
+ // falls through
34
+ case oldVersion < 2:
35
+ if (!db.objectStoreNames.contains(META_STORE)) {
36
+ db.createObjectStore(META_STORE, { keyPath: "key" });
37
+ }
38
+ break;
39
+ }
40
+ };
41
+ req.onsuccess = (e) => resolve(e.target.result);
42
+ req.onerror = (e) => {
43
+ dbCache.delete(dbName);
44
+ reject(e.target.error);
45
+ };
46
+ req.onblocked = () => {
47
+ dbCache.delete(dbName);
48
+ reject(new Error(`IndexedDB open blocked for "${dbName}" \u2014 another tab has an older connection open`));
49
+ };
50
+ });
51
+ dbCache.set(dbName, promise);
52
+ return promise;
53
+ }
54
+ function run(dbName, store, mode, fn) {
55
+ return openDB(dbName).then(
56
+ (db) => new Promise((resolve, reject) => {
57
+ const tx = db.transaction(store, mode);
58
+ const s = tx.objectStore(store);
59
+ let result;
60
+ const req = fn(s);
61
+ if (req) {
62
+ req.onsuccess = () => {
63
+ result = req.result;
64
+ };
65
+ }
66
+ tx.oncomplete = () => resolve(result);
67
+ tx.onerror = () => reject(tx.error);
68
+ tx.onabort = () => reject(tx.error);
69
+ })
70
+ );
71
+ }
72
+ function createIdb(namespace) {
73
+ const dbName = `${namespace}-db`;
74
+ if (!isIdbAvailable()) {
75
+ return {
76
+ getAll: () => Promise.resolve([]),
77
+ put: () => Promise.resolve(),
78
+ delete: () => Promise.resolve(),
79
+ clear: () => Promise.resolve()
80
+ };
81
+ }
82
+ return {
83
+ getAll: async () => {
84
+ try {
85
+ const rows = await run(dbName, NOTES_STORE, "readonly", (s) => s.getAll());
86
+ return rows ?? [];
87
+ } catch {
88
+ return [];
89
+ }
90
+ },
91
+ put: async (record) => {
92
+ try {
93
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.put(record));
94
+ } catch {
95
+ }
96
+ },
97
+ delete: async (id) => {
98
+ try {
99
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.delete(id));
100
+ } catch {
101
+ }
102
+ },
103
+ clear: async () => {
104
+ try {
105
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.clear());
106
+ } catch {
107
+ }
108
+ }
109
+ };
110
+ }
111
+ function deleteQaDatabase(namespace) {
112
+ const dbName = `${namespace}-db`;
113
+ if (!isIdbAvailable()) return Promise.resolve();
114
+ const cached = dbCache.get(dbName);
115
+ dbCache.delete(dbName);
116
+ const closed = cached ? cached.then((db) => db.close()).catch(() => {
117
+ }) : Promise.resolve();
118
+ return closed.then(
119
+ () => new Promise((resolve, reject) => {
120
+ let req;
121
+ try {
122
+ req = indexedDB.deleteDatabase(dbName);
123
+ } catch (err) {
124
+ reject(err);
125
+ return;
126
+ }
127
+ req.onsuccess = () => resolve();
128
+ req.onerror = (e) => reject(e.target.error);
129
+ req.onblocked = () => reject(new Error(`IndexedDB deleteDatabase blocked for "${dbName}" \u2014 another connection is still open`));
130
+ })
131
+ );
132
+ }
6
133
 
7
134
  // src/config/schema.ts
8
135
  var DEFAULT_THEME = {
@@ -37,6 +164,9 @@ var VALID_RISKS = /* @__PURE__ */ new Set(["red", "amber", "green"]);
37
164
  function isNonEmptyString(v) {
38
165
  return typeof v === "string" && v.trim().length > 0;
39
166
  }
167
+ function stripNewlines(v) {
168
+ return v.replace(/\r\n|\r|\n/g, " ");
169
+ }
40
170
  function isValidBilingual(v) {
41
171
  if (typeof v === "string") return true;
42
172
  if (v !== null && typeof v === "object") {
@@ -85,17 +215,17 @@ function coerceCredentials(raw, warnings) {
85
215
  continue;
86
216
  }
87
217
  const cred = {
88
- role: c["role"].trim(),
89
- login: c["login"].trim(),
90
- password: isNonEmptyString(c["password"]) ? c["password"].trim() : ""
218
+ role: stripNewlines(c["role"].trim()),
219
+ login: stripNewlines(c["login"].trim()),
220
+ password: isNonEmptyString(c["password"]) ? stripNewlines(c["password"].trim()) : ""
91
221
  };
92
- if (isNonEmptyString(c["roleAr"])) cred.roleAr = c["roleAr"].trim();
222
+ if (isNonEmptyString(c["roleAr"])) cred.roleAr = stripNewlines(c["roleAr"].trim());
93
223
  if (typeof c["seeded"] === "boolean") cred.seeded = c["seeded"];
94
224
  if (c["hint"] !== null && c["hint"] !== void 0 && typeof c["hint"] === "object") {
95
225
  const h = c["hint"];
96
226
  if (typeof h["en"] === "string") {
97
- cred.hint = { en: h["en"] };
98
- if (typeof h["ar"] === "string") cred.hint.ar = h["ar"];
227
+ cred.hint = { en: stripNewlines(h["en"]) };
228
+ if (typeof h["ar"] === "string") cred.hint.ar = stripNewlines(h["ar"]);
99
229
  }
100
230
  }
101
231
  out.push(cred);
@@ -153,9 +283,11 @@ function coerceJourney(raw, warnings) {
153
283
  if (isNonEmptyString(s["riskWhy"])) step.riskWhy = s["riskWhy"];
154
284
  steps.push(step);
155
285
  }
286
+ const rawRole = lane["role"];
287
+ const role = typeof rawRole === "string" ? stripNewlines(rawRole) : { en: stripNewlines(rawRole.en), ...typeof rawRole.ar === "string" ? { ar: stripNewlines(rawRole.ar) } : {} };
156
288
  const resolved = {
157
289
  id: lane["id"].trim(),
158
- role: lane["role"],
290
+ role,
159
291
  steps
160
292
  };
161
293
  if (isNonEmptyString(lane["color"])) resolved.color = lane["color"].trim();
@@ -166,7 +298,10 @@ function coerceJourney(raw, warnings) {
166
298
  function coercePreamble(raw) {
167
299
  if (raw === null || raw === void 0) return null;
168
300
  if (typeof raw !== "object" || Array.isArray(raw)) return null;
169
- return raw;
301
+ const p = { ...raw };
302
+ if (typeof p.projectName === "string") p.projectName = stripNewlines(p.projectName);
303
+ if (typeof p.stack === "string") p.stack = stripNewlines(p.stack);
304
+ return p;
170
305
  }
171
306
  function validateConfig(input) {
172
307
  const warnings = [];
@@ -658,11 +793,12 @@ function createStorage(namespace) {
658
793
  const prefix = `${namespace}:`;
659
794
  const fallback = /* @__PURE__ */ new Map();
660
795
  const available = isStorageAvailable();
796
+ let degraded = false;
661
797
  function fullKey(key) {
662
798
  return `${prefix}${key}`;
663
799
  }
664
800
  function getItem(key) {
665
- if (available) {
801
+ if (available && !degraded) {
666
802
  try {
667
803
  return window.localStorage.getItem(fullKey(key));
668
804
  } catch {
@@ -671,11 +807,12 @@ function createStorage(namespace) {
671
807
  return fallback.get(fullKey(key)) ?? null;
672
808
  }
673
809
  function setItem(key, value) {
674
- if (available) {
810
+ if (available && !degraded) {
675
811
  try {
676
812
  window.localStorage.setItem(fullKey(key), value);
677
813
  return;
678
814
  } catch {
815
+ degraded = true;
679
816
  }
680
817
  }
681
818
  fallback.set(fullKey(key), value);
@@ -698,109 +835,6 @@ function createStorage(namespace) {
698
835
  return { getItem, setItem, getJSON, setJSON };
699
836
  }
700
837
 
701
- // src/lib/idb.ts
702
- var DB_VERSION = 2;
703
- var NOTES_STORE = "notes";
704
- var META_STORE = "meta";
705
- var dbCache = /* @__PURE__ */ new Map();
706
- function isIdbAvailable() {
707
- return typeof indexedDB !== "undefined";
708
- }
709
- function openDB(dbName) {
710
- const cached = dbCache.get(dbName);
711
- if (cached) return cached;
712
- const promise = new Promise((resolve, reject) => {
713
- let req;
714
- try {
715
- req = indexedDB.open(dbName, DB_VERSION);
716
- } catch (err) {
717
- dbCache.delete(dbName);
718
- reject(err);
719
- return;
720
- }
721
- req.onupgradeneeded = (e) => {
722
- const db = e.target.result;
723
- const oldVersion = e.oldVersion;
724
- switch (true) {
725
- case oldVersion < 1:
726
- if (!db.objectStoreNames.contains(NOTES_STORE)) {
727
- db.createObjectStore(NOTES_STORE, { keyPath: "id" });
728
- }
729
- // falls through
730
- case oldVersion < 2:
731
- if (!db.objectStoreNames.contains(META_STORE)) {
732
- db.createObjectStore(META_STORE, { keyPath: "key" });
733
- }
734
- break;
735
- }
736
- };
737
- req.onsuccess = (e) => resolve(e.target.result);
738
- req.onerror = (e) => {
739
- dbCache.delete(dbName);
740
- reject(e.target.error);
741
- };
742
- });
743
- dbCache.set(dbName, promise);
744
- return promise;
745
- }
746
- function run(dbName, store, mode, fn) {
747
- return openDB(dbName).then(
748
- (db) => new Promise((resolve, reject) => {
749
- const tx = db.transaction(store, mode);
750
- const s = tx.objectStore(store);
751
- let result;
752
- const req = fn(s);
753
- if (req) {
754
- req.onsuccess = () => {
755
- result = req.result;
756
- };
757
- }
758
- tx.oncomplete = () => resolve(result);
759
- tx.onerror = () => reject(tx.error);
760
- tx.onabort = () => reject(tx.error);
761
- })
762
- );
763
- }
764
- function createIdb(namespace) {
765
- const dbName = `${namespace}-db`;
766
- if (!isIdbAvailable()) {
767
- return {
768
- getAll: () => Promise.resolve([]),
769
- put: () => Promise.resolve(),
770
- delete: () => Promise.resolve(),
771
- clear: () => Promise.resolve()
772
- };
773
- }
774
- return {
775
- getAll: async () => {
776
- try {
777
- const rows = await run(dbName, NOTES_STORE, "readonly", (s) => s.getAll());
778
- return rows ?? [];
779
- } catch {
780
- return [];
781
- }
782
- },
783
- put: async (record) => {
784
- try {
785
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.put(record));
786
- } catch {
787
- }
788
- },
789
- delete: async (id) => {
790
- try {
791
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.delete(id));
792
- } catch {
793
- }
794
- },
795
- clear: async () => {
796
- try {
797
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.clear());
798
- } catch {
799
- }
800
- }
801
- };
802
- }
803
-
804
838
  // src/lib/strings.ts
805
839
  var STR = {
806
840
  en: {
@@ -1029,7 +1063,7 @@ function mdTable(headers, rows) {
1029
1063
  const lines = [
1030
1064
  `| ${headers.join(" | ")} |`,
1031
1065
  `| ${sep.join(" | ")} |`,
1032
- ...rows.map((r) => `| ${r.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`)
1066
+ ...rows.map((r) => `| ${r.map((c) => c.replace(/\|/g, "\\|").replace(/\r\n|\r|\n/g, " ")).join(" | ")} |`)
1033
1067
  ];
1034
1068
  return lines.join("\n");
1035
1069
  }
@@ -1633,6 +1667,17 @@ function QaFab() {
1633
1667
  const [pos, setPos] = useState(() => loadFabPos());
1634
1668
  const dragRef = useRef(null);
1635
1669
  const didDragRef = useRef(false);
1670
+ const [, setViewportTick] = useState(0);
1671
+ useEffect(() => {
1672
+ if (typeof window === "undefined") return;
1673
+ const onViewportChange = () => setViewportTick((n) => n + 1);
1674
+ window.addEventListener("resize", onViewportChange);
1675
+ window.addEventListener("orientationchange", onViewportChange);
1676
+ return () => {
1677
+ window.removeEventListener("resize", onViewportChange);
1678
+ window.removeEventListener("orientationchange", onViewportChange);
1679
+ };
1680
+ }, []);
1636
1681
  if (captureActive) return null;
1637
1682
  const onPointerDown = (e) => {
1638
1683
  if (dragRef.current) return;
@@ -1762,9 +1807,13 @@ function NoteEditor() {
1762
1807
  const [previewUrl, setPreviewUrl] = useState(null);
1763
1808
  const [dragOver, setDragOver] = useState(false);
1764
1809
  const fileRef = useRef(null);
1810
+ const previewUrlRef = useRef(null);
1811
+ useEffect(() => {
1812
+ previewUrlRef.current = previewUrl;
1813
+ }, [previewUrl]);
1765
1814
  useEffect(() => {
1766
1815
  return () => {
1767
- if (previewUrl) URL.revokeObjectURL(previewUrl);
1816
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
1768
1817
  };
1769
1818
  }, []);
1770
1819
  const setImage = useCallback((blob) => {
@@ -1960,16 +2009,17 @@ function NoteEditor() {
1960
2009
  }
1961
2010
 
1962
2011
  // src/lib/highlight.ts
2012
+ var SETTLE_TIMEOUT_MS = 400;
1963
2013
  function readCssVar(name, fallback) {
1964
2014
  if (typeof document === "undefined") return fallback;
1965
2015
  const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1966
2016
  return val || fallback;
1967
2017
  }
1968
- function paint(rect, color) {
2018
+ function paint(rect, colors) {
1969
2019
  if (typeof document === "undefined") return;
1970
2020
  if (!rect || rect.width < 1 || rect.height < 1) return;
1971
- const accent = readCssVar("--qa-accent", "#7c3aed");
1972
- const primary = readCssVar("--qa-primary", "#4f46e5");
2021
+ const accent = colors?.accent ?? readCssVar("--qa-accent", "#7c3aed");
2022
+ const primary = colors?.primary ?? readCssVar("--qa-primary", "#4f46e5");
1973
2023
  const box = document.createElement("div");
1974
2024
  box.setAttribute("data-qa-overlay", "true");
1975
2025
  Object.assign(box.style, {
@@ -1995,7 +2045,25 @@ function paint(rect, color) {
1995
2045
  if (box.parentNode) box.remove();
1996
2046
  }, 1500);
1997
2047
  }
1998
- function flashLocate(target, color) {
2048
+ function settleThenPaint(el, colors) {
2049
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
2050
+ const start = now();
2051
+ let last = null;
2052
+ let stableFrames = 0;
2053
+ const tick = () => {
2054
+ const r = el.getBoundingClientRect();
2055
+ const unchanged = !!last && r.top === last.top && r.left === last.left && r.width === last.width && r.height === last.height;
2056
+ stableFrames = unchanged ? stableFrames + 1 : 0;
2057
+ 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);
2060
+ return;
2061
+ }
2062
+ requestAnimationFrame(tick);
2063
+ };
2064
+ requestAnimationFrame(tick);
2065
+ }
2066
+ function flashLocate(target, colors) {
1999
2067
  if (typeof document === "undefined" || !target) return;
2000
2068
  let el = null;
2001
2069
  if (target.selector) {
@@ -2007,13 +2075,18 @@ function flashLocate(target, color) {
2007
2075
  }
2008
2076
  if (el) {
2009
2077
  el.scrollIntoView({ block: "center", inline: "center" });
2010
- requestAnimationFrame(() => {
2011
- if (!el) return;
2012
- const r = el.getBoundingClientRect();
2013
- paint({ top: r.top, left: r.left, width: r.width, height: r.height });
2014
- });
2078
+ settleThenPaint(el, colors);
2015
2079
  } else if (target.rect) {
2016
- paint(target.rect);
2080
+ let rect = target.rect;
2081
+ const snap = target.scroll;
2082
+ if (snap) {
2083
+ const dx = window.scrollX - snap.x;
2084
+ const dy = window.scrollY - snap.y;
2085
+ if (dx || dy) {
2086
+ rect = { ...rect, left: rect.left - dx, top: rect.top - dy };
2087
+ }
2088
+ }
2089
+ paint(rect, colors);
2017
2090
  }
2018
2091
  }
2019
2092
  function LocationReveal({ target }) {
@@ -2097,7 +2170,7 @@ function LocationReveal({ target }) {
2097
2170
  /* @__PURE__ */ jsxs(
2098
2171
  "button",
2099
2172
  {
2100
- onClick: () => flashLocate(target),
2173
+ onClick: () => flashLocate(target, { primary: theme.primary, accent: theme.accent }),
2101
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",
2102
2175
  style: { background: theme.accent, border: "none", cursor: "pointer" },
2103
2176
  children: [
@@ -2371,8 +2444,37 @@ function NoteList() {
2371
2444
  }
2372
2445
  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)) });
2373
2446
  }
2374
- function CopyField({ value, ink }) {
2447
+ function EyeIcon({ open, size = 12, className }) {
2448
+ return /* @__PURE__ */ jsx(
2449
+ "svg",
2450
+ {
2451
+ xmlns: "http://www.w3.org/2000/svg",
2452
+ viewBox: "0 0 24 24",
2453
+ width: size,
2454
+ height: size,
2455
+ fill: "none",
2456
+ stroke: "currentColor",
2457
+ strokeWidth: 2,
2458
+ strokeLinecap: "round",
2459
+ strokeLinejoin: "round",
2460
+ className,
2461
+ "aria-hidden": "true",
2462
+ children: open ? /* @__PURE__ */ jsxs(Fragment, { children: [
2463
+ /* @__PURE__ */ jsx("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }),
2464
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
2465
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2466
+ /* @__PURE__ */ jsx("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }),
2467
+ /* @__PURE__ */ jsx("path", { d: "M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68" }),
2468
+ /* @__PURE__ */ jsx("path", { d: "M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61" }),
2469
+ /* @__PURE__ */ jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" })
2470
+ ] })
2471
+ }
2472
+ );
2473
+ }
2474
+ var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2475
+ function CopyField({ value, ink, maskable = false }) {
2375
2476
  const [done, setDone] = useState(false);
2477
+ const [revealed, setRevealed] = useState(true);
2376
2478
  const copy = async () => {
2377
2479
  if (value === "\u2014") return;
2378
2480
  if (typeof navigator === "undefined" || !navigator.clipboard) return;
@@ -2383,20 +2485,36 @@ function CopyField({ value, ink }) {
2383
2485
  } catch {
2384
2486
  }
2385
2487
  };
2386
- return /* @__PURE__ */ jsxs(
2387
- "button",
2388
- {
2389
- onClick: copy,
2390
- disabled: value === "\u2014",
2391
- dir: "ltr",
2392
- 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",
2393
- style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2394
- children: [
2395
- /* @__PURE__ */ jsx("span", { style: { color: ink }, children: value }),
2396
- 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" }))
2397
- ]
2398
- }
2399
- );
2488
+ const hidden = maskable && !revealed && value !== "\u2014";
2489
+ const displayValue = hidden ? MASK : value;
2490
+ return /* @__PURE__ */ jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1", children: [
2491
+ /* @__PURE__ */ jsxs(
2492
+ "button",
2493
+ {
2494
+ onClick: copy,
2495
+ disabled: value === "\u2014",
2496
+ dir: "ltr",
2497
+ 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
+ style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2499
+ 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" }))
2502
+ ]
2503
+ }
2504
+ ),
2505
+ maskable && value !== "\u2014" && /* @__PURE__ */ jsx(
2506
+ "button",
2507
+ {
2508
+ type: "button",
2509
+ onClick: () => setRevealed((r) => !r),
2510
+ "aria-label": revealed ? "Hide password" : "Show password",
2511
+ title: revealed ? "Hide password" : "Show password",
2512
+ className: "qa-inline-flex qa-items-center qa-rounded-md qa-p-0.5 qa-opacity-40 qa-hover-opacity-80",
2513
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2514
+ children: /* @__PURE__ */ jsx(EyeIcon, { open: revealed, size: 12 })
2515
+ }
2516
+ )
2517
+ ] });
2400
2518
  }
2401
2519
  function CredentialsSection() {
2402
2520
  const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, theme } = useQa();
@@ -2414,7 +2532,7 @@ function CredentialsSection() {
2414
2532
  }
2415
2533
  )
2416
2534
  ] }),
2417
- credentials.map((c) => {
2535
+ credentials.map((c, i) => {
2418
2536
  const used = loginsUsed.has(c.role);
2419
2537
  const label = lang === "ar" && c.roleAr ? c.roleAr : c.role;
2420
2538
  return /* @__PURE__ */ jsxs(
@@ -2452,11 +2570,11 @@ function CredentialsSection() {
2452
2570
  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: [
2453
2571
  /* @__PURE__ */ jsx(CopyField, { value: c.login, ink: theme.ink }),
2454
2572
  /* @__PURE__ */ jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
2455
- /* @__PURE__ */ jsx(CopyField, { value: c.password, ink: theme.ink })
2573
+ /* @__PURE__ */ jsx(CopyField, { value: c.password, ink: theme.ink, maskable: true })
2456
2574
  ] })
2457
2575
  ]
2458
2576
  },
2459
- c.role
2577
+ `${c.role}-${i}`
2460
2578
  );
2461
2579
  })
2462
2580
  ] });
@@ -2527,7 +2645,7 @@ function Lane({
2527
2645
  style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
2528
2646
  }
2529
2647
  ),
2530
- steps.map((s) => {
2648
+ steps.map((s, i) => {
2531
2649
  const k = keyOf(id, s.path);
2532
2650
  const on = checked.has(k);
2533
2651
  const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
@@ -2591,7 +2709,7 @@ function Lane({
2591
2709
  ] })
2592
2710
  ]
2593
2711
  }
2594
- ) }, s.path);
2712
+ ) }, `${k}-${i}`);
2595
2713
  })
2596
2714
  ] })
2597
2715
  ]
@@ -2735,6 +2853,8 @@ function QaPanel() {
2735
2853
  }
2736
2854
  if (phase === "hidden") {
2737
2855
  setShowIn(false);
2856
+ setNaming(false);
2857
+ setConfirmClear(false);
2738
2858
  }
2739
2859
  if (phase === "visible") {
2740
2860
  setShowIn(true);
@@ -2931,7 +3051,8 @@ function QaPanel() {
2931
3051
  activeTab,
2932
3052
  setActiveTab,
2933
3053
  t,
2934
- theme
3054
+ theme,
3055
+ lang
2935
3056
  }
2936
3057
  ),
2937
3058
  /* @__PURE__ */ jsx("div", { className: "qa-h-px", style: { background: `${theme.primary}14` } }),
@@ -3083,7 +3204,8 @@ function TabsBar({
3083
3204
  activeTab,
3084
3205
  setActiveTab,
3085
3206
  t,
3086
- theme
3207
+ theme,
3208
+ lang
3087
3209
  }) {
3088
3210
  const tabRefs = useRef([]);
3089
3211
  const barRef = useRef(null);
@@ -3095,7 +3217,7 @@ function TabsBar({
3095
3217
  if (!btn || !bar) return;
3096
3218
  bar.style.left = `${btn.offsetLeft + 8}px`;
3097
3219
  bar.style.width = `${Math.max(0, btn.offsetWidth - 16)}px`;
3098
- }, [activeTab]);
3220
+ }, [activeTab, lang]);
3099
3221
  useLayoutEffect(() => {
3100
3222
  reposition();
3101
3223
  }, [reposition]);
@@ -3144,6 +3266,13 @@ function TabsBar({
3144
3266
  }
3145
3267
 
3146
3268
  // src/lib/capture.ts
3269
+ var HTML2CANVAS_TIMEOUT_MS = 1e4;
3270
+ function withTimeout(promise, ms) {
3271
+ return Promise.race([
3272
+ promise,
3273
+ new Promise((resolve) => setTimeout(() => resolve(null), ms))
3274
+ ]);
3275
+ }
3147
3276
  function toBlob(canvas) {
3148
3277
  return new Promise((resolve) => {
3149
3278
  if (canvas.toBlob) {
@@ -3165,24 +3294,28 @@ async function captureRegion(rect, scroll) {
3165
3294
  try {
3166
3295
  const { default: html2canvas } = await import('html2canvas');
3167
3296
  const scale = Math.min(window.devicePixelRatio || 1, 2);
3168
- const canvas = await html2canvas(document.body, {
3169
- x: sx + rect.left,
3170
- y: sy + rect.top,
3171
- width: rect.width,
3172
- height: rect.height,
3173
- scale,
3174
- useCORS: true,
3175
- allowTaint: true,
3176
- backgroundColor: null,
3177
- logging: false,
3178
- scrollX: sx,
3179
- scrollY: sy,
3180
- // Viewport-only clone (not the full document) — see iOS canvas-cap
3181
- // rationale above. Keeps the offscreen render surface ~viewport*scale.
3182
- windowWidth: window.innerWidth,
3183
- windowHeight: window.innerHeight,
3184
- ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay")
3185
- });
3297
+ const canvas = await withTimeout(
3298
+ html2canvas(document.body, {
3299
+ x: sx + rect.left,
3300
+ y: sy + rect.top,
3301
+ width: rect.width,
3302
+ height: rect.height,
3303
+ scale,
3304
+ useCORS: true,
3305
+ allowTaint: true,
3306
+ backgroundColor: null,
3307
+ logging: false,
3308
+ scrollX: sx,
3309
+ scrollY: sy,
3310
+ // Viewport-only clone (not the full document) see iOS canvas-cap
3311
+ // rationale above. Keeps the offscreen render surface ~viewport*scale.
3312
+ windowWidth: window.innerWidth,
3313
+ windowHeight: window.innerHeight,
3314
+ ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay")
3315
+ }),
3316
+ HTML2CANVAS_TIMEOUT_MS
3317
+ );
3318
+ if (!canvas) return null;
3186
3319
  return await toBlob(canvas);
3187
3320
  } catch (err) {
3188
3321
  console.warn("[QA] region capture failed:", err);
@@ -3195,7 +3328,8 @@ function isCleanId(id) {
3195
3328
  return !!id && /^[a-zA-Z][\w-]*$/.test(id) && id.length <= 40;
3196
3329
  }
3197
3330
  function esc(value) {
3198
- return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
3331
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
3332
+ return value.replace(/[\\"]/g, "\\$&");
3199
3333
  }
3200
3334
  function nthOfTypePath(el, maxDepth = 6) {
3201
3335
  if (typeof document === "undefined" || !document.body) return "";
@@ -3223,46 +3357,68 @@ function nthOfTypePath(el, maxDepth = 6) {
3223
3357
  }
3224
3358
  return parts.join(" > ");
3225
3359
  }
3360
+ function isUniqueSelector(selector) {
3361
+ if (!selector) return false;
3362
+ try {
3363
+ return document.querySelectorAll(selector).length === 1;
3364
+ } catch {
3365
+ return false;
3366
+ }
3367
+ }
3226
3368
  function getStableSelector(el) {
3227
3369
  if (typeof document === "undefined") return "";
3228
3370
  if (!el || el.nodeType !== 1) return "";
3229
3371
  const tag = el.tagName.toLowerCase();
3230
3372
  const htmlEl = el;
3231
- if (isCleanId(htmlEl.id)) return `#${esc(htmlEl.id)}`;
3373
+ if (isCleanId(htmlEl.id)) {
3374
+ const candidate = `#${esc(htmlEl.id)}`;
3375
+ if (isUniqueSelector(candidate)) return candidate;
3376
+ }
3232
3377
  for (const attr of ["data-testid", "data-test", "data-cy", "data-id", "data-key"]) {
3233
3378
  const val = el.getAttribute(attr);
3234
- if (val) return `[${attr}="${esc(val)}"]`;
3379
+ if (val) {
3380
+ const candidate = `[${attr}="${esc(val)}"]`;
3381
+ if (isUniqueSelector(candidate)) return candidate;
3382
+ }
3235
3383
  }
3236
3384
  if (["button", "a", "input", "select", "textarea"].includes(tag)) {
3237
3385
  const label = el.getAttribute("aria-label");
3238
- if (label) return `${tag}[aria-label="${esc(label)}"]`;
3386
+ if (label) {
3387
+ const candidate = `${tag}[aria-label="${esc(label)}"]`;
3388
+ if (isUniqueSelector(candidate)) return candidate;
3389
+ }
3239
3390
  }
3240
3391
  const name = el.getAttribute("name");
3241
3392
  if (name && ["input", "select", "textarea"].includes(tag)) {
3242
- return `${tag}[name="${esc(name)}"]`;
3393
+ const candidate = `${tag}[name="${esc(name)}"]`;
3394
+ if (isUniqueSelector(candidate)) return candidate;
3243
3395
  }
3244
3396
  return nthOfTypePath(el);
3245
3397
  }
3246
3398
 
3247
3399
  // src/lib/scrollLock.ts
3248
- var locked = false;
3400
+ var lockCount = 0;
3249
3401
  var prevHtmlOverflow = "";
3250
3402
  var prevBodyOverflow = "";
3251
3403
  function lockPageScroll() {
3252
- if (typeof document === "undefined" || locked) return;
3253
- const html = document.documentElement;
3254
- const body = document.body;
3255
- prevHtmlOverflow = html.style.overflow;
3256
- prevBodyOverflow = body ? body.style.overflow : "";
3257
- html.style.overflow = "hidden";
3258
- if (body) body.style.overflow = "hidden";
3259
- locked = true;
3404
+ if (typeof document === "undefined") return;
3405
+ if (lockCount === 0) {
3406
+ const html = document.documentElement;
3407
+ const body = document.body;
3408
+ prevHtmlOverflow = html.style.overflow;
3409
+ prevBodyOverflow = body ? body.style.overflow : "";
3410
+ html.style.overflow = "hidden";
3411
+ if (body) body.style.overflow = "hidden";
3412
+ }
3413
+ lockCount++;
3260
3414
  }
3261
3415
  function unlockPageScroll() {
3262
- if (typeof document === "undefined" || !locked) return;
3263
- document.documentElement.style.overflow = prevHtmlOverflow;
3264
- if (document.body) document.body.style.overflow = prevBodyOverflow;
3265
- locked = false;
3416
+ if (typeof document === "undefined" || lockCount === 0) return;
3417
+ lockCount--;
3418
+ if (lockCount === 0) {
3419
+ document.documentElement.style.overflow = prevHtmlOverflow;
3420
+ if (document.body) document.body.style.overflow = prevBodyOverflow;
3421
+ }
3266
3422
  }
3267
3423
  var DRAG_THRESHOLD2 = 6;
3268
3424
  var TOUCH_DRAG_THRESHOLD = 12;
@@ -3281,6 +3437,7 @@ function CaptureMode() {
3281
3437
  const { addNote, endCapture, t, dir, theme } = useQa();
3282
3438
  const coarse = useCoarsePointer();
3283
3439
  const layerRef = useRef(null);
3440
+ const overlayRootRef = useRef(null);
3284
3441
  const [phase, setPhase] = useState("selecting");
3285
3442
  const [hover, setHover] = useState(null);
3286
3443
  const [drag, setDrag] = useState(null);
@@ -3297,6 +3454,10 @@ function CaptureMode() {
3297
3454
  const pointerKind = useRef("mouse");
3298
3455
  const scrollSnap = useRef({ x: 0, y: 0 });
3299
3456
  const handleDragRef = useRef(null);
3457
+ const mountedRef = useRef(true);
3458
+ useEffect(() => () => {
3459
+ mountedRef.current = false;
3460
+ }, []);
3300
3461
  const [cardIn, setCardIn] = useState(false);
3301
3462
  const elementUnder = useCallback((x, y) => {
3302
3463
  const layer = layerRef.current;
@@ -3319,6 +3480,10 @@ function CaptureMode() {
3319
3480
  lockPageScroll();
3320
3481
  try {
3321
3482
  const blob = await captureRegion(sel.rect, scrollSnap.current);
3483
+ if (!mountedRef.current) {
3484
+ if (blob) URL.revokeObjectURL(URL.createObjectURL(blob));
3485
+ return;
3486
+ }
3322
3487
  setShot(blob);
3323
3488
  setShotUrl((old) => {
3324
3489
  if (old) URL.revokeObjectURL(old);
@@ -3326,7 +3491,7 @@ function CaptureMode() {
3326
3491
  });
3327
3492
  } finally {
3328
3493
  unlockPageScroll();
3329
- setCapturing(false);
3494
+ if (mountedRef.current) setCapturing(false);
3330
3495
  }
3331
3496
  }, []);
3332
3497
  useEffect(() => {
@@ -3495,6 +3660,38 @@ function CaptureMode() {
3495
3660
  document.addEventListener("keydown", onKey, true);
3496
3661
  return () => document.removeEventListener("keydown", onKey, true);
3497
3662
  }, [endCapture]);
3663
+ useEffect(() => {
3664
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
3665
+ const onKeyDown = (e) => {
3666
+ if (e.key !== "Tab") return;
3667
+ const root = overlayRootRef.current;
3668
+ if (!root) return;
3669
+ const focusable = Array.from(
3670
+ root.querySelectorAll(FOCUSABLE_SELECTOR)
3671
+ ).filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
3672
+ if (focusable.length === 0) {
3673
+ e.preventDefault();
3674
+ return;
3675
+ }
3676
+ const first = focusable[0];
3677
+ const last = focusable[focusable.length - 1];
3678
+ const active = document.activeElement;
3679
+ const activeInside = !!active && root.contains(active);
3680
+ if (e.shiftKey) {
3681
+ if (!activeInside || active === first) {
3682
+ e.preventDefault();
3683
+ last.focus();
3684
+ }
3685
+ } else {
3686
+ if (!activeInside || active === last) {
3687
+ e.preventDefault();
3688
+ first.focus();
3689
+ }
3690
+ }
3691
+ };
3692
+ document.addEventListener("keydown", onKeyDown, true);
3693
+ return () => document.removeEventListener("keydown", onKeyDown, true);
3694
+ }, []);
3498
3695
  useEffect(() => {
3499
3696
  if (phase === "annotating" && taRef.current) taRef.current.focus();
3500
3697
  }, [phase]);
@@ -3516,7 +3713,8 @@ function CaptureMode() {
3516
3713
  left: Math.round(selection.rect.left),
3517
3714
  width: Math.round(selection.rect.width),
3518
3715
  height: Math.round(selection.rect.height)
3519
- }
3716
+ },
3717
+ scroll: { ...scrollSnap.current }
3520
3718
  };
3521
3719
  await addNote({ description, screenshot: shot ?? void 0, target });
3522
3720
  endCapture();
@@ -3540,7 +3738,7 @@ function CaptureMode() {
3540
3738
  const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
3541
3739
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
3542
3740
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
3543
- return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", children: [
3741
+ return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", ref: overlayRootRef, children: [
3544
3742
  /* @__PURE__ */ jsx(
3545
3743
  "div",
3546
3744
  {
@@ -3552,7 +3750,7 @@ function CaptureMode() {
3552
3750
  className: "qa-fixed qa-inset-0 qa-z-10090",
3553
3751
  style: {
3554
3752
  cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
3555
- touchAction: coarse ? regionMode ? "none" : "pan-x pan-y" : "auto",
3753
+ touchAction: coarse ? "none" : "auto",
3556
3754
  background: "rgba(58,42,46,0.18)"
3557
3755
  }
3558
3756
  }
@@ -4009,6 +4207,6 @@ function Qapture({ config }) {
4009
4207
  return null;
4010
4208
  }
4011
4209
 
4012
- export { Qapture, initQaStudio };
4013
- //# sourceMappingURL=chunk-L6VS36GE.js.map
4014
- //# sourceMappingURL=chunk-L6VS36GE.js.map
4210
+ export { Qapture, deleteQaDatabase, initQaStudio };
4211
+ //# sourceMappingURL=chunk-RC7ZUQ5X.js.map
4212
+ //# sourceMappingURL=chunk-RC7ZUQ5X.js.map