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.
@@ -9,7 +9,134 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
  var React__default = /*#__PURE__*/_interopDefault(React);
10
10
  var ReactDOM__default = /*#__PURE__*/_interopDefault(ReactDOM);
11
11
 
12
- // src/index.ts
12
+ // src/lib/idb.ts
13
+ var DB_VERSION = 2;
14
+ var NOTES_STORE = "notes";
15
+ var META_STORE = "meta";
16
+ var dbCache = /* @__PURE__ */ new Map();
17
+ function isIdbAvailable() {
18
+ return typeof indexedDB !== "undefined";
19
+ }
20
+ function openDB(dbName) {
21
+ const cached = dbCache.get(dbName);
22
+ if (cached) return cached;
23
+ const promise = new Promise((resolve, reject) => {
24
+ let req;
25
+ try {
26
+ req = indexedDB.open(dbName, DB_VERSION);
27
+ } catch (err) {
28
+ dbCache.delete(dbName);
29
+ reject(err);
30
+ return;
31
+ }
32
+ req.onupgradeneeded = (e) => {
33
+ const db = e.target.result;
34
+ const oldVersion = e.oldVersion;
35
+ switch (true) {
36
+ case oldVersion < 1:
37
+ if (!db.objectStoreNames.contains(NOTES_STORE)) {
38
+ db.createObjectStore(NOTES_STORE, { keyPath: "id" });
39
+ }
40
+ // falls through
41
+ case oldVersion < 2:
42
+ if (!db.objectStoreNames.contains(META_STORE)) {
43
+ db.createObjectStore(META_STORE, { keyPath: "key" });
44
+ }
45
+ break;
46
+ }
47
+ };
48
+ req.onsuccess = (e) => resolve(e.target.result);
49
+ req.onerror = (e) => {
50
+ dbCache.delete(dbName);
51
+ reject(e.target.error);
52
+ };
53
+ req.onblocked = () => {
54
+ dbCache.delete(dbName);
55
+ reject(new Error(`IndexedDB open blocked for "${dbName}" \u2014 another tab has an older connection open`));
56
+ };
57
+ });
58
+ dbCache.set(dbName, promise);
59
+ return promise;
60
+ }
61
+ function run(dbName, store, mode, fn) {
62
+ return openDB(dbName).then(
63
+ (db) => new Promise((resolve, reject) => {
64
+ const tx = db.transaction(store, mode);
65
+ const s = tx.objectStore(store);
66
+ let result;
67
+ const req = fn(s);
68
+ if (req) {
69
+ req.onsuccess = () => {
70
+ result = req.result;
71
+ };
72
+ }
73
+ tx.oncomplete = () => resolve(result);
74
+ tx.onerror = () => reject(tx.error);
75
+ tx.onabort = () => reject(tx.error);
76
+ })
77
+ );
78
+ }
79
+ function createIdb(namespace) {
80
+ const dbName = `${namespace}-db`;
81
+ if (!isIdbAvailable()) {
82
+ return {
83
+ getAll: () => Promise.resolve([]),
84
+ put: () => Promise.resolve(),
85
+ delete: () => Promise.resolve(),
86
+ clear: () => Promise.resolve()
87
+ };
88
+ }
89
+ return {
90
+ getAll: async () => {
91
+ try {
92
+ const rows = await run(dbName, NOTES_STORE, "readonly", (s) => s.getAll());
93
+ return rows ?? [];
94
+ } catch {
95
+ return [];
96
+ }
97
+ },
98
+ put: async (record) => {
99
+ try {
100
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.put(record));
101
+ } catch {
102
+ }
103
+ },
104
+ delete: async (id) => {
105
+ try {
106
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.delete(id));
107
+ } catch {
108
+ }
109
+ },
110
+ clear: async () => {
111
+ try {
112
+ await run(dbName, NOTES_STORE, "readwrite", (s) => s.clear());
113
+ } catch {
114
+ }
115
+ }
116
+ };
117
+ }
118
+ function deleteQaDatabase(namespace) {
119
+ const dbName = `${namespace}-db`;
120
+ if (!isIdbAvailable()) return Promise.resolve();
121
+ const cached = dbCache.get(dbName);
122
+ dbCache.delete(dbName);
123
+ const closed = cached ? cached.then((db) => db.close()).catch(() => {
124
+ }) : Promise.resolve();
125
+ return closed.then(
126
+ () => new Promise((resolve, reject) => {
127
+ let req;
128
+ try {
129
+ req = indexedDB.deleteDatabase(dbName);
130
+ } catch (err) {
131
+ reject(err);
132
+ return;
133
+ }
134
+ req.onsuccess = () => resolve();
135
+ req.onerror = (e) => reject(e.target.error);
136
+ req.onblocked = () => reject(new Error(`IndexedDB deleteDatabase blocked for "${dbName}" \u2014 another connection is still open`));
137
+ })
138
+ );
139
+ }
13
140
 
14
141
  // src/config/schema.ts
15
142
  var DEFAULT_THEME = {
@@ -44,6 +171,9 @@ var VALID_RISKS = /* @__PURE__ */ new Set(["red", "amber", "green"]);
44
171
  function isNonEmptyString(v) {
45
172
  return typeof v === "string" && v.trim().length > 0;
46
173
  }
174
+ function stripNewlines(v) {
175
+ return v.replace(/\r\n|\r|\n/g, " ");
176
+ }
47
177
  function isValidBilingual(v) {
48
178
  if (typeof v === "string") return true;
49
179
  if (v !== null && typeof v === "object") {
@@ -92,17 +222,17 @@ function coerceCredentials(raw, warnings) {
92
222
  continue;
93
223
  }
94
224
  const cred = {
95
- role: c["role"].trim(),
96
- login: c["login"].trim(),
97
- password: isNonEmptyString(c["password"]) ? c["password"].trim() : ""
225
+ role: stripNewlines(c["role"].trim()),
226
+ login: stripNewlines(c["login"].trim()),
227
+ password: isNonEmptyString(c["password"]) ? stripNewlines(c["password"].trim()) : ""
98
228
  };
99
- if (isNonEmptyString(c["roleAr"])) cred.roleAr = c["roleAr"].trim();
229
+ if (isNonEmptyString(c["roleAr"])) cred.roleAr = stripNewlines(c["roleAr"].trim());
100
230
  if (typeof c["seeded"] === "boolean") cred.seeded = c["seeded"];
101
231
  if (c["hint"] !== null && c["hint"] !== void 0 && typeof c["hint"] === "object") {
102
232
  const h = c["hint"];
103
233
  if (typeof h["en"] === "string") {
104
- cred.hint = { en: h["en"] };
105
- if (typeof h["ar"] === "string") cred.hint.ar = h["ar"];
234
+ cred.hint = { en: stripNewlines(h["en"]) };
235
+ if (typeof h["ar"] === "string") cred.hint.ar = stripNewlines(h["ar"]);
106
236
  }
107
237
  }
108
238
  out.push(cred);
@@ -160,9 +290,11 @@ function coerceJourney(raw, warnings) {
160
290
  if (isNonEmptyString(s["riskWhy"])) step.riskWhy = s["riskWhy"];
161
291
  steps.push(step);
162
292
  }
293
+ const rawRole = lane["role"];
294
+ const role = typeof rawRole === "string" ? stripNewlines(rawRole) : { en: stripNewlines(rawRole.en), ...typeof rawRole.ar === "string" ? { ar: stripNewlines(rawRole.ar) } : {} };
163
295
  const resolved = {
164
296
  id: lane["id"].trim(),
165
- role: lane["role"],
297
+ role,
166
298
  steps
167
299
  };
168
300
  if (isNonEmptyString(lane["color"])) resolved.color = lane["color"].trim();
@@ -173,7 +305,10 @@ function coerceJourney(raw, warnings) {
173
305
  function coercePreamble(raw) {
174
306
  if (raw === null || raw === void 0) return null;
175
307
  if (typeof raw !== "object" || Array.isArray(raw)) return null;
176
- return raw;
308
+ const p = { ...raw };
309
+ if (typeof p.projectName === "string") p.projectName = stripNewlines(p.projectName);
310
+ if (typeof p.stack === "string") p.stack = stripNewlines(p.stack);
311
+ return p;
177
312
  }
178
313
  function validateConfig(input) {
179
314
  const warnings = [];
@@ -665,11 +800,12 @@ function createStorage(namespace) {
665
800
  const prefix = `${namespace}:`;
666
801
  const fallback = /* @__PURE__ */ new Map();
667
802
  const available = isStorageAvailable();
803
+ let degraded = false;
668
804
  function fullKey(key) {
669
805
  return `${prefix}${key}`;
670
806
  }
671
807
  function getItem(key) {
672
- if (available) {
808
+ if (available && !degraded) {
673
809
  try {
674
810
  return window.localStorage.getItem(fullKey(key));
675
811
  } catch {
@@ -678,11 +814,12 @@ function createStorage(namespace) {
678
814
  return fallback.get(fullKey(key)) ?? null;
679
815
  }
680
816
  function setItem(key, value) {
681
- if (available) {
817
+ if (available && !degraded) {
682
818
  try {
683
819
  window.localStorage.setItem(fullKey(key), value);
684
820
  return;
685
821
  } catch {
822
+ degraded = true;
686
823
  }
687
824
  }
688
825
  fallback.set(fullKey(key), value);
@@ -705,109 +842,6 @@ function createStorage(namespace) {
705
842
  return { getItem, setItem, getJSON, setJSON };
706
843
  }
707
844
 
708
- // src/lib/idb.ts
709
- var DB_VERSION = 2;
710
- var NOTES_STORE = "notes";
711
- var META_STORE = "meta";
712
- var dbCache = /* @__PURE__ */ new Map();
713
- function isIdbAvailable() {
714
- return typeof indexedDB !== "undefined";
715
- }
716
- function openDB(dbName) {
717
- const cached = dbCache.get(dbName);
718
- if (cached) return cached;
719
- const promise = new Promise((resolve, reject) => {
720
- let req;
721
- try {
722
- req = indexedDB.open(dbName, DB_VERSION);
723
- } catch (err) {
724
- dbCache.delete(dbName);
725
- reject(err);
726
- return;
727
- }
728
- req.onupgradeneeded = (e) => {
729
- const db = e.target.result;
730
- const oldVersion = e.oldVersion;
731
- switch (true) {
732
- case oldVersion < 1:
733
- if (!db.objectStoreNames.contains(NOTES_STORE)) {
734
- db.createObjectStore(NOTES_STORE, { keyPath: "id" });
735
- }
736
- // falls through
737
- case oldVersion < 2:
738
- if (!db.objectStoreNames.contains(META_STORE)) {
739
- db.createObjectStore(META_STORE, { keyPath: "key" });
740
- }
741
- break;
742
- }
743
- };
744
- req.onsuccess = (e) => resolve(e.target.result);
745
- req.onerror = (e) => {
746
- dbCache.delete(dbName);
747
- reject(e.target.error);
748
- };
749
- });
750
- dbCache.set(dbName, promise);
751
- return promise;
752
- }
753
- function run(dbName, store, mode, fn) {
754
- return openDB(dbName).then(
755
- (db) => new Promise((resolve, reject) => {
756
- const tx = db.transaction(store, mode);
757
- const s = tx.objectStore(store);
758
- let result;
759
- const req = fn(s);
760
- if (req) {
761
- req.onsuccess = () => {
762
- result = req.result;
763
- };
764
- }
765
- tx.oncomplete = () => resolve(result);
766
- tx.onerror = () => reject(tx.error);
767
- tx.onabort = () => reject(tx.error);
768
- })
769
- );
770
- }
771
- function createIdb(namespace) {
772
- const dbName = `${namespace}-db`;
773
- if (!isIdbAvailable()) {
774
- return {
775
- getAll: () => Promise.resolve([]),
776
- put: () => Promise.resolve(),
777
- delete: () => Promise.resolve(),
778
- clear: () => Promise.resolve()
779
- };
780
- }
781
- return {
782
- getAll: async () => {
783
- try {
784
- const rows = await run(dbName, NOTES_STORE, "readonly", (s) => s.getAll());
785
- return rows ?? [];
786
- } catch {
787
- return [];
788
- }
789
- },
790
- put: async (record) => {
791
- try {
792
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.put(record));
793
- } catch {
794
- }
795
- },
796
- delete: async (id) => {
797
- try {
798
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.delete(id));
799
- } catch {
800
- }
801
- },
802
- clear: async () => {
803
- try {
804
- await run(dbName, NOTES_STORE, "readwrite", (s) => s.clear());
805
- } catch {
806
- }
807
- }
808
- };
809
- }
810
-
811
845
  // src/lib/strings.ts
812
846
  var STR = {
813
847
  en: {
@@ -1036,7 +1070,7 @@ function mdTable(headers, rows) {
1036
1070
  const lines = [
1037
1071
  `| ${headers.join(" | ")} |`,
1038
1072
  `| ${sep.join(" | ")} |`,
1039
- ...rows.map((r) => `| ${r.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`)
1073
+ ...rows.map((r) => `| ${r.map((c) => c.replace(/\|/g, "\\|").replace(/\r\n|\r|\n/g, " ")).join(" | ")} |`)
1040
1074
  ];
1041
1075
  return lines.join("\n");
1042
1076
  }
@@ -1640,6 +1674,17 @@ function QaFab() {
1640
1674
  const [pos, setPos] = React.useState(() => loadFabPos());
1641
1675
  const dragRef = React.useRef(null);
1642
1676
  const didDragRef = React.useRef(false);
1677
+ const [, setViewportTick] = React.useState(0);
1678
+ React.useEffect(() => {
1679
+ if (typeof window === "undefined") return;
1680
+ const onViewportChange = () => setViewportTick((n) => n + 1);
1681
+ window.addEventListener("resize", onViewportChange);
1682
+ window.addEventListener("orientationchange", onViewportChange);
1683
+ return () => {
1684
+ window.removeEventListener("resize", onViewportChange);
1685
+ window.removeEventListener("orientationchange", onViewportChange);
1686
+ };
1687
+ }, []);
1643
1688
  if (captureActive) return null;
1644
1689
  const onPointerDown = (e) => {
1645
1690
  if (dragRef.current) return;
@@ -1769,9 +1814,13 @@ function NoteEditor() {
1769
1814
  const [previewUrl, setPreviewUrl] = React.useState(null);
1770
1815
  const [dragOver, setDragOver] = React.useState(false);
1771
1816
  const fileRef = React.useRef(null);
1817
+ const previewUrlRef = React.useRef(null);
1818
+ React.useEffect(() => {
1819
+ previewUrlRef.current = previewUrl;
1820
+ }, [previewUrl]);
1772
1821
  React.useEffect(() => {
1773
1822
  return () => {
1774
- if (previewUrl) URL.revokeObjectURL(previewUrl);
1823
+ if (previewUrlRef.current) URL.revokeObjectURL(previewUrlRef.current);
1775
1824
  };
1776
1825
  }, []);
1777
1826
  const setImage = React.useCallback((blob) => {
@@ -1967,16 +2016,17 @@ function NoteEditor() {
1967
2016
  }
1968
2017
 
1969
2018
  // src/lib/highlight.ts
2019
+ var SETTLE_TIMEOUT_MS = 400;
1970
2020
  function readCssVar(name, fallback) {
1971
2021
  if (typeof document === "undefined") return fallback;
1972
2022
  const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1973
2023
  return val || fallback;
1974
2024
  }
1975
- function paint(rect, color) {
2025
+ function paint(rect, colors) {
1976
2026
  if (typeof document === "undefined") return;
1977
2027
  if (!rect || rect.width < 1 || rect.height < 1) return;
1978
- const accent = readCssVar("--qa-accent", "#7c3aed");
1979
- const primary = readCssVar("--qa-primary", "#4f46e5");
2028
+ const accent = colors?.accent ?? readCssVar("--qa-accent", "#7c3aed");
2029
+ const primary = colors?.primary ?? readCssVar("--qa-primary", "#4f46e5");
1980
2030
  const box = document.createElement("div");
1981
2031
  box.setAttribute("data-qa-overlay", "true");
1982
2032
  Object.assign(box.style, {
@@ -2002,7 +2052,25 @@ function paint(rect, color) {
2002
2052
  if (box.parentNode) box.remove();
2003
2053
  }, 1500);
2004
2054
  }
2005
- function flashLocate(target, color) {
2055
+ function settleThenPaint(el, colors) {
2056
+ const now = () => typeof performance !== "undefined" ? performance.now() : Date.now();
2057
+ const start = now();
2058
+ let last = null;
2059
+ let stableFrames = 0;
2060
+ const tick = () => {
2061
+ const r = el.getBoundingClientRect();
2062
+ const unchanged = !!last && r.top === last.top && r.left === last.left && r.width === last.width && r.height === last.height;
2063
+ stableFrames = unchanged ? stableFrames + 1 : 0;
2064
+ last = r;
2065
+ if (stableFrames >= 2 || now() - start >= SETTLE_TIMEOUT_MS) {
2066
+ paint({ top: r.top, left: r.left, width: r.width, height: r.height }, colors);
2067
+ return;
2068
+ }
2069
+ requestAnimationFrame(tick);
2070
+ };
2071
+ requestAnimationFrame(tick);
2072
+ }
2073
+ function flashLocate(target, colors) {
2006
2074
  if (typeof document === "undefined" || !target) return;
2007
2075
  let el = null;
2008
2076
  if (target.selector) {
@@ -2014,13 +2082,18 @@ function flashLocate(target, color) {
2014
2082
  }
2015
2083
  if (el) {
2016
2084
  el.scrollIntoView({ block: "center", inline: "center" });
2017
- requestAnimationFrame(() => {
2018
- if (!el) return;
2019
- const r = el.getBoundingClientRect();
2020
- paint({ top: r.top, left: r.left, width: r.width, height: r.height });
2021
- });
2085
+ settleThenPaint(el, colors);
2022
2086
  } else if (target.rect) {
2023
- paint(target.rect);
2087
+ let rect = target.rect;
2088
+ const snap = target.scroll;
2089
+ if (snap) {
2090
+ const dx = window.scrollX - snap.x;
2091
+ const dy = window.scrollY - snap.y;
2092
+ if (dx || dy) {
2093
+ rect = { ...rect, left: rect.left - dx, top: rect.top - dy };
2094
+ }
2095
+ }
2096
+ paint(rect, colors);
2024
2097
  }
2025
2098
  }
2026
2099
  function LocationReveal({ target }) {
@@ -2104,7 +2177,7 @@ function LocationReveal({ target }) {
2104
2177
  /* @__PURE__ */ jsxRuntime.jsxs(
2105
2178
  "button",
2106
2179
  {
2107
- onClick: () => flashLocate(target),
2180
+ onClick: () => flashLocate(target, { primary: theme.primary, accent: theme.accent }),
2108
2181
  className: "qa-mt-1 qa-inline-flex qa-items-center qa-gap-1 qa-rounded-md qa-px-2 qa-py-1 qa-font-medium qa-text-white qa-tap",
2109
2182
  style: { background: theme.accent, border: "none", cursor: "pointer" },
2110
2183
  children: [
@@ -2378,8 +2451,37 @@ function NoteList() {
2378
2451
  }
2379
2452
  return /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-2", children: notes.map((n, i) => /* @__PURE__ */ jsxRuntime.jsx(NoteItem, { note: n, index: notes.length - i }, n.id)) });
2380
2453
  }
2381
- function CopyField({ value, ink }) {
2454
+ function EyeIcon({ open, size = 12, className }) {
2455
+ return /* @__PURE__ */ jsxRuntime.jsx(
2456
+ "svg",
2457
+ {
2458
+ xmlns: "http://www.w3.org/2000/svg",
2459
+ viewBox: "0 0 24 24",
2460
+ width: size,
2461
+ height: size,
2462
+ fill: "none",
2463
+ stroke: "currentColor",
2464
+ strokeWidth: 2,
2465
+ strokeLinecap: "round",
2466
+ strokeLinejoin: "round",
2467
+ className,
2468
+ "aria-hidden": "true",
2469
+ children: open ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2470
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" }),
2471
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" })
2472
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2473
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9.88 9.88a3 3 0 1 0 4.24 4.24" }),
2474
+ /* @__PURE__ */ jsxRuntime.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" }),
2475
+ /* @__PURE__ */ jsxRuntime.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" }),
2476
+ /* @__PURE__ */ jsxRuntime.jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" })
2477
+ ] })
2478
+ }
2479
+ );
2480
+ }
2481
+ var MASK = "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022";
2482
+ function CopyField({ value, ink, maskable = false }) {
2382
2483
  const [done, setDone] = React.useState(false);
2484
+ const [revealed, setRevealed] = React.useState(true);
2383
2485
  const copy = async () => {
2384
2486
  if (value === "\u2014") return;
2385
2487
  if (typeof navigator === "undefined" || !navigator.clipboard) return;
@@ -2390,20 +2492,36 @@ function CopyField({ value, ink }) {
2390
2492
  } catch {
2391
2493
  }
2392
2494
  };
2393
- return /* @__PURE__ */ jsxRuntime.jsxs(
2394
- "button",
2395
- {
2396
- onClick: copy,
2397
- disabled: value === "\u2014",
2398
- dir: "ltr",
2399
- 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",
2400
- style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2401
- children: [
2402
- /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: ink }, children: value }),
2403
- value !== "\u2014" && (done ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 12, className: "qa-text-green-600" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Copy", size: 12, className: "qa-opacity-40 qa-group-hover-opacity-80" }))
2404
- ]
2405
- }
2406
- );
2495
+ const hidden = maskable && !revealed && value !== "\u2014";
2496
+ const displayValue = hidden ? MASK : value;
2497
+ return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1", children: [
2498
+ /* @__PURE__ */ jsxRuntime.jsxs(
2499
+ "button",
2500
+ {
2501
+ onClick: copy,
2502
+ disabled: value === "\u2014",
2503
+ dir: "ltr",
2504
+ className: "qa-group qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-px-1.5 qa-py-0.5 qa-font-mono qa-text-xs qa-hover-bg-black-5",
2505
+ style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2506
+ children: [
2507
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { color: ink }, children: displayValue }),
2508
+ value !== "\u2014" && (done ? /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 12, className: "qa-text-green-600" }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Copy", size: 12, className: "qa-opacity-40 qa-group-hover-opacity-80" }))
2509
+ ]
2510
+ }
2511
+ ),
2512
+ maskable && value !== "\u2014" && /* @__PURE__ */ jsxRuntime.jsx(
2513
+ "button",
2514
+ {
2515
+ type: "button",
2516
+ onClick: () => setRevealed((r) => !r),
2517
+ "aria-label": revealed ? "Hide password" : "Show password",
2518
+ title: revealed ? "Hide password" : "Show password",
2519
+ className: "qa-inline-flex qa-items-center qa-rounded-md qa-p-0.5 qa-opacity-40 qa-hover-opacity-80",
2520
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2521
+ children: /* @__PURE__ */ jsxRuntime.jsx(EyeIcon, { open: revealed, size: 12 })
2522
+ }
2523
+ )
2524
+ ] });
2407
2525
  }
2408
2526
  function CredentialsSection() {
2409
2527
  const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, theme } = useQa();
@@ -2421,7 +2539,7 @@ function CredentialsSection() {
2421
2539
  }
2422
2540
  )
2423
2541
  ] }),
2424
- credentials.map((c) => {
2542
+ credentials.map((c, i) => {
2425
2543
  const used = loginsUsed.has(c.role);
2426
2544
  const label = lang === "ar" && c.roleAr ? c.roleAr : c.role;
2427
2545
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -2459,11 +2577,11 @@ function CredentialsSection() {
2459
2577
  c.seeded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-mt-1.5 qa-flex qa-flex-wrap qa-items-center qa-gap-x-3 qa-gap-y-1 qa-ps-6", children: [
2460
2578
  /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.login, ink: theme.ink }),
2461
2579
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
2462
- /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password, ink: theme.ink })
2580
+ /* @__PURE__ */ jsxRuntime.jsx(CopyField, { value: c.password, ink: theme.ink, maskable: true })
2463
2581
  ] })
2464
2582
  ]
2465
2583
  },
2466
- c.role
2584
+ `${c.role}-${i}`
2467
2585
  );
2468
2586
  })
2469
2587
  ] });
@@ -2534,7 +2652,7 @@ function Lane({
2534
2652
  style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
2535
2653
  }
2536
2654
  ),
2537
- steps.map((s) => {
2655
+ steps.map((s, i) => {
2538
2656
  const k = keyOf(id, s.path);
2539
2657
  const on = checked.has(k);
2540
2658
  const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
@@ -2598,7 +2716,7 @@ function Lane({
2598
2716
  ] })
2599
2717
  ]
2600
2718
  }
2601
- ) }, s.path);
2719
+ ) }, `${k}-${i}`);
2602
2720
  })
2603
2721
  ] })
2604
2722
  ]
@@ -2742,6 +2860,8 @@ function QaPanel() {
2742
2860
  }
2743
2861
  if (phase === "hidden") {
2744
2862
  setShowIn(false);
2863
+ setNaming(false);
2864
+ setConfirmClear(false);
2745
2865
  }
2746
2866
  if (phase === "visible") {
2747
2867
  setShowIn(true);
@@ -2938,7 +3058,8 @@ function QaPanel() {
2938
3058
  activeTab,
2939
3059
  setActiveTab,
2940
3060
  t,
2941
- theme
3061
+ theme,
3062
+ lang
2942
3063
  }
2943
3064
  ),
2944
3065
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px", style: { background: `${theme.primary}14` } }),
@@ -3090,7 +3211,8 @@ function TabsBar({
3090
3211
  activeTab,
3091
3212
  setActiveTab,
3092
3213
  t,
3093
- theme
3214
+ theme,
3215
+ lang
3094
3216
  }) {
3095
3217
  const tabRefs = React.useRef([]);
3096
3218
  const barRef = React.useRef(null);
@@ -3102,7 +3224,7 @@ function TabsBar({
3102
3224
  if (!btn || !bar) return;
3103
3225
  bar.style.left = `${btn.offsetLeft + 8}px`;
3104
3226
  bar.style.width = `${Math.max(0, btn.offsetWidth - 16)}px`;
3105
- }, [activeTab]);
3227
+ }, [activeTab, lang]);
3106
3228
  React.useLayoutEffect(() => {
3107
3229
  reposition();
3108
3230
  }, [reposition]);
@@ -3151,6 +3273,13 @@ function TabsBar({
3151
3273
  }
3152
3274
 
3153
3275
  // src/lib/capture.ts
3276
+ var HTML2CANVAS_TIMEOUT_MS = 1e4;
3277
+ function withTimeout(promise, ms) {
3278
+ return Promise.race([
3279
+ promise,
3280
+ new Promise((resolve) => setTimeout(() => resolve(null), ms))
3281
+ ]);
3282
+ }
3154
3283
  function toBlob(canvas) {
3155
3284
  return new Promise((resolve) => {
3156
3285
  if (canvas.toBlob) {
@@ -3172,24 +3301,28 @@ async function captureRegion(rect, scroll) {
3172
3301
  try {
3173
3302
  const { default: html2canvas } = await import('html2canvas');
3174
3303
  const scale = Math.min(window.devicePixelRatio || 1, 2);
3175
- const canvas = await html2canvas(document.body, {
3176
- x: sx + rect.left,
3177
- y: sy + rect.top,
3178
- width: rect.width,
3179
- height: rect.height,
3180
- scale,
3181
- useCORS: true,
3182
- allowTaint: true,
3183
- backgroundColor: null,
3184
- logging: false,
3185
- scrollX: sx,
3186
- scrollY: sy,
3187
- // Viewport-only clone (not the full document) — see iOS canvas-cap
3188
- // rationale above. Keeps the offscreen render surface ~viewport*scale.
3189
- windowWidth: window.innerWidth,
3190
- windowHeight: window.innerHeight,
3191
- ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay")
3192
- });
3304
+ const canvas = await withTimeout(
3305
+ html2canvas(document.body, {
3306
+ x: sx + rect.left,
3307
+ y: sy + rect.top,
3308
+ width: rect.width,
3309
+ height: rect.height,
3310
+ scale,
3311
+ useCORS: true,
3312
+ allowTaint: true,
3313
+ backgroundColor: null,
3314
+ logging: false,
3315
+ scrollX: sx,
3316
+ scrollY: sy,
3317
+ // Viewport-only clone (not the full document) see iOS canvas-cap
3318
+ // rationale above. Keeps the offscreen render surface ~viewport*scale.
3319
+ windowWidth: window.innerWidth,
3320
+ windowHeight: window.innerHeight,
3321
+ ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay")
3322
+ }),
3323
+ HTML2CANVAS_TIMEOUT_MS
3324
+ );
3325
+ if (!canvas) return null;
3193
3326
  return await toBlob(canvas);
3194
3327
  } catch (err) {
3195
3328
  console.warn("[QA] region capture failed:", err);
@@ -3202,7 +3335,8 @@ function isCleanId(id) {
3202
3335
  return !!id && /^[a-zA-Z][\w-]*$/.test(id) && id.length <= 40;
3203
3336
  }
3204
3337
  function esc(value) {
3205
- return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
3338
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
3339
+ return value.replace(/[\\"]/g, "\\$&");
3206
3340
  }
3207
3341
  function nthOfTypePath(el, maxDepth = 6) {
3208
3342
  if (typeof document === "undefined" || !document.body) return "";
@@ -3230,46 +3364,68 @@ function nthOfTypePath(el, maxDepth = 6) {
3230
3364
  }
3231
3365
  return parts.join(" > ");
3232
3366
  }
3367
+ function isUniqueSelector(selector) {
3368
+ if (!selector) return false;
3369
+ try {
3370
+ return document.querySelectorAll(selector).length === 1;
3371
+ } catch {
3372
+ return false;
3373
+ }
3374
+ }
3233
3375
  function getStableSelector(el) {
3234
3376
  if (typeof document === "undefined") return "";
3235
3377
  if (!el || el.nodeType !== 1) return "";
3236
3378
  const tag = el.tagName.toLowerCase();
3237
3379
  const htmlEl = el;
3238
- if (isCleanId(htmlEl.id)) return `#${esc(htmlEl.id)}`;
3380
+ if (isCleanId(htmlEl.id)) {
3381
+ const candidate = `#${esc(htmlEl.id)}`;
3382
+ if (isUniqueSelector(candidate)) return candidate;
3383
+ }
3239
3384
  for (const attr of ["data-testid", "data-test", "data-cy", "data-id", "data-key"]) {
3240
3385
  const val = el.getAttribute(attr);
3241
- if (val) return `[${attr}="${esc(val)}"]`;
3386
+ if (val) {
3387
+ const candidate = `[${attr}="${esc(val)}"]`;
3388
+ if (isUniqueSelector(candidate)) return candidate;
3389
+ }
3242
3390
  }
3243
3391
  if (["button", "a", "input", "select", "textarea"].includes(tag)) {
3244
3392
  const label = el.getAttribute("aria-label");
3245
- if (label) return `${tag}[aria-label="${esc(label)}"]`;
3393
+ if (label) {
3394
+ const candidate = `${tag}[aria-label="${esc(label)}"]`;
3395
+ if (isUniqueSelector(candidate)) return candidate;
3396
+ }
3246
3397
  }
3247
3398
  const name = el.getAttribute("name");
3248
3399
  if (name && ["input", "select", "textarea"].includes(tag)) {
3249
- return `${tag}[name="${esc(name)}"]`;
3400
+ const candidate = `${tag}[name="${esc(name)}"]`;
3401
+ if (isUniqueSelector(candidate)) return candidate;
3250
3402
  }
3251
3403
  return nthOfTypePath(el);
3252
3404
  }
3253
3405
 
3254
3406
  // src/lib/scrollLock.ts
3255
- var locked = false;
3407
+ var lockCount = 0;
3256
3408
  var prevHtmlOverflow = "";
3257
3409
  var prevBodyOverflow = "";
3258
3410
  function lockPageScroll() {
3259
- if (typeof document === "undefined" || locked) return;
3260
- const html = document.documentElement;
3261
- const body = document.body;
3262
- prevHtmlOverflow = html.style.overflow;
3263
- prevBodyOverflow = body ? body.style.overflow : "";
3264
- html.style.overflow = "hidden";
3265
- if (body) body.style.overflow = "hidden";
3266
- locked = true;
3411
+ if (typeof document === "undefined") return;
3412
+ if (lockCount === 0) {
3413
+ const html = document.documentElement;
3414
+ const body = document.body;
3415
+ prevHtmlOverflow = html.style.overflow;
3416
+ prevBodyOverflow = body ? body.style.overflow : "";
3417
+ html.style.overflow = "hidden";
3418
+ if (body) body.style.overflow = "hidden";
3419
+ }
3420
+ lockCount++;
3267
3421
  }
3268
3422
  function unlockPageScroll() {
3269
- if (typeof document === "undefined" || !locked) return;
3270
- document.documentElement.style.overflow = prevHtmlOverflow;
3271
- if (document.body) document.body.style.overflow = prevBodyOverflow;
3272
- locked = false;
3423
+ if (typeof document === "undefined" || lockCount === 0) return;
3424
+ lockCount--;
3425
+ if (lockCount === 0) {
3426
+ document.documentElement.style.overflow = prevHtmlOverflow;
3427
+ if (document.body) document.body.style.overflow = prevBodyOverflow;
3428
+ }
3273
3429
  }
3274
3430
  var DRAG_THRESHOLD2 = 6;
3275
3431
  var TOUCH_DRAG_THRESHOLD = 12;
@@ -3288,6 +3444,7 @@ function CaptureMode() {
3288
3444
  const { addNote, endCapture, t, dir, theme } = useQa();
3289
3445
  const coarse = useCoarsePointer();
3290
3446
  const layerRef = React.useRef(null);
3447
+ const overlayRootRef = React.useRef(null);
3291
3448
  const [phase, setPhase] = React.useState("selecting");
3292
3449
  const [hover, setHover] = React.useState(null);
3293
3450
  const [drag, setDrag] = React.useState(null);
@@ -3304,6 +3461,10 @@ function CaptureMode() {
3304
3461
  const pointerKind = React.useRef("mouse");
3305
3462
  const scrollSnap = React.useRef({ x: 0, y: 0 });
3306
3463
  const handleDragRef = React.useRef(null);
3464
+ const mountedRef = React.useRef(true);
3465
+ React.useEffect(() => () => {
3466
+ mountedRef.current = false;
3467
+ }, []);
3307
3468
  const [cardIn, setCardIn] = React.useState(false);
3308
3469
  const elementUnder = React.useCallback((x, y) => {
3309
3470
  const layer = layerRef.current;
@@ -3326,6 +3487,10 @@ function CaptureMode() {
3326
3487
  lockPageScroll();
3327
3488
  try {
3328
3489
  const blob = await captureRegion(sel.rect, scrollSnap.current);
3490
+ if (!mountedRef.current) {
3491
+ if (blob) URL.revokeObjectURL(URL.createObjectURL(blob));
3492
+ return;
3493
+ }
3329
3494
  setShot(blob);
3330
3495
  setShotUrl((old) => {
3331
3496
  if (old) URL.revokeObjectURL(old);
@@ -3333,7 +3498,7 @@ function CaptureMode() {
3333
3498
  });
3334
3499
  } finally {
3335
3500
  unlockPageScroll();
3336
- setCapturing(false);
3501
+ if (mountedRef.current) setCapturing(false);
3337
3502
  }
3338
3503
  }, []);
3339
3504
  React.useEffect(() => {
@@ -3502,6 +3667,38 @@ function CaptureMode() {
3502
3667
  document.addEventListener("keydown", onKey, true);
3503
3668
  return () => document.removeEventListener("keydown", onKey, true);
3504
3669
  }, [endCapture]);
3670
+ React.useEffect(() => {
3671
+ const FOCUSABLE_SELECTOR = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
3672
+ const onKeyDown = (e) => {
3673
+ if (e.key !== "Tab") return;
3674
+ const root = overlayRootRef.current;
3675
+ if (!root) return;
3676
+ const focusable = Array.from(
3677
+ root.querySelectorAll(FOCUSABLE_SELECTOR)
3678
+ ).filter((el) => !el.hasAttribute("disabled") && el.offsetParent !== null);
3679
+ if (focusable.length === 0) {
3680
+ e.preventDefault();
3681
+ return;
3682
+ }
3683
+ const first = focusable[0];
3684
+ const last = focusable[focusable.length - 1];
3685
+ const active = document.activeElement;
3686
+ const activeInside = !!active && root.contains(active);
3687
+ if (e.shiftKey) {
3688
+ if (!activeInside || active === first) {
3689
+ e.preventDefault();
3690
+ last.focus();
3691
+ }
3692
+ } else {
3693
+ if (!activeInside || active === last) {
3694
+ e.preventDefault();
3695
+ first.focus();
3696
+ }
3697
+ }
3698
+ };
3699
+ document.addEventListener("keydown", onKeyDown, true);
3700
+ return () => document.removeEventListener("keydown", onKeyDown, true);
3701
+ }, []);
3505
3702
  React.useEffect(() => {
3506
3703
  if (phase === "annotating" && taRef.current) taRef.current.focus();
3507
3704
  }, [phase]);
@@ -3523,7 +3720,8 @@ function CaptureMode() {
3523
3720
  left: Math.round(selection.rect.left),
3524
3721
  width: Math.round(selection.rect.width),
3525
3722
  height: Math.round(selection.rect.height)
3526
- }
3723
+ },
3724
+ scroll: { ...scrollSnap.current }
3527
3725
  };
3528
3726
  await addNote({ description, screenshot: shot ?? void 0, target });
3529
3727
  endCapture();
@@ -3547,7 +3745,7 @@ function CaptureMode() {
3547
3745
  const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
3548
3746
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
3549
3747
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
3550
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", children: [
3748
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", ref: overlayRootRef, children: [
3551
3749
  /* @__PURE__ */ jsxRuntime.jsx(
3552
3750
  "div",
3553
3751
  {
@@ -3559,7 +3757,7 @@ function CaptureMode() {
3559
3757
  className: "qa-fixed qa-inset-0 qa-z-10090",
3560
3758
  style: {
3561
3759
  cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
3562
- touchAction: coarse ? regionMode ? "none" : "pan-x pan-y" : "auto",
3760
+ touchAction: coarse ? "none" : "auto",
3563
3761
  background: "rgba(58,42,46,0.18)"
3564
3762
  }
3565
3763
  }
@@ -4017,6 +4215,7 @@ function Qapture({ config }) {
4017
4215
  }
4018
4216
 
4019
4217
  exports.Qapture = Qapture;
4218
+ exports.deleteQaDatabase = deleteQaDatabase;
4020
4219
  exports.initQaStudio = initQaStudio;
4021
- //# sourceMappingURL=chunk-PBXQDL7C.cjs.map
4022
- //# sourceMappingURL=chunk-PBXQDL7C.cjs.map
4220
+ //# sourceMappingURL=chunk-DPJW626S.cjs.map
4221
+ //# sourceMappingURL=chunk-DPJW626S.cjs.map