@m2farhood-2/qapture 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3823 @@
1
+ import React, { createContext, useEffect, Component, useState, useCallback, useReducer, useContext, useRef, useLayoutEffect } from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+
5
+ // src/index.ts
6
+
7
+ // src/config/schema.ts
8
+ var DEFAULT_THEME = {
9
+ primary: "#4f46e5",
10
+ // indigo-600
11
+ primaryDark: "#3730a3",
12
+ // indigo-800
13
+ accent: "#7c3aed",
14
+ // violet-600
15
+ accentDark: "#6d28d9",
16
+ // violet-700
17
+ sage: "#6b7280",
18
+ // gray-500
19
+ cream: "#f8fafc",
20
+ // slate-50
21
+ mauve: "#a78bfa",
22
+ // violet-400
23
+ surface: "#ffffff",
24
+ ink: "#1f2937"
25
+ // gray-800
26
+ };
27
+ var DEFAULTS = {
28
+ namespace: "qapture",
29
+ brandLabel: "Qapture",
30
+ loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
31
+ rtl: false,
32
+ visible: void 0,
33
+ alwaysVisible: false,
34
+ hotkey: "shift+alt+q"
35
+ };
36
+ var VALID_RISKS = /* @__PURE__ */ new Set(["red", "amber", "green"]);
37
+ function isNonEmptyString(v) {
38
+ return typeof v === "string" && v.trim().length > 0;
39
+ }
40
+ function isValidBilingual(v) {
41
+ if (typeof v === "string") return true;
42
+ if (v !== null && typeof v === "object") {
43
+ const o = v;
44
+ return typeof o["en"] === "string";
45
+ }
46
+ return false;
47
+ }
48
+ function coerceTheme(input) {
49
+ if (!input || typeof input !== "object") return { ...DEFAULT_THEME };
50
+ const out = { ...DEFAULT_THEME };
51
+ const keys = [
52
+ "primary",
53
+ "primaryDark",
54
+ "accent",
55
+ "accentDark",
56
+ "sage",
57
+ "cream",
58
+ "mauve",
59
+ "surface",
60
+ "ink"
61
+ ];
62
+ for (const k of keys) {
63
+ const v = input[k];
64
+ if (typeof v === "string" && v.trim().length > 0) {
65
+ out[k] = v.trim();
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ function coerceCredentials(raw, warnings) {
71
+ if (!Array.isArray(raw)) return [];
72
+ const out = [];
73
+ for (let i = 0; i < raw.length; i++) {
74
+ const c = raw[i];
75
+ if (!c || typeof c !== "object") {
76
+ warnings.push(`credentials[${i}]: not an object \u2014 skipped`);
77
+ continue;
78
+ }
79
+ if (!isNonEmptyString(c["role"])) {
80
+ warnings.push(`credentials[${i}]: missing or empty "role" \u2014 skipped`);
81
+ continue;
82
+ }
83
+ if (!isNonEmptyString(c["login"])) {
84
+ warnings.push(`credentials[${i}] (role="${String(c["role"])}"): missing or empty "login" \u2014 skipped`);
85
+ continue;
86
+ }
87
+ const cred = {
88
+ role: c["role"].trim(),
89
+ login: c["login"].trim(),
90
+ password: isNonEmptyString(c["password"]) ? c["password"].trim() : ""
91
+ };
92
+ if (isNonEmptyString(c["roleAr"])) cred.roleAr = c["roleAr"].trim();
93
+ if (typeof c["seeded"] === "boolean") cred.seeded = c["seeded"];
94
+ if (c["hint"] !== null && c["hint"] !== void 0 && typeof c["hint"] === "object") {
95
+ const h = c["hint"];
96
+ if (typeof h["en"] === "string") {
97
+ cred.hint = { en: h["en"] };
98
+ if (typeof h["ar"] === "string") cred.hint.ar = h["ar"];
99
+ }
100
+ }
101
+ out.push(cred);
102
+ }
103
+ return out;
104
+ }
105
+ function coerceJourney(raw, warnings) {
106
+ if (!Array.isArray(raw)) return [];
107
+ const out = [];
108
+ for (let i = 0; i < raw.length; i++) {
109
+ const lane = raw[i];
110
+ if (!lane || typeof lane !== "object") {
111
+ warnings.push(`journey[${i}]: not an object \u2014 skipped`);
112
+ continue;
113
+ }
114
+ if (!isNonEmptyString(lane["id"])) {
115
+ warnings.push(`journey[${i}]: missing or empty "id" \u2014 skipped`);
116
+ continue;
117
+ }
118
+ if (!isValidBilingual(lane["role"])) {
119
+ warnings.push(`journey[${i}] (id="${String(lane["id"])}"): invalid "role" \u2014 skipped`);
120
+ continue;
121
+ }
122
+ if (!Array.isArray(lane["steps"])) {
123
+ warnings.push(`journey[${i}] (id="${String(lane["id"])}"): "steps" is not an array \u2014 lane skipped`);
124
+ continue;
125
+ }
126
+ const steps = [];
127
+ const rawSteps = lane["steps"];
128
+ for (let j = 0; j < rawSteps.length; j++) {
129
+ const s = rawSteps[j];
130
+ if (!s || typeof s !== "object") {
131
+ warnings.push(`journey[${i}].steps[${j}]: not an object \u2014 skipped`);
132
+ continue;
133
+ }
134
+ if (!isNonEmptyString(s["path"])) {
135
+ warnings.push(`journey[${i}].steps[${j}]: missing or empty "path" \u2014 skipped`);
136
+ continue;
137
+ }
138
+ if (!isValidBilingual(s["what"])) {
139
+ warnings.push(`journey[${i}].steps[${j}] (path="${String(s["path"])}"): invalid "what" \u2014 skipped`);
140
+ continue;
141
+ }
142
+ const step = {
143
+ path: s["path"].trim(),
144
+ what: s["what"]
145
+ };
146
+ if (s["risk"] !== void 0) {
147
+ if (VALID_RISKS.has(s["risk"])) {
148
+ step.risk = s["risk"];
149
+ } else {
150
+ warnings.push(`journey[${i}].steps[${j}]: invalid risk "${String(s["risk"])}" \u2014 ignored`);
151
+ }
152
+ }
153
+ if (isNonEmptyString(s["riskWhy"])) step.riskWhy = s["riskWhy"];
154
+ steps.push(step);
155
+ }
156
+ const resolved = {
157
+ id: lane["id"].trim(),
158
+ role: lane["role"],
159
+ steps
160
+ };
161
+ if (isNonEmptyString(lane["color"])) resolved.color = lane["color"].trim();
162
+ out.push(resolved);
163
+ }
164
+ return out;
165
+ }
166
+ function coercePreamble(raw) {
167
+ if (raw === null || raw === void 0) return null;
168
+ if (typeof raw !== "object" || Array.isArray(raw)) return null;
169
+ return raw;
170
+ }
171
+ function validateConfig(input) {
172
+ const warnings = [];
173
+ if (input === void 0 || input === null) {
174
+ return {
175
+ config: {
176
+ namespace: DEFAULTS.namespace,
177
+ theme: { ...DEFAULT_THEME },
178
+ brand: { label: DEFAULTS.brandLabel },
179
+ loginField: { ...DEFAULTS.loginField },
180
+ credentials: [],
181
+ journey: [],
182
+ preamble: null,
183
+ rtl: DEFAULTS.rtl,
184
+ visible: DEFAULTS.visible,
185
+ alwaysVisible: DEFAULTS.alwaysVisible,
186
+ hotkey: DEFAULTS.hotkey
187
+ },
188
+ warnings
189
+ };
190
+ }
191
+ if (typeof input !== "object" || Array.isArray(input)) {
192
+ warnings.push("config: expected an object \u2014 using defaults");
193
+ return {
194
+ config: {
195
+ namespace: DEFAULTS.namespace,
196
+ theme: { ...DEFAULT_THEME },
197
+ brand: { label: DEFAULTS.brandLabel },
198
+ loginField: { ...DEFAULTS.loginField },
199
+ credentials: [],
200
+ journey: [],
201
+ preamble: null,
202
+ rtl: DEFAULTS.rtl,
203
+ visible: DEFAULTS.visible,
204
+ alwaysVisible: DEFAULTS.alwaysVisible,
205
+ hotkey: DEFAULTS.hotkey
206
+ },
207
+ warnings
208
+ };
209
+ }
210
+ const raw = input;
211
+ const namespace = isNonEmptyString(raw["namespace"]) ? raw["namespace"].trim() : DEFAULTS.namespace;
212
+ const theme = coerceTheme(raw["theme"]);
213
+ let brandLabel = DEFAULTS.brandLabel;
214
+ if (raw["brand"] !== void 0 && raw["brand"] !== null && typeof raw["brand"] === "object") {
215
+ const b = raw["brand"];
216
+ if (isNonEmptyString(b["label"])) brandLabel = b["label"].trim();
217
+ }
218
+ let loginField = { ...DEFAULTS.loginField };
219
+ if (raw["loginField"] !== void 0 && typeof raw["loginField"] === "object" && raw["loginField"] !== null) {
220
+ const lf = raw["loginField"];
221
+ if (typeof lf["en"] === "string") {
222
+ loginField = { en: lf["en"] };
223
+ if (typeof lf["ar"] === "string") loginField.ar = lf["ar"];
224
+ } else {
225
+ warnings.push('loginField: missing "en" key \u2014 using default');
226
+ }
227
+ }
228
+ const credentials = raw["credentials"] !== void 0 ? coerceCredentials(raw["credentials"], warnings) : [];
229
+ const journey = raw["journey"] !== void 0 ? coerceJourney(raw["journey"], warnings) : [];
230
+ const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
231
+ const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
232
+ const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
233
+ const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
234
+ let visible = DEFAULTS.visible;
235
+ if (raw["visible"] !== void 0) {
236
+ if (typeof raw["visible"] === "boolean") {
237
+ visible = raw["visible"];
238
+ } else {
239
+ warnings.push("visible: expected boolean \u2014 using default (dev-only)");
240
+ }
241
+ }
242
+ return {
243
+ config: {
244
+ namespace,
245
+ theme,
246
+ brand: { label: brandLabel },
247
+ loginField,
248
+ credentials,
249
+ journey,
250
+ preamble,
251
+ rtl,
252
+ visible,
253
+ alwaysVisible,
254
+ hotkey
255
+ },
256
+ warnings
257
+ };
258
+ }
259
+
260
+ // src/lib/styles.ts
261
+ var QA_CSS = `
262
+ /* \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 */
263
+ *, *::before, *::after { box-sizing: border-box; }
264
+
265
+ /* \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 */
266
+ .qa-fixed { position: fixed; }
267
+ .qa-absolute { position: absolute; }
268
+ .qa-relative { position: relative; }
269
+ .qa-sticky { position: sticky; }
270
+ .qa-inset-0 { inset: 0; }
271
+ .qa-top-0 { top: 0; }
272
+ .qa-top-4 { top: 1rem; }
273
+ .qa-top-auto { top: auto; }
274
+ .qa-bottom-0 { bottom: 0; }
275
+ .qa-bottom-4 { bottom: 1rem; }
276
+ .qa-left-0 { left: 0; }
277
+ .qa-left-half { left: 50%; }
278
+ .qa-right-0 { right: 0; }
279
+
280
+ /* z-index */
281
+ .qa-z-1 { z-index: 1; }
282
+ .qa-z-50 { z-index: 50; }
283
+ .qa-z-100 { z-index: 100; }
284
+ .qa-z-10090 { z-index: 10090; }
285
+ .qa-z-10092 { z-index: 10092; }
286
+ /* region-handle layering */
287
+ .qa-z-10093 { z-index: 10093; }
288
+ .qa-z-10094 { z-index: 10094; }
289
+ .qa-z-10095 { z-index: 10095; }
290
+ .qa-z-10096 { z-index: 10096; }
291
+
292
+ /* \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 */
293
+ .qa-flex { display: flex; }
294
+ .qa-inline-flex { display: inline-flex; }
295
+ .qa-block { display: block; }
296
+ .qa-inline-block { display: inline-block; }
297
+ .qa-hidden { display: none; }
298
+ .qa-flex-1 { flex: 1 1 0%; }
299
+ .qa-flex-col { flex-direction: column; }
300
+ .qa-flex-wrap { flex-wrap: wrap; }
301
+ .qa-items-center { align-items: center; }
302
+ .qa-items-start { align-items: flex-start; }
303
+ .qa-items-end { align-items: flex-end; }
304
+ .qa-justify-center { justify-content: center; }
305
+ .qa-justify-between { justify-content: space-between; }
306
+ .qa-justify-start { justify-content: flex-start; }
307
+ .qa-justify-end { justify-content: flex-end; }
308
+ .qa-shrink-0 { flex-shrink: 0; }
309
+ .qa-grow { flex-grow: 1; }
310
+ .qa-ms-auto { margin-inline-start: auto; }
311
+ .qa-me-auto { margin-inline-end: auto; }
312
+
313
+ /* gap */
314
+ .qa-gap-1 { gap: 0.25rem; }
315
+ .qa-gap-1\\.5 { gap: 0.375rem; }
316
+ .qa-gap-2 { gap: 0.5rem; }
317
+ .qa-gap-2\\.5 { gap: 0.625rem; }
318
+ .qa-gap-3 { gap: 0.75rem; }
319
+ .qa-gap-x-3 { column-gap: 0.75rem; }
320
+ .qa-gap-y-1 { row-gap: 0.25rem; }
321
+
322
+ /* space-y (margin-top on siblings) */
323
+ .qa-space-y-1 > * + * { margin-top: 0.25rem; }
324
+ .qa-space-y-2 > * + * { margin-top: 0.5rem; }
325
+ .qa-space-y-2\\.5 > * + * { margin-top: 0.625rem; }
326
+ .qa-space-y-3 > * + * { margin-top: 0.75rem; }
327
+
328
+ /* \u2500\u2500 Size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
329
+ .qa-w-full { width: 100%; }
330
+ .qa-h-full { height: 100%; }
331
+ .qa-w-px { width: 1px; }
332
+ .qa-h-px { height: 1px; }
333
+ .qa-w-2 { width: 0.5rem; }
334
+ .qa-h-2 { height: 0.5rem; }
335
+ .qa-w-2\\.5 { width: 0.625rem; }
336
+ .qa-h-2\\.5 { height: 0.625rem; }
337
+ .qa-w-3 { width: 0.75rem; }
338
+ .qa-h-3 { height: 0.75rem; }
339
+ .qa-w-3\\.5 { width: 0.875rem; }
340
+ .qa-h-3\\.5 { height: 0.875rem; }
341
+ .qa-w-4 { width: 1rem; }
342
+ .qa-h-4 { height: 1rem; }
343
+ .qa-w-5 { width: 1.25rem; }
344
+ .qa-h-5 { height: 1.25rem; }
345
+ .qa-w-6 { width: 1.5rem; }
346
+ .qa-h-6 { height: 1.5rem; }
347
+ .qa-h-1 { height: 0.25rem; }
348
+ .qa-h-1\\.5 { height: 0.375rem; }
349
+ .qa-min-w-0 { min-width: 0; }
350
+ .qa-min-h-0 { min-height: 0; }
351
+ .qa-max-w-xs { max-width: 16rem; } /* 256px */
352
+ .qa-max-w-sm { max-width: 20rem; } /* 320px */
353
+ .qa-max-w-md { max-width: 24rem; } /* 384px */
354
+ .qa-max-h-28 { max-height: 7rem; } /* 112px */
355
+ .qa-max-h-32 { max-height: 8rem; } /* 128px */
356
+ .qa-min-h-16 { min-height: 4rem; } /* 64px */
357
+ .qa-overflow-hidden { overflow: hidden; }
358
+ .qa-overflow-y-auto { overflow-y: auto; }
359
+ .qa-overflow-x-hidden { overflow-x: hidden; }
360
+ .qa-resize-y { resize: vertical; }
361
+
362
+ /* \u2500\u2500 Spacing \u2014 padding \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
363
+ .qa-p-0 { padding: 0; }
364
+ .qa-p-1 { padding: 0.25rem; }
365
+ .qa-p-2 { padding: 0.5rem; }
366
+ .qa-p-2\\.5 { padding: 0.625rem; }
367
+ .qa-p-3 { padding: 0.75rem; }
368
+ .qa-p-4 { padding: 1rem; }
369
+ .qa-px-1 { padding-inline: 0.25rem; }
370
+ .qa-px-1\\.5 { padding-inline: 0.375rem; }
371
+ .qa-px-2 { padding-inline: 0.5rem; }
372
+ .qa-px-3 { padding-inline: 0.75rem; }
373
+ .qa-px-4 { padding-inline: 1rem; }
374
+ .qa-py-0\\.5 { padding-block: 0.125rem; }
375
+ .qa-py-1 { padding-block: 0.25rem; }
376
+ .qa-py-1\\.5 { padding-block: 0.375rem; }
377
+ .qa-py-2 { padding-block: 0.5rem; }
378
+ .qa-py-4 { padding-block: 1rem; }
379
+ .qa-py-8 { padding-block: 2rem; }
380
+ .qa-ps-6 { padding-inline-start: 1.5rem; }
381
+ .qa-pe-2 { padding-inline-end: 0.5rem; }
382
+
383
+ /* \u2500\u2500 Spacing \u2014 margin \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
384
+ .qa-m-0 { margin: 0; }
385
+ .qa-mb-1 { margin-bottom: 0.25rem; }
386
+ .qa-mb-2 { margin-bottom: 0.5rem; }
387
+ .qa-mb-3 { margin-bottom: 0.75rem; }
388
+ .qa-mt-1 { margin-top: 0.25rem; }
389
+ .qa-mt-1\\.5 { margin-top: 0.375rem; }
390
+ .qa-mt-2 { margin-top: 0.5rem; }
391
+ .qa-ms-1 { margin-inline-start: 0.25rem; }
392
+ .qa-ms-1\\.5 { margin-inline-start: 0.375rem; }
393
+ .qa-ms-auto { margin-inline-start: auto; }
394
+ .qa-me-1 { margin-inline-end: 0.25rem; }
395
+
396
+ /* \u2500\u2500 Border \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
397
+ .qa-border { border-width: 1px; border-style: solid; }
398
+ .qa-border-2 { border-width: 2px; border-style: solid; }
399
+ .qa-border-0 { border: none; }
400
+ .qa-border-dashed { border-style: dashed; }
401
+ .qa-border-t { border-top-width: 1px; border-top-style: solid; }
402
+ .qa-border-b { border-bottom-width: 1px; border-bottom-style: solid; }
403
+ .qa-border-white { border-color: #ffffff; }
404
+ .qa-border-white-40 { border-color: rgba(255,255,255,0.40); }
405
+
406
+ /* \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 */
407
+ .qa-rounded { border-radius: 0.25rem; }
408
+ .qa-rounded-md { border-radius: 0.375rem; }
409
+ .qa-rounded-lg { border-radius: 0.5rem; }
410
+ .qa-rounded-xl { border-radius: 0.75rem; }
411
+ .qa-rounded-full { border-radius: 9999px; }
412
+
413
+ /* \u2500\u2500 Shadows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
414
+ .qa-shadow-sm { box-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.1); }
415
+ .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); }
416
+ .qa-shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0,0,0,0.25); }
417
+
418
+ /* \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 */
419
+ .qa-text-10 { font-size: 10px; }
420
+ .qa-text-11 { font-size: 11px; }
421
+ .qa-text-xs { font-size: 0.75rem; line-height: 1rem; }
422
+ .qa-text-sm { font-size: 0.875rem; line-height: 1.25rem; }
423
+ .qa-text-base { font-size: 1rem; line-height: 1.5rem; }
424
+ .qa-font-normal { font-weight: 400; }
425
+ .qa-font-medium { font-weight: 500; }
426
+ .qa-font-semibold { font-weight: 600; }
427
+ .qa-font-bold { font-weight: 700; }
428
+ .qa-font-mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; }
429
+ .qa-leading-relaxed { line-height: 1.625; }
430
+ .qa-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
431
+ .qa-whitespace-pre-wrap { white-space: pre-wrap; }
432
+ .qa-break-words { overflow-wrap: break-word; word-break: break-word; }
433
+ .qa-text-start { text-align: start; }
434
+ .qa-text-center { text-align: center; }
435
+ .qa-text-end { text-align: end; }
436
+ .qa-select-all { user-select: all; }
437
+
438
+ /* \u2500\u2500 Touch density tokens \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
439
+ :host { --qa-tap: 0px; }
440
+ @media (pointer: coarse) {
441
+ :host { --qa-tap: 44px; }
442
+ .qa-text-10 { font-size: 11px; }
443
+ .qa-text-11 { font-size: 12px; }
444
+ }
445
+ .qa-tap { min-height: var(--qa-tap); }
446
+ .qa-tap-icon { min-width: var(--qa-tap); min-height: var(--qa-tap); display: inline-flex; align-items: center; justify-content: center; }
447
+
448
+ /* \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 */
449
+ .qa-text-white { color: #ffffff; }
450
+ .qa-text-current { color: currentColor; }
451
+ .qa-text-slate-300 { color: #cbd5e1; }
452
+ .qa-text-slate-400 { color: #94a3b8; }
453
+ .qa-text-slate-500 { color: #64748b; }
454
+ .qa-text-green-600 { color: #16a34a; }
455
+ .qa-text-red-500 { color: #ef4444; }
456
+ .qa-text-red-600 { color: #dc2626; }
457
+
458
+ /* \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 */
459
+ .qa-bg-white { background-color: #ffffff; }
460
+ .qa-bg-white-25 { background-color: rgba(255,255,255,0.25); }
461
+ .qa-bg-transparent { background-color: transparent; }
462
+ .qa-bg-black-3 { background-color: rgba(0,0,0,0.03); }
463
+ .qa-bg-black-5 { background-color: rgba(0,0,0,0.05); }
464
+
465
+ /* \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 */
466
+ .qa-opacity-0 { opacity: 0; }
467
+ .qa-opacity-30 { opacity: 0.30; }
468
+ .qa-opacity-40 { opacity: 0.40; }
469
+ .qa-opacity-50 { opacity: 0.50; }
470
+ .qa-opacity-55 { opacity: 0.55; }
471
+ .qa-opacity-80 { opacity: 0.80; }
472
+ .qa-opacity-100 { opacity: 1; }
473
+
474
+ /* \u2500\u2500 Interactions / State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
475
+ .qa-cursor-crosshair { cursor: crosshair; }
476
+ .qa-cursor-default { cursor: default; }
477
+ .qa-cursor-pointer { cursor: pointer; }
478
+ .qa-pointer-events-none { pointer-events: none; }
479
+ .qa-touch-none { touch-action: none; }
480
+ .qa-touch-pan { touch-action: pan-x pan-y; }
481
+
482
+ .qa-focus-ring:focus {
483
+ outline: 2px solid var(--qa-primary, #4f46e5);
484
+ outline-offset: 2px;
485
+ }
486
+
487
+ button:disabled,
488
+ input:disabled,
489
+ .qa-disabled {
490
+ opacity: 0.40;
491
+ pointer-events: none;
492
+ }
493
+
494
+ /* Hover helpers */
495
+ .qa-hover-bg-black-3:hover { background-color: rgba(0,0,0,0.03); }
496
+ .qa-hover-bg-black-5:hover { background-color: rgba(0,0,0,0.05); }
497
+ .qa-hover-bg-white-15:hover { background-color: rgba(255,255,255,0.15); }
498
+ .qa-hover-opacity-80:hover { opacity: 0.80; }
499
+ .qa-hover-opacity-100:hover { opacity: 1; }
500
+ .qa-hover-text-red:hover { color: #ef4444; }
501
+ .qa-hover-text-slate-600:hover { color: #475569; }
502
+
503
+ /* Group-hover (child uses .qa-group-hover-opacity-80 inside a .qa-group parent) */
504
+ .qa-group .qa-group-hover-opacity-80 { opacity: 0.40; }
505
+ .qa-group:hover .qa-group-hover-opacity-80 { opacity: 0.80; }
506
+
507
+ /* last-child margin reset */
508
+ .qa-last-mb-0:last-child { margin-bottom: 0; }
509
+
510
+ /* \u2500\u2500 Transitions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
511
+ .qa-transition { transition-property: color,background-color,border-color,opacity,box-shadow,transform; transition-duration: 150ms; transition-timing-function: cubic-bezier(0.4,0,0.2,1); }
512
+ .qa-transition-all { transition: all 150ms cubic-bezier(0.4,0,0.2,1); }
513
+
514
+ /* \u2500\u2500 Animations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
515
+ @keyframes qaSpin {
516
+ from { transform: rotate(0deg); }
517
+ to { transform: rotate(360deg); }
518
+ }
519
+
520
+ @keyframes qaPulse {
521
+ 0%, 100% { opacity: 1; }
522
+ 50% { opacity: 0.5; box-shadow: 0 0 0 8px transparent; }
523
+ }
524
+
525
+ .qa-animate-spin {
526
+ animation: qaSpin 1s linear infinite;
527
+ }
528
+
529
+ .qa-animate-pulse-accent {
530
+ animation: qaPulse 2s ease-in-out infinite;
531
+ color: var(--qa-accent, #7c3aed);
532
+ }
533
+
534
+ /* \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 */
535
+ @media print {
536
+ .qa-print-hidden { display: none !important; }
537
+ }
538
+
539
+ /* \u2500\u2500 Transform \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
540
+ .qa-translate-x-neg-half { transform: translateX(-50%); }
541
+ .qa-translate-y-neg-full { transform: translateY(-100%); }
542
+
543
+ /* \u2500\u2500 Misc \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
544
+ .qa-w-320 { width: 320px; }
545
+ .qa-dir-ltr { direction: ltr; }
546
+
547
+ /* \u2500\u2500 Panel size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
548
+ .qa-w-panel { width: min(93vw, 420px); }
549
+ .qa-max-h-74vh { max-height: 74vh; max-height: min(74vh, 74dvh); }
550
+
551
+ /* \u2500\u2500 Extra 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 */
552
+ .qa-rounded-2xl { border-radius: 1rem; }
553
+
554
+ /* \u2500\u2500 Extra padding (top / bottom) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
555
+ .qa-pt-1 { padding-top: 0.25rem; }
556
+ .qa-pt-2 { padding-top: 0.5rem; }
557
+ .qa-pt-3 { padding-top: 0.75rem; }
558
+ .qa-pb-1 { padding-bottom: 0.25rem; }
559
+ .qa-pb-2 { padding-bottom: 0.5rem; }
560
+ .qa-pb-3 { padding-bottom: 0.75rem; }
561
+
562
+ /* \u2500\u2500 Extra margin-top \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
563
+ .qa-mt-0\\.5 { margin-top: 0.125rem; }
564
+
565
+ /* \u2500\u2500 Panel slide-in / slide-out animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
566
+ .qa-panel-anim {
567
+ opacity: 0;
568
+ transform: translateY(16px) scale(0.98);
569
+ transition: opacity 200ms cubic-bezier(0.4,0,0.2,1),
570
+ transform 200ms cubic-bezier(0.4,0,0.2,1);
571
+ }
572
+ .qa-panel-anim.qa-panel-in {
573
+ opacity: 1;
574
+ transform: translateY(0) scale(1);
575
+ }
576
+
577
+ /* \u2500\u2500 Capture-card fade-in animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
578
+ .qa-card-anim {
579
+ opacity: 0;
580
+ transform: scale(0.96);
581
+ transition: opacity 140ms ease, transform 140ms ease;
582
+ }
583
+ .qa-card-anim.qa-card-in {
584
+ opacity: 1;
585
+ transform: scale(1);
586
+ }
587
+
588
+ /* \u2500\u2500 FAB button interactions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
589
+ .qa-fab-btn {
590
+ transition: transform 150ms cubic-bezier(0.4,0,0.2,1),
591
+ box-shadow 150ms cubic-bezier(0.4,0,0.2,1);
592
+ cursor: pointer;
593
+ border: none;
594
+ }
595
+ .qa-fab-btn:hover { transform: scale(1.06); }
596
+ .qa-fab-btn:active { transform: scale(0.94); }
597
+ .qa-fab-btn:focus-visible {
598
+ outline: 3px solid rgba(255,255,255,0.6);
599
+ outline-offset: 2px;
600
+ }
601
+
602
+ /* \u2500\u2500 Tab indicator bar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
603
+ .qa-tab-indicator {
604
+ position: absolute;
605
+ bottom: -1px;
606
+ height: 2px;
607
+ border-radius: 9999px;
608
+ transition: left 200ms cubic-bezier(0.4,0,0.2,1),
609
+ width 200ms cubic-bezier(0.4,0,0.2,1);
610
+ pointer-events: none;
611
+ }
612
+
613
+ /* \u2500\u2500 brightness hover (capture / note buttons) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
614
+ .qa-hover-brightness-105:hover { filter: brightness(1.05); }
615
+
616
+ /* \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 */
617
+ .qa-space-y-1\\.5 > * + * { margin-top: 0.375rem; }
618
+ `;
619
+ function injectStyles(root) {
620
+ if (typeof CSSStyleSheet !== "undefined" && "adoptedStyleSheets" in Document.prototype) {
621
+ try {
622
+ const sheet = new CSSStyleSheet();
623
+ sheet.replaceSync(QA_CSS);
624
+ root.adoptedStyleSheets = [sheet];
625
+ return;
626
+ } catch {
627
+ }
628
+ }
629
+ const style = document.createElement("style");
630
+ style.textContent = QA_CSS;
631
+ root.appendChild(style);
632
+ }
633
+ function applyThemeVars(host, theme) {
634
+ host.style.setProperty("--qa-primary", theme.primary);
635
+ host.style.setProperty("--qa-primary-dark", theme.primaryDark);
636
+ host.style.setProperty("--qa-accent", theme.accent);
637
+ host.style.setProperty("--qa-accent-dark", theme.accentDark);
638
+ host.style.setProperty("--qa-sage", theme.sage);
639
+ host.style.setProperty("--qa-cream", theme.cream);
640
+ host.style.setProperty("--qa-mauve", theme.mauve);
641
+ host.style.setProperty("--qa-surface", theme.surface);
642
+ host.style.setProperty("--qa-ink", theme.ink);
643
+ }
644
+
645
+ // src/lib/storage.ts
646
+ function isStorageAvailable() {
647
+ if (typeof window === "undefined") return false;
648
+ try {
649
+ const probe = "__qa_probe__";
650
+ window.localStorage.setItem(probe, "1");
651
+ window.localStorage.removeItem(probe);
652
+ return true;
653
+ } catch {
654
+ return false;
655
+ }
656
+ }
657
+ function createStorage(namespace) {
658
+ const prefix = `${namespace}:`;
659
+ const fallback = /* @__PURE__ */ new Map();
660
+ const available = isStorageAvailable();
661
+ function fullKey(key) {
662
+ return `${prefix}${key}`;
663
+ }
664
+ function getItem(key) {
665
+ if (available) {
666
+ try {
667
+ return window.localStorage.getItem(fullKey(key));
668
+ } catch {
669
+ }
670
+ }
671
+ return fallback.get(fullKey(key)) ?? null;
672
+ }
673
+ function setItem(key, value) {
674
+ if (available) {
675
+ try {
676
+ window.localStorage.setItem(fullKey(key), value);
677
+ return;
678
+ } catch {
679
+ }
680
+ }
681
+ fallback.set(fullKey(key), value);
682
+ }
683
+ function getJSON(key, fb) {
684
+ const raw = getItem(key);
685
+ if (raw === null) return fb;
686
+ try {
687
+ return JSON.parse(raw);
688
+ } catch {
689
+ return fb;
690
+ }
691
+ }
692
+ function setJSON(key, value) {
693
+ try {
694
+ setItem(key, JSON.stringify(value));
695
+ } catch {
696
+ }
697
+ }
698
+ return { getItem, setItem, getJSON, setJSON };
699
+ }
700
+
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
+ // src/lib/strings.ts
805
+ var STR = {
806
+ en: {
807
+ tab_notes: "Notes",
808
+ tab_logins: "Logins",
809
+ tab_guide: "Guide",
810
+ export: "Export",
811
+ delete_all_q: "Delete all {n}?",
812
+ yes: "Yes",
813
+ no: "No",
814
+ clear_all: "Clear all points",
815
+ capture_cta: "Capture from page",
816
+ quick_note: "quick note (no selection)",
817
+ desc_placeholder: "Describe the change / bug / idea\u2026",
818
+ image_hint: "paste, drag, or click to add an image",
819
+ add_point: "Add point",
820
+ cancel: "Cancel",
821
+ save: "Save",
822
+ edit: "Edit",
823
+ remove_image: "Remove image",
824
+ add_image: "Add image",
825
+ no_points: "No points yet.",
826
+ no_points_hint: 'Hit "{cta}" to start.',
827
+ kind_note: "note",
828
+ kind_element: "element",
829
+ kind_region: "region",
830
+ cap_click: "Click an element",
831
+ cap_drag: "Drag to draw a region",
832
+ sel_region: "Region selected",
833
+ sel_element: "Element selected",
834
+ capturing: "capturing screenshot\u2026",
835
+ no_shot: "no screenshot (location saved)",
836
+ annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
837
+ save_point: "Save point",
838
+ reselect: "Reselect",
839
+ save_hint: "\u2318/Ctrl + Enter to save",
840
+ login_with: "Log in with {field} + password.",
841
+ used_count: "{n}/{m} used",
842
+ used: "used",
843
+ loc_captured: "Place captured",
844
+ loc_show: "Show captured location",
845
+ loc_hide: "Hide location",
846
+ loc_locate: "Locate on page",
847
+ journey_title: "Testing journey",
848
+ export_name_title: "Name your export",
849
+ export_name_placeholder: "file name",
850
+ tap_element: "Tap an element",
851
+ draw_region: "Draw region",
852
+ use_this: "Use this",
853
+ adjust: "Adjust",
854
+ resize: "Resize",
855
+ confirm_region: "Confirm region"
856
+ },
857
+ ar: {
858
+ tab_notes: "\u0627\u0644\u0645\u0644\u0627\u062D\u0638\u0627\u062A",
859
+ tab_logins: "\u0627\u0644\u062F\u062E\u0648\u0644",
860
+ tab_guide: "\u0627\u0644\u062F\u0644\u064A\u0644",
861
+ export: "\u062A\u0635\u062F\u064A\u0631",
862
+ delete_all_q: "\u062D\u0630\u0641 \u0643\u0644 {n}\u061F",
863
+ yes: "\u0646\u0639\u0645",
864
+ no: "\u0644\u0627",
865
+ clear_all: "\u0645\u0633\u062D \u0643\u0644 \u0627\u0644\u0646\u0642\u0627\u0637",
866
+ capture_cta: "\u0627\u0644\u062A\u0642\u0627\u0637 \u0645\u0646 \u0627\u0644\u0635\u0641\u062D\u0629",
867
+ quick_note: "\u0645\u0644\u0627\u062D\u0638\u0629 \u0633\u0631\u064A\u0639\u0629 (\u0628\u062F\u0648\u0646 \u062A\u062D\u062F\u064A\u062F)",
868
+ desc_placeholder: "\u0635\u0650\u0641 \u0627\u0644\u062A\u063A\u064A\u064A\u0631 / \u0627\u0644\u062E\u0644\u0644 / \u0627\u0644\u0641\u0643\u0631\u0629\u2026",
869
+ image_hint: "\u0627\u0644\u0635\u0642 \u0623\u0648 \u0627\u0633\u062D\u0628 \u0623\u0648 \u0627\u0646\u0642\u0631 \u0644\u0625\u0636\u0627\u0641\u0629 \u0635\u0648\u0631\u0629",
870
+ add_point: "\u0625\u0636\u0627\u0641\u0629 \u0646\u0642\u0637\u0629",
871
+ cancel: "\u0625\u0644\u063A\u0627\u0621",
872
+ save: "\u062D\u0641\u0638",
873
+ edit: "\u062A\u0639\u062F\u064A\u0644",
874
+ remove_image: "\u062D\u0630\u0641 \u0627\u0644\u0635\u0648\u0631\u0629",
875
+ add_image: "\u0625\u0636\u0627\u0641\u0629 \u0635\u0648\u0631\u0629",
876
+ no_points: "\u0644\u0627 \u062A\u0648\u062C\u062F \u0646\u0642\u0627\u0637 \u0628\u0639\u062F.",
877
+ no_points_hint: "\u0627\u0636\u063A\u0637 \xAB{cta}\xBB \u0644\u0644\u0628\u062F\u0621.",
878
+ kind_note: "\u0645\u0644\u0627\u062D\u0638\u0629",
879
+ kind_element: "\u0639\u0646\u0635\u0631",
880
+ kind_region: "\u0645\u0646\u0637\u0642\u0629",
881
+ cap_click: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
882
+ cap_drag: "\u0627\u0633\u062D\u0628 \u0644\u0631\u0633\u0645 \u0645\u0646\u0637\u0642\u0629",
883
+ sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
884
+ sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
885
+ capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
886
+ no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
887
+ annotate_placeholder: "\u0645\u0627\u0630\u0627 \u062A\u0631\u064A\u062F \u0623\u0646 \u062A\u0641\u0639\u0644 \u0647\u0646\u0627\u061F (\u0625\u0636\u0627\u0641\u0629 / \u062D\u0630\u0641 / \u062A\u063A\u064A\u064A\u0631\u2026)",
888
+ save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
889
+ reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
890
+ save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
891
+ login_with: "\u0633\u062C\u0651\u0644 \u0627\u0644\u062F\u062E\u0648\u0644 \u0628\u0640 {field} + \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631.",
892
+ used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
893
+ used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
894
+ loc_captured: "\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0645\u0648\u0642\u0639",
895
+ loc_show: "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u064F\u0644\u062A\u0642\u064E\u0637",
896
+ loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
897
+ loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
898
+ journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
899
+ export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
900
+ export_name_placeholder: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0644\u0641",
901
+ tap_element: "\u0627\u0646\u0642\u0631 \u0639\u0644\u0649 \u0639\u0646\u0635\u0631",
902
+ draw_region: "\u0627\u0631\u0633\u0645 \u0645\u0646\u0637\u0642\u0629",
903
+ use_this: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0647\u0630\u0627",
904
+ adjust: "\u062A\u0639\u062F\u064A\u0644",
905
+ resize: "\u062A\u063A\u064A\u064A\u0631 \u0627\u0644\u062D\u062C\u0645",
906
+ confirm_region: "\u062A\u0623\u0643\u064A\u062F \u0627\u0644\u0645\u0646\u0637\u0642\u0629"
907
+ }
908
+ };
909
+ function translate(lang, key, vars) {
910
+ const map = STR[lang] ?? STR.en;
911
+ const s = map[key] ?? STR.en[key] ?? key;
912
+ if (!vars) return s;
913
+ return s.replace(
914
+ /\{(\w+)\}/g,
915
+ (_, k) => vars[k] != null ? String(vars[k]) : `{${k}}`
916
+ );
917
+ }
918
+ function pick(value, lang) {
919
+ if (value == null) return "";
920
+ if (typeof value === "string") return value;
921
+ return value[lang] ?? value.en ?? "";
922
+ }
923
+
924
+ // src/lib/coverage.ts
925
+ var RISK_COLORS = {
926
+ red: "#EF4444",
927
+ amber: "#F59E0B",
928
+ green: "#22C55E",
929
+ none: "#CBD5E1"
930
+ };
931
+ function computeCoverage(journey, guideChecked) {
932
+ const red = { total: 0, covered: 0 };
933
+ const amber = { total: 0, covered: 0 };
934
+ const green = { total: 0, covered: 0 };
935
+ const uncoveredReds = [];
936
+ const coveredReds = [];
937
+ for (const lane of journey) {
938
+ const laneLabel = typeof lane.role === "string" ? lane.role : lane.role.en;
939
+ for (const step of lane.steps) {
940
+ const key = `${lane.id}::${step.path}`;
941
+ const covered = guideChecked.has(key);
942
+ const risk = step.risk ?? "green";
943
+ if (risk === "red") {
944
+ red.total++;
945
+ if (covered) {
946
+ red.covered++;
947
+ coveredReds.push({ lane: laneLabel, path: step.path });
948
+ } else {
949
+ uncoveredReds.push({
950
+ lane: laneLabel,
951
+ path: step.path,
952
+ riskWhy: step.riskWhy
953
+ });
954
+ }
955
+ } else if (risk === "amber") {
956
+ amber.total++;
957
+ if (covered) amber.covered++;
958
+ } else {
959
+ green.total++;
960
+ if (covered) green.covered++;
961
+ }
962
+ }
963
+ }
964
+ const totalTotal = red.total + amber.total + green.total;
965
+ const totalCovered = red.covered + amber.covered + green.covered;
966
+ const redScore = red.total === 0 ? 1 : red.covered / red.total;
967
+ let tier;
968
+ if (redScore >= 1) {
969
+ tier = "Complete";
970
+ } else if (redScore >= 0.8) {
971
+ tier = "Full";
972
+ } else if (redScore >= 0.5) {
973
+ tier = "Adequate";
974
+ } else {
975
+ tier = "Minimal";
976
+ }
977
+ return {
978
+ red,
979
+ amber,
980
+ green,
981
+ total: { total: totalTotal, covered: totalCovered },
982
+ redScore,
983
+ tier,
984
+ uncoveredReds,
985
+ coveredReds
986
+ };
987
+ }
988
+
989
+ // src/lib/exportZip.ts
990
+ function fmtTarget(t) {
991
+ const lines = [];
992
+ lines.push(`- **Target:** ${t.kind === "region" ? "freeform region" : "element"}`);
993
+ if (t.selector) lines.push(`- **Selector:** \`${t.selector}\``);
994
+ if (t.tagName) lines.push(`- **Tag:** \`<${t.tagName}>\``);
995
+ if (t.text) lines.push(`- **Text:** ${t.text}`);
996
+ if (t.rect) {
997
+ lines.push(
998
+ `- **Position:** top ${t.rect.top}, left ${t.rect.left}, ${t.rect.width}\xD7${t.rect.height}`
999
+ );
1000
+ }
1001
+ return lines;
1002
+ }
1003
+ function fmt(note, index) {
1004
+ const num = index + 1;
1005
+ const lines = [`## Point ${num}`];
1006
+ lines.push(`- **Page:** ${note.route || note.url || "(unknown)"}`);
1007
+ if (note.url && note.url !== note.route) lines.push(`- **Full URL:** ${note.url}`);
1008
+ lines.push(`- **When:** ${note.timestamp}`);
1009
+ if (note.target) {
1010
+ lines.push(...fmtTarget(note.target));
1011
+ }
1012
+ if (note.screenshot) lines.push(`- **Screenshot:** screenshots/point-${num}.png`);
1013
+ lines.push("", note.description || "(no description)", "", "---", "");
1014
+ return lines.join("\n");
1015
+ }
1016
+ function safeName(name, stamp) {
1017
+ const fallback = `qa-notes-${stamp.slice(0, 10)}`;
1018
+ let base = (name ?? "").trim().replace(/\.zip$/i, "");
1019
+ base = base.replace(/[\\/:*?"<>|]+/g, "-").replace(/\s+/g, " ").slice(0, 80).trim();
1020
+ return `${base || fallback}.zip`;
1021
+ }
1022
+ function toStrings(val) {
1023
+ if (val == null) return [];
1024
+ if (Array.isArray(val)) return val.filter((l) => String(l).trim().length > 0);
1025
+ return val.split("\n").filter((l) => l.trim().length > 0);
1026
+ }
1027
+ function mdTable(headers, rows) {
1028
+ const sep = headers.map(() => "---");
1029
+ const lines = [
1030
+ `| ${headers.join(" | ")} |`,
1031
+ `| ${sep.join(" | ")} |`,
1032
+ ...rows.map((r) => `| ${r.map((c) => c.replace(/\|/g, "\\|")).join(" | ")} |`)
1033
+ ];
1034
+ return lines.join("\n");
1035
+ }
1036
+ function fmtPct(n, d) {
1037
+ if (d === 0) return "N/A";
1038
+ return `${Math.round(n / d * 100)}%`;
1039
+ }
1040
+ function buildPreamble(config, guideChecked, stamp, noteCount) {
1041
+ const sections = [];
1042
+ const p = config.preamble ?? {};
1043
+ const brandLabel = config.brand?.label ?? "Qapture";
1044
+ const projectName = typeof p.projectName === "string" && p.projectName.trim() ? p.projectName.trim() : brandLabel;
1045
+ const loginLabel = config.loginField?.en ?? "Login";
1046
+ const journey = config.journey ?? [];
1047
+ sections.push(
1048
+ "<!-- Qapture Export Preamble \u2014 read before acting on any point. NO AI is bundled in Qapture \u2014 YOU are the AI reading this. -->"
1049
+ );
1050
+ const oneLiner = typeof p.oneLiner === "string" && p.oneLiner.trim() ? `
1051
+ > ${p.oneLiner.trim()}` : "";
1052
+ sections.push(
1053
+ `# ${projectName} \u2014 QA Handoff${oneLiner}
1054
+
1055
+ Exported: ${stamp}
1056
+ Points: ${noteCount}`
1057
+ );
1058
+ const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
1059
+ const runArr = toStrings(p.runCommands);
1060
+ const runValue = runArr.length > 0 ? runArr.map((c) => `\`${c}\``).join(", ") : "(not provided)";
1061
+ sections.push(
1062
+ `## Project
1063
+
1064
+ ${mdTable(
1065
+ ["Field", "Value"],
1066
+ [
1067
+ ["Name", projectName],
1068
+ ["Stack", stack],
1069
+ ["Run commands", runValue]
1070
+ ]
1071
+ )}`
1072
+ );
1073
+ if (config.theme) {
1074
+ const tokenRows = Object.entries(config.theme).map(
1075
+ ([k, v]) => [k, typeof v === "string" ? v : String(v)]
1076
+ );
1077
+ sections.push(
1078
+ `## Theme Tokens
1079
+
1080
+ ${mdTable(["Token", "Hex"], tokenRows)}`
1081
+ );
1082
+ } else {
1083
+ sections.push("## Theme Tokens\n\n(not provided)");
1084
+ }
1085
+ const conventions = toStrings(p.conventions);
1086
+ if (conventions.length > 0) {
1087
+ const list = conventions.map((c, i) => `${i + 1}. ${c}`).join("\n");
1088
+ sections.push(`## Conventions
1089
+
1090
+ ${list}`);
1091
+ } else {
1092
+ sections.push("## Conventions\n\n(not provided)");
1093
+ }
1094
+ const creds = config.credentials ?? [];
1095
+ let credBlock;
1096
+ if (creds.length > 0) {
1097
+ const credRows = creds.map((c) => [
1098
+ c.role,
1099
+ c.login,
1100
+ c.password || "(none)",
1101
+ c.seeded ? "seeded" : "manual",
1102
+ c.hint?.en ?? "\u2014"
1103
+ ]);
1104
+ credBlock = mdTable(
1105
+ ["Role", loginLabel, "Password", "Status", "Hint"],
1106
+ credRows
1107
+ );
1108
+ } else {
1109
+ credBlock = "(not provided)";
1110
+ }
1111
+ sections.push(
1112
+ `## Login Context
1113
+
1114
+ ${credBlock}
1115
+
1116
+ > **WARNING:** These are DEV/TEST/SEED credentials only. Never forward, commit, or use in production.`
1117
+ );
1118
+ const cov = computeCoverage(journey, guideChecked);
1119
+ const covTableRows = [
1120
+ ["RED", String(cov.red.total), String(cov.red.covered), String(cov.red.total - cov.red.covered), fmtPct(cov.red.covered, cov.red.total)],
1121
+ ["AMBER", String(cov.amber.total), String(cov.amber.covered), String(cov.amber.total - cov.amber.covered), fmtPct(cov.amber.covered, cov.amber.total)],
1122
+ ["GREEN", String(cov.green.total), String(cov.green.covered), String(cov.green.total - cov.green.covered), fmtPct(cov.green.covered, cov.green.total)],
1123
+ ["TOTAL", String(cov.total.total), String(cov.total.covered), String(cov.total.total - cov.total.covered), fmtPct(cov.total.covered, cov.total.total)]
1124
+ ];
1125
+ const uncoveredList = cov.uncoveredReds.length > 0 ? cov.uncoveredReds.map((r) => {
1126
+ const why = r.riskWhy ? ` \u2014 ${r.riskWhy}` : "";
1127
+ return `- [ ] [${r.lane}] ${r.path}${why}`;
1128
+ }).join("\n") : "(none)";
1129
+ const coveredList = cov.coveredReds.length > 0 ? cov.coveredReds.map((r) => `- [x] [${r.lane}] ${r.path}`).join("\n") : "(none)";
1130
+ sections.push(
1131
+ `## Coverage Report
1132
+
1133
+ ${mdTable(["Risk", "Total", "Covered", "Uncovered", "Coverage %"], covTableRows)}
1134
+
1135
+ Coverage tier: **${cov.tier}**
1136
+
1137
+ ### Uncovered RED zones (verify before shipping)
1138
+
1139
+ ${uncoveredList}
1140
+
1141
+ ### Covered RED zones
1142
+
1143
+ ${coveredList}
1144
+
1145
+ > Supervision note: Flag any uncovered RED zones that are directly related to the change requests in this batch \u2014 but do not block delivery on unrelated uncovered reds.`
1146
+ );
1147
+ const verifySteps = toStrings(p.verifySteps);
1148
+ if (verifySteps.length > 0) {
1149
+ const list = verifySteps.map((s, i) => `${i + 1}. ${s}`).join("\n");
1150
+ sections.push(`## How to Verify a Fix
1151
+
1152
+ ${list}`);
1153
+ } else {
1154
+ sections.push("## How to Verify a Fix\n\n(not provided)");
1155
+ }
1156
+ const invariants = toStrings(p.invariants);
1157
+ if (invariants.length > 0) {
1158
+ const list = invariants.map((s, i) => `${i + 1}. ${s}`).join("\n");
1159
+ sections.push(`## Invariants (Do Not Break)
1160
+
1161
+ ${list}`);
1162
+ } else {
1163
+ sections.push("## Invariants (Do Not Break)\n\n(not provided)");
1164
+ }
1165
+ const additionalContext = typeof p.additionalContext === "string" && p.additionalContext.trim() ? p.additionalContext.trim() : "(none)";
1166
+ sections.push(`## Additional Context
1167
+
1168
+ ${additionalContext}`);
1169
+ return sections.join("\n\n");
1170
+ }
1171
+ async function buildAndDownloadZip(notes, stamp, filename, config, guideChecked) {
1172
+ if (typeof document === "undefined") return;
1173
+ const { default: JSZip } = await import('jszip');
1174
+ const zip = new JSZip();
1175
+ const shots = zip.folder("screenshots");
1176
+ const resolvedChecked = guideChecked ?? /* @__PURE__ */ new Set();
1177
+ const resolvedConfig = config ?? {};
1178
+ const preambleMd = buildPreamble(resolvedConfig, resolvedChecked, stamp, notes.length);
1179
+ const brandLabel = resolvedConfig.brand?.label ?? "Qapture";
1180
+ const notesHeader = [
1181
+ `# ${brandLabel} Testing Notes`,
1182
+ "",
1183
+ `Exported: ${stamp}`,
1184
+ `Total points: ${notes.length}`,
1185
+ "",
1186
+ "Each point below is a requested change, bug, or observation captured while",
1187
+ "testing. Where present, a screenshot of the exact element/region is in the",
1188
+ "screenshots/ folder, referenced by point number.",
1189
+ "",
1190
+ "---",
1191
+ ""
1192
+ ].join("\n");
1193
+ const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notes.map((n, i) => fmt(n, i)).join("\n");
1194
+ zip.file("notes.md", notesMd);
1195
+ notes.forEach((n, i) => {
1196
+ if (n.screenshot && shots) {
1197
+ shots.file(`point-${i + 1}.png`, n.screenshot);
1198
+ }
1199
+ });
1200
+ const blob = await zip.generateAsync({ type: "blob" });
1201
+ const url = URL.createObjectURL(blob);
1202
+ const a = document.createElement("a");
1203
+ a.href = url;
1204
+ a.download = safeName(filename, stamp);
1205
+ document.body.appendChild(a);
1206
+ a.click();
1207
+ a.remove();
1208
+ setTimeout(() => URL.revokeObjectURL(url), 2e3);
1209
+ }
1210
+ function uid() {
1211
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
1212
+ return crypto.randomUUID();
1213
+ }
1214
+ return Date.now().toString(36) + Math.random().toString(36).slice(2);
1215
+ }
1216
+ var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1217
+ function safeLocation() {
1218
+ if (typeof window === "undefined") return { href: "", pathname: "", search: "" };
1219
+ return {
1220
+ href: window.location.href,
1221
+ pathname: window.location.pathname,
1222
+ search: window.location.search
1223
+ };
1224
+ }
1225
+ var QaContext = createContext(null);
1226
+ var LANG_KEY = "lang";
1227
+ var GUIDE_KEY = "guide";
1228
+ var LOGIN_KEY = "logins";
1229
+ function QaProvider({
1230
+ config,
1231
+ children
1232
+ }) {
1233
+ const [storage] = useState(() => createStorage(config.namespace));
1234
+ const [idb] = useState(() => createIdb(config.namespace));
1235
+ const [notes, setNotes] = useState([]);
1236
+ const [isOpen, setIsOpen] = useState(false);
1237
+ const [activeTab, setActiveTab] = useState("notes");
1238
+ const [captureActive, setCaptureActive] = useState(false);
1239
+ const [isExporting, setIsExporting] = useState(false);
1240
+ const [lang, setLangState] = useState(() => {
1241
+ const saved = storage.getItem(LANG_KEY);
1242
+ if (saved === "ar" || saved === "en") return saved;
1243
+ return config.rtl ? "ar" : "en";
1244
+ });
1245
+ const [guideChecked, setGuideChecked] = useState(
1246
+ () => new Set(storage.getJSON(GUIDE_KEY, []))
1247
+ );
1248
+ const [loginsUsed, setLoginsUsed] = useState(
1249
+ () => new Set(storage.getJSON(LOGIN_KEY, []))
1250
+ );
1251
+ useEffect(() => {
1252
+ let alive = true;
1253
+ idb.getAll().then((rows) => {
1254
+ if (!alive) return;
1255
+ const sorted = rows.slice().sort(
1256
+ (a, b) => a.timestamp < b.timestamp ? 1 : -1
1257
+ );
1258
+ setNotes(sorted);
1259
+ }).catch(() => {
1260
+ });
1261
+ return () => {
1262
+ alive = false;
1263
+ };
1264
+ }, [idb]);
1265
+ const setLang = useCallback((l) => {
1266
+ setLangState(l);
1267
+ storage.setItem(LANG_KEY, l);
1268
+ }, [storage]);
1269
+ const t = useCallback(
1270
+ (key, vars) => translate(lang, key, vars),
1271
+ [lang]
1272
+ );
1273
+ const pick2 = useCallback(
1274
+ (value2) => pick(value2, lang),
1275
+ [lang]
1276
+ );
1277
+ const addNote = useCallback(
1278
+ async ({
1279
+ description,
1280
+ screenshot,
1281
+ target
1282
+ }) => {
1283
+ const loc = safeLocation();
1284
+ const note = {
1285
+ id: uid(),
1286
+ url: loc.href,
1287
+ route: loc.pathname + loc.search,
1288
+ timestamp: nowIso(),
1289
+ description: (description || "").trim(),
1290
+ screenshot: screenshot ?? void 0,
1291
+ target: target ?? void 0
1292
+ };
1293
+ setNotes((prev) => [note, ...prev]);
1294
+ await idb.put(note);
1295
+ return note;
1296
+ },
1297
+ [idb]
1298
+ );
1299
+ const updateNote = useCallback(
1300
+ async (id, patch) => {
1301
+ let updated = null;
1302
+ setNotes(
1303
+ (prev) => prev.map((n) => {
1304
+ if (n.id !== id) return n;
1305
+ const next = { ...n };
1306
+ if (patch.description != null) next.description = patch.description.trim();
1307
+ if (patch.screenshot === null) {
1308
+ next.screenshot = void 0;
1309
+ } else if (patch.screenshot !== void 0) {
1310
+ next.screenshot = patch.screenshot;
1311
+ }
1312
+ updated = next;
1313
+ return next;
1314
+ })
1315
+ );
1316
+ if (updated) {
1317
+ await idb.put(updated);
1318
+ }
1319
+ },
1320
+ [idb]
1321
+ );
1322
+ const deleteNote = useCallback(
1323
+ async (id) => {
1324
+ setNotes((prev) => prev.filter((n) => n.id !== id));
1325
+ await idb.delete(id);
1326
+ },
1327
+ [idb]
1328
+ );
1329
+ const clearAll = useCallback(async () => {
1330
+ setNotes([]);
1331
+ await idb.clear();
1332
+ }, [idb]);
1333
+ const startCapture = useCallback(() => {
1334
+ setIsOpen(false);
1335
+ setCaptureActive(true);
1336
+ }, []);
1337
+ const endCapture = useCallback((reopen = true) => {
1338
+ setCaptureActive(false);
1339
+ if (reopen) setIsOpen(true);
1340
+ }, []);
1341
+ const toggleGuide = useCallback((key) => {
1342
+ setGuideChecked((prev) => {
1343
+ const next = new Set(prev);
1344
+ if (next.has(key)) next.delete(key);
1345
+ else next.add(key);
1346
+ storage.setJSON(GUIDE_KEY, [...next]);
1347
+ return next;
1348
+ });
1349
+ }, [storage]);
1350
+ const toggleLogin = useCallback((key) => {
1351
+ setLoginsUsed((prev) => {
1352
+ const next = new Set(prev);
1353
+ if (next.has(key)) next.delete(key);
1354
+ else next.add(key);
1355
+ storage.setJSON(LOGIN_KEY, [...next]);
1356
+ return next;
1357
+ });
1358
+ }, [storage]);
1359
+ const exportZipFn = useCallback(
1360
+ async (filename) => {
1361
+ if (!notes.length || isExporting) return;
1362
+ setIsExporting(true);
1363
+ try {
1364
+ await buildAndDownloadZip(notes, nowIso(), filename, config, guideChecked);
1365
+ } catch (err) {
1366
+ console.error("[QA] export failed", err);
1367
+ } finally {
1368
+ setIsExporting(false);
1369
+ }
1370
+ },
1371
+ [notes, isExporting, config, guideChecked]
1372
+ );
1373
+ const value = {
1374
+ // Data
1375
+ notes,
1376
+ guideChecked,
1377
+ loginsUsed,
1378
+ // UI state
1379
+ isOpen,
1380
+ activeTab,
1381
+ captureActive,
1382
+ isExporting,
1383
+ // i18n
1384
+ lang,
1385
+ dir: lang === "ar" ? "rtl" : "ltr",
1386
+ // Config passthrough
1387
+ theme: config.theme,
1388
+ brand: config.brand,
1389
+ loginField: config.loginField,
1390
+ credentials: config.credentials,
1391
+ journey: config.journey,
1392
+ preamble: config.preamble,
1393
+ // i18n helpers
1394
+ t,
1395
+ pick: pick2,
1396
+ // Actions
1397
+ setIsOpen,
1398
+ setActiveTab,
1399
+ setLang,
1400
+ addNote,
1401
+ updateNote,
1402
+ deleteNote,
1403
+ clearAll,
1404
+ startCapture,
1405
+ endCapture,
1406
+ toggleGuide,
1407
+ toggleLogin,
1408
+ exportZip: exportZipFn
1409
+ };
1410
+ return /* @__PURE__ */ jsx(QaContext.Provider, { value, children });
1411
+ }
1412
+ function useQa() {
1413
+ const ctx = useContext(QaContext);
1414
+ if (!ctx) throw new Error("useQa must be used inside <QaProvider>");
1415
+ return ctx;
1416
+ }
1417
+ var ICONS = {
1418
+ Check: [
1419
+ ["path", { d: "M20 6 9 17l-5-5" }]
1420
+ ],
1421
+ X: [
1422
+ ["path", { d: "M18 6 6 18" }],
1423
+ ["path", { d: "m6 6 12 12" }]
1424
+ ],
1425
+ Loader2: [
1426
+ ["path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }]
1427
+ ],
1428
+ MousePointerClick: [
1429
+ ["path", { d: "m9 9 5 12 1.8-5.2L21 14Z" }],
1430
+ ["path", { d: "M7.2 2.2 8 5.1" }],
1431
+ ["path", { d: "m5.1 8-2.9-.8" }],
1432
+ ["path", { d: "M14 4.1 12 6" }],
1433
+ ["path", { d: "m6 12-1.9 2" }]
1434
+ ],
1435
+ Square: [
1436
+ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
1437
+ ],
1438
+ ImagePlus: [
1439
+ ["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
1440
+ ["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
1441
+ ["line", { x1: "19", x2: "19", y1: "2", y2: "8" }],
1442
+ ["circle", { cx: "9", cy: "9", r: "2" }],
1443
+ ["path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }]
1444
+ ],
1445
+ CheckCircle2: [
1446
+ ["circle", { cx: "12", cy: "12", r: "10" }],
1447
+ ["path", { d: "m9 12 2 2 4-4" }]
1448
+ ],
1449
+ Crosshair: [
1450
+ ["circle", { cx: "12", cy: "12", r: "10" }],
1451
+ ["line", { x1: "22", x2: "18", y1: "12", y2: "12" }],
1452
+ ["line", { x1: "6", x2: "2", y1: "12", y2: "12" }],
1453
+ ["line", { x1: "12", x2: "12", y1: "6", y2: "2" }],
1454
+ ["line", { x1: "12", x2: "12", y1: "22", y2: "18" }]
1455
+ ],
1456
+ Trash2: [
1457
+ ["path", { d: "M3 6h18" }],
1458
+ ["path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }],
1459
+ ["path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }],
1460
+ ["line", { x1: "10", x2: "10", y1: "11", y2: "17" }],
1461
+ ["line", { x1: "14", x2: "14", y1: "11", y2: "17" }]
1462
+ ],
1463
+ MapPin: [
1464
+ ["path", { d: "M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z" }],
1465
+ ["circle", { cx: "12", cy: "10", r: "3" }]
1466
+ ],
1467
+ FileText: [
1468
+ ["path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" }],
1469
+ ["polyline", { points: "14 2 14 8 20 8" }],
1470
+ ["line", { x1: "16", x2: "8", y1: "13", y2: "13" }],
1471
+ ["line", { x1: "16", x2: "8", y1: "17", y2: "17" }],
1472
+ ["line", { x1: "10", x2: "8", y1: "9", y2: "9" }]
1473
+ ],
1474
+ Pencil: [
1475
+ ["path", { d: "M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" }],
1476
+ ["path", { d: "m15 5 4 4" }]
1477
+ ],
1478
+ Download: [
1479
+ ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
1480
+ ["polyline", { points: "7 10 12 15 17 10" }],
1481
+ ["line", { x1: "12", x2: "12", y1: "15", y2: "3" }]
1482
+ ],
1483
+ Trash: [
1484
+ ["path", { d: "M3 6h18" }],
1485
+ ["path", { d: "M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" }],
1486
+ ["path", { d: "M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" }]
1487
+ ],
1488
+ StickyNote: [
1489
+ ["path", { d: "M15.5 3H5a2 2 0 0 0-2 2v14c0 1.1.9 2 2 2h14a2 2 0 0 0 2-2V8.5L15.5 3Z" }],
1490
+ ["path", { d: "M15 3v6h6" }]
1491
+ ],
1492
+ KeyRound: [
1493
+ ["path", { d: "M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z" }],
1494
+ ["circle", { cx: "16.5", cy: "7.5", r: ".5" }]
1495
+ ],
1496
+ Map: [
1497
+ ["polygon", { points: "3 6 9 3 15 6 21 3 21 18 15 21 9 18 3 21" }],
1498
+ ["line", { x1: "9", x2: "9", y1: "3", y2: "18" }],
1499
+ ["line", { x1: "15", x2: "15", y1: "6", y2: "21" }]
1500
+ ],
1501
+ ClipboardList: [
1502
+ ["rect", { width: "8", height: "4", x: "8", y: "2", rx: "1", ry: "1" }],
1503
+ ["path", { d: "M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" }],
1504
+ ["path", { d: "M12 11h4" }],
1505
+ ["path", { d: "M12 16h4" }],
1506
+ ["path", { d: "M8 11h.01" }],
1507
+ ["path", { d: "M8 16h.01" }]
1508
+ ],
1509
+ Copy: [
1510
+ ["rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }],
1511
+ ["path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" }]
1512
+ ],
1513
+ CircleUser: [
1514
+ ["circle", { cx: "12", cy: "12", r: "10" }],
1515
+ ["circle", { cx: "12", cy: "10", r: "3" }],
1516
+ ["path", { d: "M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" }]
1517
+ ],
1518
+ Circle: [
1519
+ ["circle", { cx: "12", cy: "12", r: "10" }]
1520
+ ],
1521
+ Plus: [
1522
+ ["path", { d: "M5 12h14" }],
1523
+ ["path", { d: "M12 5v14" }]
1524
+ ],
1525
+ MapPinned: [
1526
+ ["path", { d: "M18 8c0 4.5-6 9-6 9s-6-4.5-6-9a6 6 0 0 1 12 0" }],
1527
+ ["circle", { cx: "12", cy: "8", r: "2" }],
1528
+ ["path", { d: "M8.835 14H5a1 1 0 0 0-.9.7l-2 6c-.1.1-.1.2-.1.3 0 .6.4 1 1 1h18c.6 0 1-.4 1-1 0-.1 0-.2-.1-.3l-2-6a1 1 0 0 0-.9-.7h-3.835" }]
1529
+ ],
1530
+ ChevronDown: [
1531
+ ["path", { d: "m6 9 6 6 6-6" }]
1532
+ ]
1533
+ };
1534
+ function Icon({
1535
+ name,
1536
+ size = 24,
1537
+ className,
1538
+ style,
1539
+ strokeWidth = 2
1540
+ }) {
1541
+ const descriptors = ICONS[name];
1542
+ return /* @__PURE__ */ jsx(
1543
+ "svg",
1544
+ {
1545
+ xmlns: "http://www.w3.org/2000/svg",
1546
+ viewBox: "0 0 24 24",
1547
+ width: size,
1548
+ height: size,
1549
+ fill: "none",
1550
+ stroke: "currentColor",
1551
+ strokeWidth,
1552
+ strokeLinecap: "round",
1553
+ strokeLinejoin: "round",
1554
+ className,
1555
+ style,
1556
+ "aria-hidden": "true",
1557
+ children: descriptors.map(
1558
+ ([tag, rawAttrs], idx) => renderSvgElement(tag, rawAttrs, idx)
1559
+ )
1560
+ }
1561
+ );
1562
+ }
1563
+ function renderSvgElement(tag, attrs, key) {
1564
+ return React.createElement(tag, { key, ...attrs });
1565
+ }
1566
+ function QaFab() {
1567
+ const { isOpen, setIsOpen, notes, captureActive, theme } = useQa();
1568
+ if (captureActive) return null;
1569
+ return /* @__PURE__ */ jsxs(
1570
+ "button",
1571
+ {
1572
+ type: "button",
1573
+ "data-qa-overlay": "true",
1574
+ dir: "ltr",
1575
+ onClick: () => setIsOpen(!isOpen),
1576
+ "aria-label": "Qapture \u2014 testing notes",
1577
+ title: "Qapture",
1578
+ className: "qa-fixed qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-white qa-print-hidden qa-fab-btn",
1579
+ style: {
1580
+ left: "calc(1.25rem + env(safe-area-inset-left))",
1581
+ bottom: "calc(5rem + env(safe-area-inset-bottom))",
1582
+ width: "3.5rem",
1583
+ height: "3.5rem",
1584
+ backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1585
+ 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)",
1586
+ zIndex: 9990
1587
+ },
1588
+ children: [
1589
+ !isOpen && /* @__PURE__ */ jsx(
1590
+ "span",
1591
+ {
1592
+ className: "qa-absolute qa-inset-0 qa-rounded-full qa-opacity-60 qa-animate-pulse-accent",
1593
+ style: { pointerEvents: "none" },
1594
+ "aria-hidden": "true"
1595
+ }
1596
+ ),
1597
+ /* @__PURE__ */ jsx(Icon, { name: isOpen ? "X" : "ClipboardList", size: 24 }),
1598
+ !isOpen && notes.length > 0 && /* @__PURE__ */ jsx(
1599
+ "span",
1600
+ {
1601
+ className: "qa-absolute qa-flex qa-items-center qa-justify-center qa-rounded-full qa-text-xs qa-font-bold",
1602
+ "aria-label": `${notes.length} notes`,
1603
+ style: {
1604
+ top: "-4px",
1605
+ right: "-4px",
1606
+ minWidth: "1.5rem",
1607
+ height: "1.5rem",
1608
+ padding: "0 4px",
1609
+ background: "#fff",
1610
+ color: theme.primary,
1611
+ boxShadow: "0 1px 3px rgba(0,0,0,0.2)"
1612
+ },
1613
+ children: notes.length
1614
+ }
1615
+ )
1616
+ ]
1617
+ }
1618
+ );
1619
+ }
1620
+ function NoteEditor() {
1621
+ const { addNote, startCapture, t, theme } = useQa();
1622
+ const [open, setOpen] = useState(false);
1623
+ const [description, setDescription] = useState("");
1624
+ const [screenshot, setScreenshot] = useState(null);
1625
+ const [previewUrl, setPreviewUrl] = useState(null);
1626
+ const [dragOver, setDragOver] = useState(false);
1627
+ const fileRef = useRef(null);
1628
+ useEffect(() => {
1629
+ return () => {
1630
+ if (previewUrl) URL.revokeObjectURL(previewUrl);
1631
+ };
1632
+ }, []);
1633
+ const setImage = useCallback((blob) => {
1634
+ if (!blob) return;
1635
+ setScreenshot(blob);
1636
+ setPreviewUrl((old) => {
1637
+ if (old) URL.revokeObjectURL(old);
1638
+ return URL.createObjectURL(blob);
1639
+ });
1640
+ }, []);
1641
+ const clearImage = () => {
1642
+ setScreenshot(null);
1643
+ setPreviewUrl((o) => {
1644
+ if (o) URL.revokeObjectURL(o);
1645
+ return null;
1646
+ });
1647
+ };
1648
+ const onPaste = (e) => {
1649
+ const item = Array.from(e.clipboardData?.items ?? []).find(
1650
+ (i) => i.type.startsWith("image/")
1651
+ );
1652
+ if (item) {
1653
+ const b = item.getAsFile();
1654
+ if (b) {
1655
+ e.preventDefault();
1656
+ setImage(b);
1657
+ }
1658
+ }
1659
+ };
1660
+ const onDrop = (e) => {
1661
+ e.preventDefault();
1662
+ setDragOver(false);
1663
+ const f = Array.from(e.dataTransfer.files ?? []).find(
1664
+ (x) => x.type.startsWith("image/")
1665
+ );
1666
+ if (f) setImage(f);
1667
+ };
1668
+ const onFile = (e) => {
1669
+ const f = e.target.files?.[0];
1670
+ if (f?.type.startsWith("image/")) setImage(f);
1671
+ e.target.value = "";
1672
+ };
1673
+ const save = async () => {
1674
+ if (!description.trim()) return;
1675
+ await addNote({ description, screenshot: screenshot ?? void 0 });
1676
+ setDescription("");
1677
+ clearImage();
1678
+ setOpen(false);
1679
+ };
1680
+ return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", children: [
1681
+ /* @__PURE__ */ jsxs(
1682
+ "button",
1683
+ {
1684
+ onClick: startCapture,
1685
+ 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",
1686
+ style: {
1687
+ backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})`,
1688
+ border: "none",
1689
+ cursor: "pointer"
1690
+ },
1691
+ children: [
1692
+ /* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 16 }),
1693
+ t("capture_cta")
1694
+ ]
1695
+ }
1696
+ ),
1697
+ !open ? /* @__PURE__ */ jsxs(
1698
+ "button",
1699
+ {
1700
+ onClick: () => setOpen(true),
1701
+ 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",
1702
+ style: {
1703
+ borderColor: `${theme.primary}33`,
1704
+ color: theme.primary,
1705
+ background: "transparent",
1706
+ cursor: "pointer"
1707
+ },
1708
+ children: [
1709
+ /* @__PURE__ */ jsx(Icon, { name: "Plus", size: 14 }),
1710
+ t("quick_note")
1711
+ ]
1712
+ }
1713
+ ) : /* @__PURE__ */ jsxs(
1714
+ "div",
1715
+ {
1716
+ onPaste,
1717
+ className: "qa-space-y-2 qa-rounded-xl qa-border qa-p-2.5",
1718
+ style: { borderColor: `${theme.primary}1a`, background: theme.cream },
1719
+ children: [
1720
+ /* @__PURE__ */ jsx(
1721
+ "textarea",
1722
+ {
1723
+ autoFocus: true,
1724
+ value: description,
1725
+ onChange: (e) => setDescription(e.target.value),
1726
+ rows: 3,
1727
+ placeholder: t("desc_placeholder"),
1728
+ 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",
1729
+ style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
1730
+ }
1731
+ ),
1732
+ /* @__PURE__ */ jsxs(
1733
+ "div",
1734
+ {
1735
+ onDragOver: (e) => {
1736
+ e.preventDefault();
1737
+ setDragOver(true);
1738
+ },
1739
+ onDragLeave: () => setDragOver(false),
1740
+ onDrop,
1741
+ className: "qa-rounded-lg qa-border qa-border-dashed qa-px-2 qa-py-2 qa-text-center qa-text-xs",
1742
+ style: {
1743
+ borderColor: dragOver ? theme.accent : `${theme.primary}33`,
1744
+ background: dragOver ? `${theme.accent}12` : "#fff"
1745
+ },
1746
+ children: [
1747
+ previewUrl ? /* @__PURE__ */ jsxs("div", { className: "qa-relative qa-inline-block", children: [
1748
+ /* @__PURE__ */ jsx("img", { src: previewUrl, alt: "preview", style: { maxHeight: "7rem", borderRadius: "0.25rem" } }),
1749
+ /* @__PURE__ */ jsx(
1750
+ "button",
1751
+ {
1752
+ onClick: clearImage,
1753
+ className: "qa-absolute qa-rounded-full qa-p-1 qa-text-white qa-tap-icon",
1754
+ style: {
1755
+ top: "-8px",
1756
+ insetInlineEnd: "-8px",
1757
+ background: theme.primary,
1758
+ border: "none",
1759
+ cursor: "pointer"
1760
+ },
1761
+ children: /* @__PURE__ */ jsx(Icon, { name: "Trash2", size: 12 })
1762
+ }
1763
+ )
1764
+ ] }) : /* @__PURE__ */ jsxs(
1765
+ "button",
1766
+ {
1767
+ onClick: () => fileRef.current?.click(),
1768
+ className: "qa-inline-flex qa-items-center qa-gap-1 qa-tap",
1769
+ style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
1770
+ children: [
1771
+ /* @__PURE__ */ jsx(Icon, { name: "ImagePlus", size: 16 }),
1772
+ t("image_hint")
1773
+ ]
1774
+ }
1775
+ ),
1776
+ /* @__PURE__ */ jsx(
1777
+ "input",
1778
+ {
1779
+ ref: fileRef,
1780
+ type: "file",
1781
+ accept: "image/*",
1782
+ onChange: onFile,
1783
+ className: "qa-hidden"
1784
+ }
1785
+ )
1786
+ ]
1787
+ }
1788
+ ),
1789
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-gap-2", children: [
1790
+ /* @__PURE__ */ jsx(
1791
+ "button",
1792
+ {
1793
+ onClick: save,
1794
+ disabled: !description.trim(),
1795
+ 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",
1796
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
1797
+ children: t("add_point")
1798
+ }
1799
+ ),
1800
+ /* @__PURE__ */ jsx(
1801
+ "button",
1802
+ {
1803
+ onClick: () => {
1804
+ setOpen(false);
1805
+ clearImage();
1806
+ setDescription("");
1807
+ },
1808
+ className: "qa-rounded-lg qa-border qa-px-3 qa-text-sm qa-tap",
1809
+ style: {
1810
+ borderColor: `${theme.primary}33`,
1811
+ color: theme.primary,
1812
+ background: "transparent",
1813
+ cursor: "pointer"
1814
+ },
1815
+ children: t("cancel")
1816
+ }
1817
+ )
1818
+ ] })
1819
+ ]
1820
+ }
1821
+ )
1822
+ ] });
1823
+ }
1824
+
1825
+ // src/lib/highlight.ts
1826
+ function readCssVar(name, fallback) {
1827
+ if (typeof document === "undefined") return fallback;
1828
+ const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
1829
+ return val || fallback;
1830
+ }
1831
+ function paint(rect, color) {
1832
+ if (typeof document === "undefined") return;
1833
+ if (!rect || rect.width < 1 || rect.height < 1) return;
1834
+ const accent = readCssVar("--qa-accent", "#7c3aed");
1835
+ const primary = readCssVar("--qa-primary", "#4f46e5");
1836
+ const box = document.createElement("div");
1837
+ box.setAttribute("data-qa-overlay", "true");
1838
+ Object.assign(box.style, {
1839
+ position: "fixed",
1840
+ top: `${rect.top}px`,
1841
+ left: `${rect.left}px`,
1842
+ width: `${rect.width}px`,
1843
+ height: `${rect.height}px`,
1844
+ zIndex: "10098",
1845
+ pointerEvents: "none",
1846
+ borderRadius: "3px",
1847
+ outline: `3px solid ${accent}`,
1848
+ background: `${accent}22`,
1849
+ boxShadow: `0 0 0 4px ${primary}55`,
1850
+ transition: "opacity 0.45s ease",
1851
+ opacity: "1"
1852
+ });
1853
+ document.body.appendChild(box);
1854
+ setTimeout(() => {
1855
+ box.style.opacity = "0";
1856
+ }, 1e3);
1857
+ setTimeout(() => {
1858
+ if (box.parentNode) box.remove();
1859
+ }, 1500);
1860
+ }
1861
+ function flashLocate(target, color) {
1862
+ if (typeof document === "undefined" || !target) return;
1863
+ let el = null;
1864
+ if (target.selector) {
1865
+ try {
1866
+ el = document.querySelector(target.selector);
1867
+ } catch {
1868
+ el = null;
1869
+ }
1870
+ }
1871
+ if (el) {
1872
+ el.scrollIntoView({ block: "center", inline: "center" });
1873
+ requestAnimationFrame(() => {
1874
+ if (!el) return;
1875
+ const r = el.getBoundingClientRect();
1876
+ paint({ top: r.top, left: r.left, width: r.width, height: r.height });
1877
+ });
1878
+ } else if (target.rect) {
1879
+ paint(target.rect);
1880
+ }
1881
+ }
1882
+ function LocationReveal({ target }) {
1883
+ const { t, theme } = useQa();
1884
+ const [open, setOpen] = useState(false);
1885
+ if (!target) return null;
1886
+ const r = target.rect;
1887
+ return /* @__PURE__ */ jsxs(
1888
+ "div",
1889
+ {
1890
+ className: "qa-rounded-lg qa-border",
1891
+ style: { borderColor: `${theme.primary}1a`, background: theme.cream },
1892
+ children: [
1893
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1.5 qa-px-2 qa-py-1.5 qa-text-11", children: [
1894
+ /* @__PURE__ */ jsx(Icon, { name: "CheckCircle2", size: 14, style: { color: theme.sage } }),
1895
+ /* @__PURE__ */ jsx("span", { className: "qa-font-medium", style: { color: theme.ink }, children: t("loc_captured") }),
1896
+ /* @__PURE__ */ jsxs(
1897
+ "button",
1898
+ {
1899
+ onClick: () => setOpen((o) => !o),
1900
+ className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-font-medium qa-tap",
1901
+ style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
1902
+ children: [
1903
+ open ? t("loc_hide") : t("loc_show"),
1904
+ /* @__PURE__ */ jsx(
1905
+ Icon,
1906
+ {
1907
+ name: "ChevronDown",
1908
+ size: 14,
1909
+ style: {
1910
+ transition: "transform 150ms",
1911
+ transform: open ? "rotate(180deg)" : "rotate(0deg)"
1912
+ }
1913
+ }
1914
+ )
1915
+ ]
1916
+ }
1917
+ )
1918
+ ] }),
1919
+ open && /* @__PURE__ */ jsxs(
1920
+ "div",
1921
+ {
1922
+ className: "qa-space-y-1 qa-px-2 qa-pb-2 qa-text-11 qa-dir-ltr",
1923
+ style: { color: theme.ink },
1924
+ children: [
1925
+ target.selector && /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-gap-1", children: [
1926
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "selector" }),
1927
+ /* @__PURE__ */ jsx(
1928
+ "code",
1929
+ {
1930
+ className: "qa-min-w-0 qa-flex-1 qa-truncate qa-rounded qa-bg-white qa-px-1",
1931
+ title: target.selector,
1932
+ children: target.selector
1933
+ }
1934
+ )
1935
+ ] }),
1936
+ target.tagName && /* @__PURE__ */ jsxs("div", { children: [
1937
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "tag " }),
1938
+ /* @__PURE__ */ jsxs("code", { className: "qa-rounded qa-bg-white qa-px-1", children: [
1939
+ "<",
1940
+ target.tagName,
1941
+ ">"
1942
+ ] })
1943
+ ] }),
1944
+ target.text && /* @__PURE__ */ jsxs("div", { className: "qa-truncate", children: [
1945
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "text " }),
1946
+ '"',
1947
+ target.text,
1948
+ '"'
1949
+ ] }),
1950
+ r && /* @__PURE__ */ jsxs("div", { children: [
1951
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "pos " }),
1952
+ Math.round(r.left),
1953
+ ", ",
1954
+ Math.round(r.top),
1955
+ " \xB7 ",
1956
+ Math.round(r.width),
1957
+ "\xD7",
1958
+ Math.round(r.height)
1959
+ ] }),
1960
+ /* @__PURE__ */ jsxs(
1961
+ "button",
1962
+ {
1963
+ onClick: () => flashLocate(target),
1964
+ 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",
1965
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
1966
+ children: [
1967
+ /* @__PURE__ */ jsx(Icon, { name: "Crosshair", size: 12 }),
1968
+ /* @__PURE__ */ jsx(Icon, { name: "MapPinned", size: 12 }),
1969
+ t("loc_locate")
1970
+ ]
1971
+ }
1972
+ )
1973
+ ]
1974
+ }
1975
+ )
1976
+ ]
1977
+ }
1978
+ );
1979
+ }
1980
+ function useObjectUrl(blob) {
1981
+ const [url, setUrl] = useState(null);
1982
+ useEffect(() => {
1983
+ if (!blob) {
1984
+ setUrl(null);
1985
+ return;
1986
+ }
1987
+ const u = URL.createObjectURL(blob);
1988
+ setUrl(u);
1989
+ return () => URL.revokeObjectURL(u);
1990
+ }, [blob]);
1991
+ return url;
1992
+ }
1993
+ function KindBadge({
1994
+ target,
1995
+ t,
1996
+ theme
1997
+ }) {
1998
+ if (!target) {
1999
+ return /* @__PURE__ */ jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-10 qa-text-slate-400", children: [
2000
+ /* @__PURE__ */ jsx(Icon, { name: "FileText", size: 12 }),
2001
+ t("kind_note")
2002
+ ] });
2003
+ }
2004
+ const region = target.kind === "region";
2005
+ return /* @__PURE__ */ jsxs(
2006
+ "span",
2007
+ {
2008
+ 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",
2009
+ style: { background: region ? theme.accentDark : theme.primary },
2010
+ children: [
2011
+ /* @__PURE__ */ jsx(Icon, { name: region ? "Square" : "MousePointerClick", size: 10 }),
2012
+ region ? t("kind_region") : t("kind_element")
2013
+ ]
2014
+ }
2015
+ );
2016
+ }
2017
+ function NoteItem({ note, index }) {
2018
+ const { deleteNote, updateNote, t, theme } = useQa();
2019
+ const [editing, setEditing] = useState(false);
2020
+ const [desc, setDesc] = useState(note.description);
2021
+ const [img, setImg] = useState(note.screenshot ?? null);
2022
+ const fileRef = useRef(null);
2023
+ const thumbUrl = useObjectUrl(editing ? img ?? void 0 : note.screenshot);
2024
+ const startEdit = () => {
2025
+ setDesc(note.description);
2026
+ setImg(note.screenshot ?? null);
2027
+ setEditing(true);
2028
+ };
2029
+ const onFile = (e) => {
2030
+ const f = e.target.files?.[0];
2031
+ if (f?.type.startsWith("image/")) setImg(f);
2032
+ e.target.value = "";
2033
+ };
2034
+ const onPaste = (e) => {
2035
+ const it = Array.from(e.clipboardData?.items ?? []).find(
2036
+ (i) => i.type.startsWith("image/")
2037
+ );
2038
+ if (it) {
2039
+ const b = it.getAsFile();
2040
+ if (b) {
2041
+ e.preventDefault();
2042
+ setImg(b);
2043
+ }
2044
+ }
2045
+ };
2046
+ const save = () => {
2047
+ const patch = { description: desc };
2048
+ if (img !== (note.screenshot ?? null)) patch.screenshot = img;
2049
+ updateNote(note.id, patch);
2050
+ setEditing(false);
2051
+ };
2052
+ return /* @__PURE__ */ jsxs(
2053
+ "li",
2054
+ {
2055
+ className: "qa-rounded-xl qa-border qa-bg-white qa-p-3 qa-text-sm qa-shadow-sm",
2056
+ style: { borderColor: `${theme.primary}14` },
2057
+ children: [
2058
+ /* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-2", children: [
2059
+ /* @__PURE__ */ jsx(
2060
+ "span",
2061
+ {
2062
+ 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",
2063
+ style: { background: theme.accent },
2064
+ children: index
2065
+ }
2066
+ ),
2067
+ /* @__PURE__ */ jsx(KindBadge, { target: note.target, t, theme }),
2068
+ /* @__PURE__ */ jsxs("div", { className: "qa-ms-auto qa-flex qa-items-center qa-gap-1.5", children: [
2069
+ !editing && /* @__PURE__ */ jsx(
2070
+ "button",
2071
+ {
2072
+ onClick: startEdit,
2073
+ className: "qa-text-slate-300 qa-hover-text-slate-600 qa-tap-icon",
2074
+ title: t("edit"),
2075
+ "aria-label": t("edit"),
2076
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2077
+ children: /* @__PURE__ */ jsx(Icon, { name: "Pencil", size: 14 })
2078
+ }
2079
+ ),
2080
+ /* @__PURE__ */ jsx(
2081
+ "button",
2082
+ {
2083
+ onClick: () => deleteNote(note.id),
2084
+ className: "qa-text-slate-300 qa-hover-text-red qa-tap-icon",
2085
+ "aria-label": "delete",
2086
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2087
+ children: /* @__PURE__ */ jsx(Icon, { name: "Trash2", size: 16 })
2088
+ }
2089
+ )
2090
+ ] })
2091
+ ] }),
2092
+ editing ? /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2", onPaste, children: [
2093
+ /* @__PURE__ */ jsx(
2094
+ "textarea",
2095
+ {
2096
+ autoFocus: true,
2097
+ value: desc,
2098
+ onChange: (e) => setDesc(e.target.value),
2099
+ rows: 3,
2100
+ 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",
2101
+ style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
2102
+ }
2103
+ ),
2104
+ /* @__PURE__ */ jsxs(
2105
+ "div",
2106
+ {
2107
+ className: "qa-rounded-lg qa-border qa-border-dashed qa-p-2 qa-text-center qa-text-xs",
2108
+ style: { borderColor: `${theme.primary}33` },
2109
+ children: [
2110
+ thumbUrl ? /* @__PURE__ */ jsxs("div", { className: "qa-relative qa-inline-block", children: [
2111
+ /* @__PURE__ */ jsx(
2112
+ "img",
2113
+ {
2114
+ src: thumbUrl,
2115
+ alt: "screenshot",
2116
+ style: { maxHeight: "7rem", borderRadius: "0.25rem" }
2117
+ }
2118
+ ),
2119
+ /* @__PURE__ */ jsx(
2120
+ "button",
2121
+ {
2122
+ onClick: () => setImg(null),
2123
+ className: "qa-absolute qa-rounded-full qa-p-1 qa-text-white qa-tap-icon",
2124
+ title: t("remove_image"),
2125
+ style: {
2126
+ top: "-8px",
2127
+ insetInlineEnd: "-8px",
2128
+ background: theme.primary,
2129
+ border: "none",
2130
+ cursor: "pointer"
2131
+ },
2132
+ children: /* @__PURE__ */ jsx(Icon, { name: "X", size: 12 })
2133
+ }
2134
+ )
2135
+ ] }) : /* @__PURE__ */ jsxs(
2136
+ "button",
2137
+ {
2138
+ onClick: () => fileRef.current?.click(),
2139
+ className: "qa-inline-flex qa-items-center qa-gap-1",
2140
+ style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
2141
+ children: [
2142
+ /* @__PURE__ */ jsx(Icon, { name: "ImagePlus", size: 16 }),
2143
+ t("image_hint")
2144
+ ]
2145
+ }
2146
+ ),
2147
+ /* @__PURE__ */ jsx(
2148
+ "input",
2149
+ {
2150
+ ref: fileRef,
2151
+ type: "file",
2152
+ accept: "image/*",
2153
+ onChange: onFile,
2154
+ className: "qa-hidden"
2155
+ }
2156
+ )
2157
+ ]
2158
+ }
2159
+ ),
2160
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-gap-2", children: [
2161
+ /* @__PURE__ */ jsxs(
2162
+ "button",
2163
+ {
2164
+ onClick: save,
2165
+ disabled: !desc.trim(),
2166
+ 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",
2167
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
2168
+ children: [
2169
+ /* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
2170
+ t("save")
2171
+ ]
2172
+ }
2173
+ ),
2174
+ /* @__PURE__ */ jsx(
2175
+ "button",
2176
+ {
2177
+ onClick: () => setEditing(false),
2178
+ className: "qa-rounded-lg qa-border qa-px-3 qa-text-sm qa-tap",
2179
+ style: {
2180
+ borderColor: `${theme.primary}33`,
2181
+ color: theme.primary,
2182
+ background: "transparent",
2183
+ cursor: "pointer"
2184
+ },
2185
+ children: t("cancel")
2186
+ }
2187
+ )
2188
+ ] })
2189
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
2190
+ /* @__PURE__ */ jsx(
2191
+ "p",
2192
+ {
2193
+ className: "qa-whitespace-pre-wrap qa-break-words",
2194
+ style: { color: theme.ink },
2195
+ children: note.description
2196
+ }
2197
+ ),
2198
+ /* @__PURE__ */ jsxs("div", { className: "qa-mt-1.5 qa-space-y-1.5 qa-text-11 qa-text-slate-500", children: [
2199
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
2200
+ /* @__PURE__ */ jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
2201
+ /* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
2202
+ ] }),
2203
+ note.target && /* @__PURE__ */ jsx(LocationReveal, { target: note.target })
2204
+ ] }),
2205
+ thumbUrl && /* @__PURE__ */ jsx(
2206
+ "img",
2207
+ {
2208
+ src: thumbUrl,
2209
+ alt: "screenshot",
2210
+ className: "qa-mt-2 qa-w-full qa-rounded-lg qa-border",
2211
+ style: { borderColor: `${theme.primary}1a` }
2212
+ }
2213
+ )
2214
+ ] })
2215
+ ]
2216
+ }
2217
+ );
2218
+ }
2219
+ function NoteList() {
2220
+ const { notes, t, theme } = useQa();
2221
+ if (!notes.length) {
2222
+ return /* @__PURE__ */ jsxs(
2223
+ "div",
2224
+ {
2225
+ className: "qa-rounded-xl qa-border qa-border-dashed qa-py-8 qa-text-center qa-text-sm qa-text-slate-400",
2226
+ style: { borderColor: `${theme.primary}22` },
2227
+ children: [
2228
+ t("no_points"),
2229
+ /* @__PURE__ */ jsx("br", {}),
2230
+ t("no_points_hint", { cta: t("capture_cta") })
2231
+ ]
2232
+ }
2233
+ );
2234
+ }
2235
+ 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)) });
2236
+ }
2237
+ function CopyField({ value, ink }) {
2238
+ const [done, setDone] = useState(false);
2239
+ const copy = async () => {
2240
+ if (value === "\u2014") return;
2241
+ if (typeof navigator === "undefined" || !navigator.clipboard) return;
2242
+ try {
2243
+ await navigator.clipboard.writeText(value);
2244
+ setDone(true);
2245
+ setTimeout(() => setDone(false), 1100);
2246
+ } catch {
2247
+ }
2248
+ };
2249
+ return /* @__PURE__ */ jsxs(
2250
+ "button",
2251
+ {
2252
+ onClick: copy,
2253
+ disabled: value === "\u2014",
2254
+ dir: "ltr",
2255
+ 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",
2256
+ style: { background: "transparent", border: "none", cursor: value === "\u2014" ? "default" : "pointer" },
2257
+ children: [
2258
+ /* @__PURE__ */ jsx("span", { style: { color: ink }, children: value }),
2259
+ 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" }))
2260
+ ]
2261
+ }
2262
+ );
2263
+ }
2264
+ function CredentialsSection() {
2265
+ const { loginsUsed, toggleLogin, t, lang, pick: pick2, loginField, credentials, theme } = useQa();
2266
+ const usedCount = credentials.filter((c) => loginsUsed.has(c.role)).length;
2267
+ const field = pick2(loginField);
2268
+ return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2.5", children: [
2269
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2 qa-text-xs", children: [
2270
+ /* @__PURE__ */ jsx("span", { className: "qa-text-slate-500", children: t("login_with", { field }) }),
2271
+ /* @__PURE__ */ jsx(
2272
+ "span",
2273
+ {
2274
+ className: "qa-shrink-0 qa-rounded-full qa-px-2 qa-py-0.5 qa-font-medium qa-text-white",
2275
+ style: { background: theme.sage },
2276
+ children: t("used_count", { n: usedCount, m: credentials.length })
2277
+ }
2278
+ )
2279
+ ] }),
2280
+ credentials.map((c) => {
2281
+ const used = loginsUsed.has(c.role);
2282
+ const label = lang === "ar" && c.roleAr ? c.roleAr : c.role;
2283
+ return /* @__PURE__ */ jsxs(
2284
+ "div",
2285
+ {
2286
+ className: "qa-rounded-xl qa-border qa-p-2.5 qa-shadow-sm qa-transition",
2287
+ style: {
2288
+ borderColor: used ? theme.sage : `${theme.primary}14`,
2289
+ background: used ? `${theme.sage}12` : "#fff"
2290
+ },
2291
+ children: [
2292
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
2293
+ /* @__PURE__ */ jsx(Icon, { name: "CircleUser", size: 16, className: "qa-shrink-0", style: { color: theme.primary } }),
2294
+ /* @__PURE__ */ jsx("span", { className: "qa-text-sm qa-font-semibold", style: { color: theme.ink }, children: label }),
2295
+ c.hint && /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-slate-400", children: pick2(c.hint) }),
2296
+ /* @__PURE__ */ jsxs(
2297
+ "button",
2298
+ {
2299
+ onClick: () => toggleLogin(c.role),
2300
+ disabled: !c.seeded,
2301
+ className: "qa-ms-auto qa-inline-flex qa-items-center qa-gap-1 qa-text-xs",
2302
+ style: {
2303
+ color: used ? theme.sage : "#94a3b8",
2304
+ background: "transparent",
2305
+ border: "none",
2306
+ cursor: c.seeded ? "pointer" : "default"
2307
+ },
2308
+ children: [
2309
+ /* @__PURE__ */ jsx(Icon, { name: used ? "CheckCircle2" : "Circle", size: 16 }),
2310
+ t("used")
2311
+ ]
2312
+ }
2313
+ )
2314
+ ] }),
2315
+ 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: [
2316
+ /* @__PURE__ */ jsx(CopyField, { value: c.login, ink: theme.ink }),
2317
+ /* @__PURE__ */ jsx("span", { className: "qa-text-slate-300", children: "\xB7" }),
2318
+ /* @__PURE__ */ jsx(CopyField, { value: c.password, ink: theme.ink })
2319
+ ] })
2320
+ ]
2321
+ },
2322
+ c.role
2323
+ );
2324
+ })
2325
+ ] });
2326
+ }
2327
+ var keyOf = (id, path) => `${id}::${path}`;
2328
+ function Lane({
2329
+ group,
2330
+ checked,
2331
+ toggle,
2332
+ pick: pick2
2333
+ }) {
2334
+ const { theme, lang } = useQa();
2335
+ const { id, color = theme.primary, steps } = group;
2336
+ const done = steps.filter((s) => checked.has(keyOf(id, s.path))).length;
2337
+ const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
2338
+ const uncoveredRedCount = steps.filter(
2339
+ (s) => s.risk === "red" && !checked.has(keyOf(id, s.path))
2340
+ ).length;
2341
+ return /* @__PURE__ */ jsxs(
2342
+ "div",
2343
+ {
2344
+ className: "qa-rounded-xl qa-border qa-bg-white qa-p-3 qa-shadow-sm",
2345
+ style: { borderColor: `${theme.primary}14` },
2346
+ children: [
2347
+ /* @__PURE__ */ jsxs("div", { className: "qa-mb-2 qa-flex qa-items-center qa-gap-2", children: [
2348
+ /* @__PURE__ */ jsx(
2349
+ "span",
2350
+ {
2351
+ className: "qa-h-2.5 qa-w-2.5 qa-rounded-full",
2352
+ style: { background: color }
2353
+ }
2354
+ ),
2355
+ /* @__PURE__ */ jsx("span", { className: "qa-text-sm qa-font-bold", style: { color: theme.ink }, children: pick2(group.role) }),
2356
+ /* @__PURE__ */ jsxs("span", { className: "qa-ms-auto qa-text-11 qa-font-medium qa-text-slate-400", children: [
2357
+ done,
2358
+ "/",
2359
+ steps.length
2360
+ ] }),
2361
+ uncoveredRedCount > 0 && /* @__PURE__ */ jsx(
2362
+ "span",
2363
+ {
2364
+ className: "qa-rounded qa-px-1 qa-text-10 qa-font-medium",
2365
+ style: { background: "#FEF2F2", color: RISK_COLORS.red },
2366
+ 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)`,
2367
+ children: lang === "ar" ? `\u0623\u062D\u0645\u0631: ${uncoveredRedCount}` : `red: ${uncoveredRedCount}`
2368
+ }
2369
+ )
2370
+ ] }),
2371
+ /* @__PURE__ */ jsx(
2372
+ "div",
2373
+ {
2374
+ className: "qa-mb-3 qa-h-1.5 qa-overflow-hidden qa-rounded-full",
2375
+ style: { background: `${color}22` },
2376
+ children: /* @__PURE__ */ jsx(
2377
+ "div",
2378
+ {
2379
+ className: "qa-h-full qa-rounded-full qa-transition-all",
2380
+ style: { width: `${pct}%`, background: color }
2381
+ }
2382
+ )
2383
+ }
2384
+ ),
2385
+ /* @__PURE__ */ jsxs("ol", { className: "qa-relative qa-ms-1.5", children: [
2386
+ /* @__PURE__ */ jsx(
2387
+ "span",
2388
+ {
2389
+ className: "qa-absolute qa-top-1 qa-bottom-0 qa-w-px",
2390
+ style: { insetInlineStart: "7px", background: `${color}40`, bottom: "4px" }
2391
+ }
2392
+ ),
2393
+ steps.map((s) => {
2394
+ const k = keyOf(id, s.path);
2395
+ const on = checked.has(k);
2396
+ const riskColor = s.risk ? RISK_COLORS[s.risk] : RISK_COLORS.none;
2397
+ 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;
2398
+ return /* @__PURE__ */ jsx("li", { className: "qa-relative qa-mb-2 qa-last-mb-0", children: /* @__PURE__ */ jsxs(
2399
+ "button",
2400
+ {
2401
+ onClick: () => toggle(k),
2402
+ 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",
2403
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2404
+ children: [
2405
+ /* @__PURE__ */ jsx(
2406
+ "span",
2407
+ {
2408
+ 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",
2409
+ style: {
2410
+ borderColor: color,
2411
+ background: on ? color : "#fff",
2412
+ zIndex: 1
2413
+ },
2414
+ children: on && /* @__PURE__ */ jsx(Icon, { name: "Check", size: 10, strokeWidth: 3, className: "qa-text-white" })
2415
+ }
2416
+ ),
2417
+ /* @__PURE__ */ jsxs("span", { className: "qa-min-w-0", children: [
2418
+ /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1", children: [
2419
+ /* @__PURE__ */ jsx(
2420
+ "code",
2421
+ {
2422
+ className: "qa-rounded qa-px-1 qa-text-11 qa-font-semibold qa-dir-ltr",
2423
+ style: {
2424
+ background: `${color}14`,
2425
+ color: theme.ink,
2426
+ textDecoration: on ? "line-through" : "none",
2427
+ opacity: on ? 0.55 : 1
2428
+ },
2429
+ children: s.path
2430
+ }
2431
+ ),
2432
+ /* @__PURE__ */ jsx(
2433
+ "span",
2434
+ {
2435
+ className: "qa-inline-block qa-rounded-full qa-shrink-0",
2436
+ style: {
2437
+ width: "6px",
2438
+ height: "6px",
2439
+ background: riskColor,
2440
+ flexShrink: 0
2441
+ },
2442
+ title: dotTitle
2443
+ }
2444
+ )
2445
+ ] }),
2446
+ /* @__PURE__ */ jsx(
2447
+ "span",
2448
+ {
2449
+ className: "qa-mt-0.5 qa-block qa-text-11 qa-leading-relaxed qa-text-slate-500",
2450
+ style: { opacity: on ? 0.5 : 1 },
2451
+ children: pick2(s.what)
2452
+ }
2453
+ )
2454
+ ] })
2455
+ ]
2456
+ }
2457
+ ) }, s.path);
2458
+ })
2459
+ ] })
2460
+ ]
2461
+ }
2462
+ );
2463
+ }
2464
+ function GuideSection() {
2465
+ const { guideChecked, toggleGuide, t, journey, pick: pick2, theme, lang } = useQa();
2466
+ const all = journey.flatMap((g) => g.steps.map((s) => keyOf(g.id, s.path)));
2467
+ const done = all.filter((k) => guideChecked.has(k)).length;
2468
+ const pct = all.length > 0 ? Math.round(done / all.length * 100) : 0;
2469
+ const coverage = computeCoverage(journey, guideChecked);
2470
+ return /* @__PURE__ */ jsxs("div", { className: "qa-space-y-3", children: [
2471
+ /* @__PURE__ */ jsxs(
2472
+ "div",
2473
+ {
2474
+ className: "qa-rounded-xl qa-p-3 qa-text-white qa-shadow-sm",
2475
+ style: { backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})` },
2476
+ children: [
2477
+ coverage.red.total > 0 && /* @__PURE__ */ jsxs("div", { className: "qa-mb-1 qa-flex qa-items-center qa-gap-1.5 qa-text-11", children: [
2478
+ /* @__PURE__ */ jsx(
2479
+ "span",
2480
+ {
2481
+ className: "qa-rounded qa-px-1 qa-font-bold",
2482
+ style: { background: "rgba(0,0,0,0.25)" },
2483
+ children: lang === "ar" ? "\u0623\u062D\u0645\u0631" : "RED"
2484
+ }
2485
+ ),
2486
+ /* @__PURE__ */ jsxs("span", { className: "qa-dir-ltr qa-font-semibold", children: [
2487
+ coverage.red.covered,
2488
+ "/",
2489
+ coverage.red.total,
2490
+ " ",
2491
+ lang === "ar" ? "\u0645\u063A\u0637\u0649" : "covered"
2492
+ ] })
2493
+ ] }),
2494
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-justify-between qa-text-sm qa-font-semibold", children: [
2495
+ /* @__PURE__ */ jsx("span", { children: t("journey_title") }),
2496
+ /* @__PURE__ */ jsxs("span", { className: "qa-dir-ltr", children: [
2497
+ done,
2498
+ "/",
2499
+ all.length,
2500
+ " \xB7 ",
2501
+ pct,
2502
+ "%"
2503
+ ] })
2504
+ ] }),
2505
+ /* @__PURE__ */ jsx("div", { className: "qa-mt-2 qa-h-2 qa-overflow-hidden qa-rounded-full qa-bg-white-25", children: /* @__PURE__ */ jsx(
2506
+ "div",
2507
+ {
2508
+ className: "qa-h-full qa-rounded-full qa-bg-white qa-transition-all",
2509
+ style: { width: `${pct}%` }
2510
+ }
2511
+ ) })
2512
+ ]
2513
+ }
2514
+ ),
2515
+ journey.map((g) => /* @__PURE__ */ jsx(
2516
+ Lane,
2517
+ {
2518
+ group: g,
2519
+ checked: guideChecked,
2520
+ toggle: toggleGuide,
2521
+ pick: pick2
2522
+ },
2523
+ g.id
2524
+ )),
2525
+ journey.length === 0 && /* @__PURE__ */ jsx("p", { className: "qa-py-8 qa-text-center qa-text-sm qa-text-slate-400", children: t("tab_guide") })
2526
+ ] });
2527
+ }
2528
+ var TABS = [
2529
+ { key: "notes", labelKey: "tab_notes", icon: "StickyNote" },
2530
+ { key: "logins", labelKey: "tab_logins", icon: "KeyRound" },
2531
+ { key: "guide", labelKey: "tab_guide", icon: "Map" }
2532
+ ];
2533
+ function todayName() {
2534
+ return `qa-notes-${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
2535
+ }
2536
+ function panelReducer(state, action) {
2537
+ switch (action.type) {
2538
+ case "open":
2539
+ if (state === "hidden" || state === "exiting") return "entering";
2540
+ return state;
2541
+ case "close":
2542
+ if (state === "visible" || state === "entering") return "exiting";
2543
+ return state;
2544
+ case "done":
2545
+ if (state === "entering") return "visible";
2546
+ if (state === "exiting") return "hidden";
2547
+ return state;
2548
+ }
2549
+ }
2550
+ function QaPanel() {
2551
+ const {
2552
+ isOpen,
2553
+ activeTab,
2554
+ setActiveTab,
2555
+ notes,
2556
+ exportZip,
2557
+ isExporting,
2558
+ clearAll,
2559
+ t,
2560
+ lang,
2561
+ setLang,
2562
+ dir,
2563
+ brand,
2564
+ theme,
2565
+ journey,
2566
+ guideChecked
2567
+ } = useQa();
2568
+ const [confirmClear, setConfirmClear] = useState(false);
2569
+ const [naming, setNaming] = useState(false);
2570
+ const [filename, setFilename] = useState("");
2571
+ const [phase, dispatch] = useReducer(panelReducer, "hidden");
2572
+ const [showIn, setShowIn] = useState(false);
2573
+ useEffect(() => {
2574
+ if (isOpen) dispatch({ type: "open" });
2575
+ else dispatch({ type: "close" });
2576
+ }, [isOpen]);
2577
+ useEffect(() => {
2578
+ if (phase === "entering") {
2579
+ const id = requestAnimationFrame(() => setShowIn(true));
2580
+ return () => cancelAnimationFrame(id);
2581
+ }
2582
+ if (phase === "exiting") {
2583
+ setShowIn(false);
2584
+ }
2585
+ if (phase === "hidden") {
2586
+ setShowIn(false);
2587
+ }
2588
+ if (phase === "visible") {
2589
+ setShowIn(true);
2590
+ }
2591
+ return void 0;
2592
+ }, [phase]);
2593
+ const handleTransitionEnd = useCallback((e) => {
2594
+ if (e.target !== e.currentTarget) return;
2595
+ if (e.propertyName !== "opacity") return;
2596
+ dispatch({ type: "done" });
2597
+ }, []);
2598
+ const [isIpadLandscape, setIsIpadLandscape] = useState(false);
2599
+ useEffect(() => {
2600
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
2601
+ return void 0;
2602
+ }
2603
+ const mql = window.matchMedia(
2604
+ "(pointer: coarse) and (min-width: 768px) and (orientation: landscape)"
2605
+ );
2606
+ setIsIpadLandscape(mql.matches);
2607
+ const handleChange = (e) => setIsIpadLandscape(e.matches);
2608
+ if (typeof mql.addEventListener === "function") {
2609
+ mql.addEventListener("change", handleChange);
2610
+ return () => mql.removeEventListener("change", handleChange);
2611
+ }
2612
+ mql.addListener(handleChange);
2613
+ return () => mql.removeListener(handleChange);
2614
+ }, []);
2615
+ if (phase === "hidden") return null;
2616
+ const openNaming = () => {
2617
+ setFilename(todayName());
2618
+ setNaming(true);
2619
+ };
2620
+ const doExport = () => {
2621
+ setNaming(false);
2622
+ void exportZip(filename);
2623
+ };
2624
+ const namingCoverage = naming ? computeCoverage(journey, guideChecked) : null;
2625
+ return /* @__PURE__ */ jsxs(
2626
+ "div",
2627
+ {
2628
+ "data-qa-overlay": "true",
2629
+ dir,
2630
+ onTransitionEnd: handleTransitionEnd,
2631
+ 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" : ""}`,
2632
+ style: {
2633
+ // Floating popover position (default). Fully overridden below when
2634
+ // docked as an iPad-landscape side-sheet.
2635
+ left: isIpadLandscape ? "auto" : "calc(1rem + env(safe-area-inset-left))",
2636
+ right: isIpadLandscape ? "0" : void 0,
2637
+ top: isIpadLandscape ? "0" : void 0,
2638
+ bottom: isIpadLandscape ? "0" : dir === "rtl" ? "calc(9rem + env(safe-area-inset-bottom))" : "calc(8.75rem + env(safe-area-inset-bottom))",
2639
+ height: isIpadLandscape ? "100dvh" : void 0,
2640
+ width: isIpadLandscape ? "min(92vw, 420px)" : void 0,
2641
+ // qa-max-h-74vh (class) would otherwise cap the sheet well short of
2642
+ // full height — neutralize it only in the docked sheet variant.
2643
+ maxHeight: isIpadLandscape ? "none" : void 0,
2644
+ borderRadius: isIpadLandscape ? 0 : void 0,
2645
+ background: theme.surface,
2646
+ borderColor: `${theme.primary}22`,
2647
+ fontFamily: lang === "ar" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif",
2648
+ zIndex: 9990
2649
+ },
2650
+ children: [
2651
+ /* @__PURE__ */ jsxs(
2652
+ "div",
2653
+ {
2654
+ className: "qa-flex qa-items-center qa-gap-2 qa-px-4 qa-py-3 qa-text-white",
2655
+ style: { backgroundImage: `linear-gradient(135deg, ${theme.primary}, ${theme.accent})` },
2656
+ children: [
2657
+ /* @__PURE__ */ jsx(
2658
+ "span",
2659
+ {
2660
+ className: "qa-text-sm qa-font-bold qa-dir-ltr",
2661
+ style: { fontFamily: "'Cormorant Garamond', Georgia, serif", letterSpacing: "-0.02em" },
2662
+ dir: "ltr",
2663
+ children: brand.label
2664
+ }
2665
+ ),
2666
+ /* @__PURE__ */ jsx("span", { className: "qa-rounded-full qa-bg-white-25 qa-px-2 qa-text-xs qa-font-medium", children: notes.length }),
2667
+ /* @__PURE__ */ jsx(
2668
+ "div",
2669
+ {
2670
+ className: "qa-ms-auto qa-flex qa-items-center qa-overflow-hidden qa-rounded-lg qa-text-11 qa-font-semibold",
2671
+ dir: "ltr",
2672
+ style: { background: "rgba(255,255,255,0.15)" },
2673
+ children: ["en", "ar"].map((l) => /* @__PURE__ */ jsx(
2674
+ "button",
2675
+ {
2676
+ onClick: () => setLang(l),
2677
+ className: "qa-px-2 qa-py-1 qa-transition qa-tap",
2678
+ style: {
2679
+ background: lang === l ? "#ffffff" : "transparent",
2680
+ color: lang === l ? theme.primary : "#fff",
2681
+ border: "none",
2682
+ cursor: "pointer"
2683
+ },
2684
+ children: l === "en" ? "EN" : "\u0639"
2685
+ },
2686
+ l
2687
+ ))
2688
+ }
2689
+ ),
2690
+ /* @__PURE__ */ jsxs(
2691
+ "button",
2692
+ {
2693
+ onClick: openNaming,
2694
+ disabled: !notes.length || isExporting,
2695
+ title: t("export"),
2696
+ 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",
2697
+ style: { background: "rgba(255,255,255,0.15)", color: "#fff", border: "none", cursor: "pointer" },
2698
+ children: [
2699
+ /* @__PURE__ */ jsx(
2700
+ Icon,
2701
+ {
2702
+ name: isExporting ? "Loader2" : "Download",
2703
+ size: 14,
2704
+ className: isExporting ? "qa-animate-spin" : void 0
2705
+ }
2706
+ ),
2707
+ t("export")
2708
+ ]
2709
+ }
2710
+ )
2711
+ ]
2712
+ }
2713
+ ),
2714
+ /* @__PURE__ */ jsx(
2715
+ TabsBar,
2716
+ {
2717
+ activeTab,
2718
+ setActiveTab,
2719
+ t,
2720
+ theme
2721
+ }
2722
+ ),
2723
+ /* @__PURE__ */ jsx("div", { className: "qa-h-px", style: { background: `${theme.primary}14` } }),
2724
+ /* @__PURE__ */ jsxs(
2725
+ "div",
2726
+ {
2727
+ className: "qa-flex-1 qa-space-y-3 qa-overflow-y-auto qa-p-3",
2728
+ style: { background: `${theme.cream}80` },
2729
+ children: [
2730
+ activeTab === "notes" && /* @__PURE__ */ jsxs(Fragment, { children: [
2731
+ /* @__PURE__ */ jsx(NoteEditor, {}),
2732
+ /* @__PURE__ */ jsx(NoteList, {}),
2733
+ 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: [
2734
+ t("delete_all_q", { n: notes.length }),
2735
+ " ",
2736
+ /* @__PURE__ */ jsx(
2737
+ "button",
2738
+ {
2739
+ onClick: () => {
2740
+ void clearAll();
2741
+ setConfirmClear(false);
2742
+ },
2743
+ className: "qa-font-semibold qa-text-red-600 qa-tap",
2744
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2745
+ children: t("yes")
2746
+ }
2747
+ ),
2748
+ " / ",
2749
+ /* @__PURE__ */ jsx(
2750
+ "button",
2751
+ {
2752
+ onClick: () => setConfirmClear(false),
2753
+ className: "qa-tap",
2754
+ style: { color: theme.primary, background: "transparent", border: "none", cursor: "pointer" },
2755
+ children: t("no")
2756
+ }
2757
+ )
2758
+ ] }) : /* @__PURE__ */ jsxs(
2759
+ "button",
2760
+ {
2761
+ onClick: () => setConfirmClear(true),
2762
+ className: "qa-inline-flex qa-items-center qa-gap-1 qa-text-xs qa-text-slate-400 qa-hover-text-red",
2763
+ style: { background: "transparent", border: "none", cursor: "pointer" },
2764
+ children: [
2765
+ /* @__PURE__ */ jsx(Icon, { name: "Trash", size: 12 }),
2766
+ t("clear_all")
2767
+ ]
2768
+ }
2769
+ ) })
2770
+ ] }),
2771
+ activeTab === "logins" && /* @__PURE__ */ jsx(CredentialsSection, {}),
2772
+ activeTab === "guide" && /* @__PURE__ */ jsx(GuideSection, {})
2773
+ ]
2774
+ }
2775
+ ),
2776
+ naming && /* @__PURE__ */ jsx(
2777
+ "div",
2778
+ {
2779
+ className: "qa-absolute qa-inset-0 qa-z-50 qa-flex qa-items-center qa-justify-center qa-p-5",
2780
+ style: { background: "rgba(58,42,46,0.45)" },
2781
+ children: /* @__PURE__ */ jsxs(
2782
+ "div",
2783
+ {
2784
+ className: "qa-w-full qa-rounded-xl qa-border qa-bg-white qa-p-4 qa-shadow-2xl",
2785
+ style: { borderColor: `${theme.primary}22` },
2786
+ children: [
2787
+ /* @__PURE__ */ jsx(
2788
+ "p",
2789
+ {
2790
+ className: "qa-mb-2 qa-text-sm qa-font-semibold",
2791
+ style: { color: theme.ink },
2792
+ children: t("export_name_title")
2793
+ }
2794
+ ),
2795
+ /* @__PURE__ */ jsxs(
2796
+ "div",
2797
+ {
2798
+ className: "qa-flex qa-items-center qa-rounded-lg qa-border qa-dir-ltr",
2799
+ style: { borderColor: `${theme.primary}33` },
2800
+ children: [
2801
+ /* @__PURE__ */ jsx(
2802
+ "input",
2803
+ {
2804
+ autoFocus: true,
2805
+ value: filename,
2806
+ onChange: (e) => setFilename(e.target.value),
2807
+ onKeyDown: (e) => {
2808
+ if (e.key === "Enter") doExport();
2809
+ if (e.key === "Escape") setNaming(false);
2810
+ },
2811
+ placeholder: t("export_name_placeholder"),
2812
+ className: "qa-min-w-0 qa-flex-1 qa-rounded-lg qa-px-2 qa-py-1.5 qa-text-sm qa-border-0",
2813
+ style: { outline: "none", background: "transparent", color: "inherit" }
2814
+ }
2815
+ ),
2816
+ /* @__PURE__ */ jsx("span", { className: "qa-px-2 qa-text-xs qa-text-slate-400", children: ".zip" })
2817
+ ]
2818
+ }
2819
+ ),
2820
+ namingCoverage && namingCoverage.uncoveredReds.length > 0 && /* @__PURE__ */ jsx(
2821
+ "p",
2822
+ {
2823
+ className: "qa-mt-2 qa-text-11",
2824
+ style: { color: "#F59E0B" },
2825
+ 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?`
2826
+ }
2827
+ ),
2828
+ /* @__PURE__ */ jsxs("div", { className: "qa-mt-3 qa-flex qa-gap-2", children: [
2829
+ /* @__PURE__ */ jsxs(
2830
+ "button",
2831
+ {
2832
+ onClick: doExport,
2833
+ 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",
2834
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
2835
+ children: [
2836
+ /* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
2837
+ t("export")
2838
+ ]
2839
+ }
2840
+ ),
2841
+ /* @__PURE__ */ jsxs(
2842
+ "button",
2843
+ {
2844
+ onClick: () => setNaming(false),
2845
+ 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",
2846
+ style: {
2847
+ borderColor: `${theme.primary}33`,
2848
+ color: theme.primary,
2849
+ background: "transparent",
2850
+ cursor: "pointer"
2851
+ },
2852
+ children: [
2853
+ /* @__PURE__ */ jsx(Icon, { name: "X", size: 16 }),
2854
+ t("cancel")
2855
+ ]
2856
+ }
2857
+ )
2858
+ ] })
2859
+ ]
2860
+ }
2861
+ )
2862
+ }
2863
+ )
2864
+ ]
2865
+ }
2866
+ );
2867
+ }
2868
+ function TabsBar({
2869
+ activeTab,
2870
+ setActiveTab,
2871
+ t,
2872
+ theme
2873
+ }) {
2874
+ const tabRefs = useRef([]);
2875
+ const barRef = useRef(null);
2876
+ const containerRef = useRef(null);
2877
+ const reposition = useCallback(() => {
2878
+ const idx = TABS.findIndex((tab) => tab.key === activeTab);
2879
+ const btn = tabRefs.current[idx];
2880
+ const bar = barRef.current;
2881
+ if (!btn || !bar) return;
2882
+ bar.style.left = `${btn.offsetLeft + 8}px`;
2883
+ bar.style.width = `${Math.max(0, btn.offsetWidth - 16)}px`;
2884
+ }, [activeTab]);
2885
+ useLayoutEffect(() => {
2886
+ reposition();
2887
+ }, [reposition]);
2888
+ useEffect(() => {
2889
+ const container = containerRef.current;
2890
+ if (!container || typeof ResizeObserver === "undefined") return;
2891
+ const ro = new ResizeObserver(reposition);
2892
+ ro.observe(container);
2893
+ return () => ro.disconnect();
2894
+ }, [reposition]);
2895
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "qa-flex qa-px-2 qa-pt-2 qa-relative", children: [
2896
+ TABS.map((tab, i) => {
2897
+ const on = activeTab === tab.key;
2898
+ return /* @__PURE__ */ jsxs(
2899
+ "button",
2900
+ {
2901
+ ref: (el) => {
2902
+ tabRefs.current[i] = el;
2903
+ },
2904
+ onClick: () => setActiveTab(tab.key),
2905
+ 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",
2906
+ style: {
2907
+ color: on ? theme.primary : "#94a3b8",
2908
+ background: "transparent",
2909
+ border: "none",
2910
+ cursor: "pointer"
2911
+ },
2912
+ children: [
2913
+ /* @__PURE__ */ jsx(Icon, { name: tab.icon, size: 16 }),
2914
+ t(tab.labelKey)
2915
+ ]
2916
+ },
2917
+ tab.key
2918
+ );
2919
+ }),
2920
+ /* @__PURE__ */ jsx(
2921
+ "span",
2922
+ {
2923
+ ref: barRef,
2924
+ className: "qa-tab-indicator",
2925
+ style: { background: theme.accent },
2926
+ "aria-hidden": "true"
2927
+ }
2928
+ )
2929
+ ] });
2930
+ }
2931
+
2932
+ // src/lib/capture.ts
2933
+ function toBlob(canvas) {
2934
+ return new Promise((resolve) => {
2935
+ if (canvas.toBlob) {
2936
+ canvas.toBlob((b) => resolve(b), "image/png");
2937
+ } else {
2938
+ const dataUrl = canvas.toDataURL("image/png");
2939
+ const bin = atob(dataUrl.split(",")[1]);
2940
+ const arr = new Uint8Array(bin.length);
2941
+ for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
2942
+ resolve(new Blob([arr], { type: "image/png" }));
2943
+ }
2944
+ });
2945
+ }
2946
+ async function captureRegion(rect, scroll) {
2947
+ if (typeof document === "undefined" || typeof window === "undefined") return null;
2948
+ if (!rect || rect.width < 2 || rect.height < 2) return null;
2949
+ const sx = scroll?.x ?? window.scrollX;
2950
+ const sy = scroll?.y ?? window.scrollY;
2951
+ try {
2952
+ const { default: html2canvas } = await import('html2canvas');
2953
+ const scale = Math.min(window.devicePixelRatio || 1, 2);
2954
+ const canvas = await html2canvas(document.body, {
2955
+ x: sx + rect.left,
2956
+ y: sy + rect.top,
2957
+ width: rect.width,
2958
+ height: rect.height,
2959
+ scale,
2960
+ useCORS: true,
2961
+ allowTaint: true,
2962
+ backgroundColor: null,
2963
+ logging: false,
2964
+ scrollX: sx,
2965
+ scrollY: sy,
2966
+ // Viewport-only clone (not the full document) — see iOS canvas-cap
2967
+ // rationale above. Keeps the offscreen render surface ~viewport*scale.
2968
+ windowWidth: window.innerWidth,
2969
+ windowHeight: window.innerHeight,
2970
+ ignoreElements: (el) => el.nodeType === 1 && typeof el.hasAttribute === "function" && el.hasAttribute("data-qa-overlay")
2971
+ });
2972
+ return await toBlob(canvas);
2973
+ } catch (err) {
2974
+ console.warn("[QA] region capture failed:", err);
2975
+ return null;
2976
+ }
2977
+ }
2978
+
2979
+ // src/lib/selector.ts
2980
+ function isCleanId(id) {
2981
+ return !!id && /^[a-zA-Z][\w-]*$/.test(id) && id.length <= 40;
2982
+ }
2983
+ function esc(value) {
2984
+ return typeof CSS !== "undefined" && CSS.escape ? CSS.escape(value) : value;
2985
+ }
2986
+ function nthOfTypePath(el, maxDepth = 6) {
2987
+ if (typeof document === "undefined" || !document.body) return "";
2988
+ const parts = [];
2989
+ let node = el;
2990
+ let depth = 0;
2991
+ while (node && node.nodeType === 1 && node !== document.body && depth < maxDepth) {
2992
+ const current = node;
2993
+ const tag = current.tagName.toLowerCase();
2994
+ const parent = current.parentElement;
2995
+ if (!parent) {
2996
+ parts.unshift(tag);
2997
+ break;
2998
+ }
2999
+ const sameTag = Array.from(parent.children).filter(
3000
+ (c) => c.tagName === current.tagName
3001
+ );
3002
+ if (sameTag.length === 1) {
3003
+ parts.unshift(tag);
3004
+ } else {
3005
+ parts.unshift(`${tag}:nth-of-type(${sameTag.indexOf(current) + 1})`);
3006
+ }
3007
+ node = parent;
3008
+ depth++;
3009
+ }
3010
+ return parts.join(" > ");
3011
+ }
3012
+ function getStableSelector(el) {
3013
+ if (typeof document === "undefined") return "";
3014
+ if (!el || el.nodeType !== 1) return "";
3015
+ const tag = el.tagName.toLowerCase();
3016
+ const htmlEl = el;
3017
+ if (isCleanId(htmlEl.id)) return `#${esc(htmlEl.id)}`;
3018
+ for (const attr of ["data-testid", "data-test", "data-cy", "data-id", "data-key"]) {
3019
+ const val = el.getAttribute(attr);
3020
+ if (val) return `[${attr}="${esc(val)}"]`;
3021
+ }
3022
+ if (["button", "a", "input", "select", "textarea"].includes(tag)) {
3023
+ const label = el.getAttribute("aria-label");
3024
+ if (label) return `${tag}[aria-label="${esc(label)}"]`;
3025
+ }
3026
+ const name = el.getAttribute("name");
3027
+ if (name && ["input", "select", "textarea"].includes(tag)) {
3028
+ return `${tag}[name="${esc(name)}"]`;
3029
+ }
3030
+ return nthOfTypePath(el);
3031
+ }
3032
+ function isCoarsePointer() {
3033
+ if (typeof window === "undefined") return false;
3034
+ try {
3035
+ if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return true;
3036
+ } catch {
3037
+ }
3038
+ return typeof navigator !== "undefined" && (navigator.maxTouchPoints || 0) > 0;
3039
+ }
3040
+ function useCoarsePointer() {
3041
+ const [coarse, setCoarse] = useState(() => isCoarsePointer());
3042
+ useEffect(() => {
3043
+ if (typeof window === "undefined" || !window.matchMedia) return;
3044
+ const mq = window.matchMedia("(pointer: coarse)");
3045
+ const on = () => setCoarse(isCoarsePointer());
3046
+ if (mq.addEventListener) mq.addEventListener("change", on);
3047
+ else if (mq.addListener) mq.addListener(on);
3048
+ return () => {
3049
+ if (mq.removeEventListener) mq.removeEventListener("change", on);
3050
+ else if (mq.removeListener) mq.removeListener(on);
3051
+ };
3052
+ }, []);
3053
+ return coarse;
3054
+ }
3055
+
3056
+ // src/lib/scrollLock.ts
3057
+ var locked = false;
3058
+ var prevHtmlOverflow = "";
3059
+ var prevBodyOverflow = "";
3060
+ function lockPageScroll() {
3061
+ if (typeof document === "undefined" || locked) return;
3062
+ const html = document.documentElement;
3063
+ const body = document.body;
3064
+ prevHtmlOverflow = html.style.overflow;
3065
+ prevBodyOverflow = body ? body.style.overflow : "";
3066
+ html.style.overflow = "hidden";
3067
+ if (body) body.style.overflow = "hidden";
3068
+ locked = true;
3069
+ }
3070
+ function unlockPageScroll() {
3071
+ if (typeof document === "undefined" || !locked) return;
3072
+ document.documentElement.style.overflow = prevHtmlOverflow;
3073
+ if (document.body) document.body.style.overflow = prevBodyOverflow;
3074
+ locked = false;
3075
+ }
3076
+ var DRAG_THRESHOLD = 6;
3077
+ var TOUCH_DRAG_THRESHOLD = 12;
3078
+ var MIN_REGION_SIZE = 8;
3079
+ var REGION_HANDLES = [
3080
+ { edge: "nw", top: "0%", left: "0%", cursor: "nwse-resize" },
3081
+ { edge: "n", top: "0%", left: "50%", cursor: "ns-resize" },
3082
+ { edge: "ne", top: "0%", left: "100%", cursor: "nesw-resize" },
3083
+ { edge: "w", top: "50%", left: "0%", cursor: "ew-resize" },
3084
+ { edge: "e", top: "50%", left: "100%", cursor: "ew-resize" },
3085
+ { edge: "sw", top: "100%", left: "0%", cursor: "nesw-resize" },
3086
+ { edge: "s", top: "100%", left: "50%", cursor: "ns-resize" },
3087
+ { edge: "se", top: "100%", left: "100%", cursor: "nwse-resize" }
3088
+ ];
3089
+ function CaptureMode() {
3090
+ const { addNote, endCapture, t, dir, theme } = useQa();
3091
+ const coarse = useCoarsePointer();
3092
+ const layerRef = useRef(null);
3093
+ const [phase, setPhase] = useState("selecting");
3094
+ const [hover, setHover] = useState(null);
3095
+ const [drag, setDrag] = useState(null);
3096
+ const dragRef = useRef(null);
3097
+ const [selection, setSelection] = useState(null);
3098
+ const [candidate, setCandidate] = useState(null);
3099
+ const [regionMode, setRegionMode] = useState(false);
3100
+ const [shot, setShot] = useState(null);
3101
+ const [shotUrl, setShotUrl] = useState(null);
3102
+ const [capturing, setCapturing] = useState(false);
3103
+ const [description, setDescription] = useState("");
3104
+ const taRef = useRef(null);
3105
+ const activePointerId = useRef(null);
3106
+ const pointerKind = useRef("mouse");
3107
+ const scrollSnap = useRef({ x: 0, y: 0 });
3108
+ const handleDragRef = useRef(null);
3109
+ const [cardIn, setCardIn] = useState(false);
3110
+ const elementUnder = useCallback((x, y) => {
3111
+ const layer = layerRef.current;
3112
+ if (!layer) return null;
3113
+ const prev = layer.style.pointerEvents;
3114
+ layer.style.pointerEvents = "none";
3115
+ const el = document.elementFromPoint(x, y);
3116
+ layer.style.pointerEvents = prev;
3117
+ if (!el || el.closest?.("[data-qa-overlay]")) return null;
3118
+ return el;
3119
+ }, []);
3120
+ const beginAnnotation = useCallback(async (sel) => {
3121
+ setSelection(sel);
3122
+ setCandidate(null);
3123
+ setHover(null);
3124
+ setRegionMode(false);
3125
+ setPhase("annotating");
3126
+ setCardIn(false);
3127
+ setCapturing(true);
3128
+ lockPageScroll();
3129
+ try {
3130
+ const blob = await captureRegion(sel.rect, scrollSnap.current);
3131
+ setShot(blob);
3132
+ setShotUrl((old) => {
3133
+ if (old) URL.revokeObjectURL(old);
3134
+ return blob ? URL.createObjectURL(blob) : null;
3135
+ });
3136
+ } finally {
3137
+ unlockPageScroll();
3138
+ setCapturing(false);
3139
+ }
3140
+ }, []);
3141
+ useEffect(() => {
3142
+ if (phase !== "annotating") {
3143
+ setCardIn(false);
3144
+ return;
3145
+ }
3146
+ const id = requestAnimationFrame(() => setCardIn(true));
3147
+ return () => cancelAnimationFrame(id);
3148
+ }, [phase]);
3149
+ const onPointerDown = (e) => {
3150
+ if (phase !== "selecting") return;
3151
+ pointerKind.current = e.pointerType;
3152
+ if (e.pointerType === "mouse" && e.button !== 0) return;
3153
+ if (activePointerId.current !== null) return;
3154
+ activePointerId.current = e.pointerId;
3155
+ try {
3156
+ e.currentTarget.setPointerCapture(e.pointerId);
3157
+ } catch {
3158
+ }
3159
+ if (e.pointerType === "mouse" || coarse && regionMode) {
3160
+ dragRef.current = { x0: e.clientX, y0: e.clientY, rect: null };
3161
+ }
3162
+ };
3163
+ const onPointerMove = (e) => {
3164
+ if (phase !== "selecting") return;
3165
+ if (dragRef.current && activePointerId.current === e.pointerId) {
3166
+ const d = dragRef.current;
3167
+ const rect = {
3168
+ left: Math.min(d.x0, e.clientX),
3169
+ top: Math.min(d.y0, e.clientY),
3170
+ width: Math.abs(e.clientX - d.x0),
3171
+ height: Math.abs(e.clientY - d.y0)
3172
+ };
3173
+ setDrag({ ...d, rect });
3174
+ return;
3175
+ }
3176
+ if (!coarse) {
3177
+ const el = elementUnder(e.clientX, e.clientY);
3178
+ if (!el) {
3179
+ setHover(null);
3180
+ return;
3181
+ }
3182
+ const r = el.getBoundingClientRect();
3183
+ setHover({
3184
+ rect: { top: r.top, left: r.left, width: r.width, height: r.height },
3185
+ selector: getStableSelector(el)
3186
+ });
3187
+ }
3188
+ };
3189
+ const onPointerUp = (e) => {
3190
+ if (phase !== "selecting") return;
3191
+ if (activePointerId.current !== e.pointerId) return;
3192
+ try {
3193
+ e.currentTarget.releasePointerCapture(e.pointerId);
3194
+ } catch {
3195
+ }
3196
+ activePointerId.current = null;
3197
+ const d = dragRef.current;
3198
+ dragRef.current = null;
3199
+ const threshold = pointerKind.current === "mouse" ? DRAG_THRESHOLD : TOUCH_DRAG_THRESHOLD;
3200
+ const moved = d !== null && Math.hypot(e.clientX - d.x0, e.clientY - d.y0) > threshold;
3201
+ scrollSnap.current = { x: window.scrollX, y: window.scrollY };
3202
+ if (moved && d) {
3203
+ const rect = {
3204
+ left: Math.min(d.x0, e.clientX),
3205
+ top: Math.min(d.y0, e.clientY),
3206
+ width: Math.abs(e.clientX - d.x0),
3207
+ height: Math.abs(e.clientY - d.y0)
3208
+ };
3209
+ setDrag(null);
3210
+ const sel = { kind: "region", rect };
3211
+ if (coarse) {
3212
+ setCandidate(sel);
3213
+ setPhase("confirming");
3214
+ } else {
3215
+ void beginAnnotation(sel);
3216
+ }
3217
+ } else {
3218
+ const el = elementUnder(e.clientX, e.clientY);
3219
+ if (!el) return;
3220
+ const r = el.getBoundingClientRect();
3221
+ const sel = {
3222
+ kind: "element",
3223
+ rect: { top: r.top, left: r.left, width: r.width, height: r.height },
3224
+ selector: getStableSelector(el),
3225
+ text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
3226
+ tagName: el.tagName.toLowerCase()
3227
+ };
3228
+ if (coarse) {
3229
+ setCandidate(sel);
3230
+ setHover({ rect: sel.rect, selector: sel.selector || "" });
3231
+ setPhase("confirming");
3232
+ } else {
3233
+ void beginAnnotation(sel);
3234
+ }
3235
+ }
3236
+ };
3237
+ const onPointerCancel = (e) => {
3238
+ if (activePointerId.current === e.pointerId) {
3239
+ activePointerId.current = null;
3240
+ dragRef.current = null;
3241
+ setDrag(null);
3242
+ }
3243
+ };
3244
+ const onHandlePointerDown = useCallback(
3245
+ (edge) => (e) => {
3246
+ e.stopPropagation();
3247
+ if (!candidate) return;
3248
+ try {
3249
+ e.currentTarget.setPointerCapture(e.pointerId);
3250
+ } catch {
3251
+ }
3252
+ handleDragRef.current = {
3253
+ edge,
3254
+ pointerId: e.pointerId,
3255
+ startRect: { ...candidate.rect },
3256
+ startX: e.clientX,
3257
+ startY: e.clientY
3258
+ };
3259
+ },
3260
+ [candidate]
3261
+ );
3262
+ const onHandlePointerMove = useCallback((e) => {
3263
+ const hd = handleDragRef.current;
3264
+ if (!hd || hd.pointerId !== e.pointerId) return;
3265
+ e.stopPropagation();
3266
+ const dx = e.clientX - hd.startX;
3267
+ const dy = e.clientY - hd.startY;
3268
+ const { startRect, edge } = hd;
3269
+ let { top, left, width, height } = startRect;
3270
+ if (edge === "move") {
3271
+ left = startRect.left + dx;
3272
+ top = startRect.top + dy;
3273
+ } else {
3274
+ if (edge.includes("e")) width = Math.max(MIN_REGION_SIZE, startRect.width + dx);
3275
+ if (edge.includes("w")) {
3276
+ width = Math.max(MIN_REGION_SIZE, startRect.width - dx);
3277
+ left = startRect.left + (startRect.width - width);
3278
+ }
3279
+ if (edge.includes("s")) height = Math.max(MIN_REGION_SIZE, startRect.height + dy);
3280
+ if (edge.includes("n")) {
3281
+ height = Math.max(MIN_REGION_SIZE, startRect.height - dy);
3282
+ top = startRect.top + (startRect.height - height);
3283
+ }
3284
+ }
3285
+ setCandidate((prev) => prev ? { ...prev, rect: { top, left, width, height } } : prev);
3286
+ }, []);
3287
+ const onHandlePointerUp = useCallback((e) => {
3288
+ const hd = handleDragRef.current;
3289
+ if (!hd || hd.pointerId !== e.pointerId) return;
3290
+ e.stopPropagation();
3291
+ try {
3292
+ e.currentTarget.releasePointerCapture(e.pointerId);
3293
+ } catch {
3294
+ }
3295
+ handleDragRef.current = null;
3296
+ }, []);
3297
+ useEffect(() => {
3298
+ const onKey = (e) => {
3299
+ if (e.key === "Escape") {
3300
+ e.preventDefault();
3301
+ endCapture();
3302
+ }
3303
+ };
3304
+ document.addEventListener("keydown", onKey, true);
3305
+ return () => document.removeEventListener("keydown", onKey, true);
3306
+ }, [endCapture]);
3307
+ useEffect(() => {
3308
+ if (phase === "annotating" && taRef.current) taRef.current.focus();
3309
+ }, [phase]);
3310
+ useEffect(() => () => {
3311
+ if (shotUrl) URL.revokeObjectURL(shotUrl);
3312
+ }, [shotUrl]);
3313
+ useEffect(() => () => {
3314
+ unlockPageScroll();
3315
+ }, []);
3316
+ const save = async () => {
3317
+ if (!selection || !description.trim()) return;
3318
+ const target = {
3319
+ kind: selection.kind,
3320
+ selector: selection.selector,
3321
+ text: selection.text,
3322
+ tagName: selection.tagName,
3323
+ rect: {
3324
+ top: Math.round(selection.rect.top),
3325
+ left: Math.round(selection.rect.left),
3326
+ width: Math.round(selection.rect.width),
3327
+ height: Math.round(selection.rect.height)
3328
+ }
3329
+ };
3330
+ await addNote({ description, screenshot: shot ?? void 0, target });
3331
+ endCapture();
3332
+ };
3333
+ const popStyleFor = useCallback((r) => {
3334
+ if (typeof window === "undefined") return {};
3335
+ const below = r.top + r.height + 12;
3336
+ const placeAbove = below + 220 > window.innerHeight;
3337
+ const top = placeAbove ? Math.max(12, r.top - 12) : below;
3338
+ let left = r.left;
3339
+ left = Math.min(left, window.innerWidth - 340);
3340
+ left = Math.max(12, left);
3341
+ return {
3342
+ top,
3343
+ left,
3344
+ transform: placeAbove ? "translateY(-100%)" : "none"
3345
+ };
3346
+ }, []);
3347
+ const popStyle = selection ? popStyleFor(selection.rect) : {};
3348
+ const confirmPopStyle = candidate ? popStyleFor(candidate.rect) : {};
3349
+ const activeRect = drag?.rect ?? candidate?.rect ?? selection?.rect ?? hover?.rect ?? null;
3350
+ const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
3351
+ const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
3352
+ return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", children: [
3353
+ /* @__PURE__ */ jsx(
3354
+ "div",
3355
+ {
3356
+ ref: layerRef,
3357
+ onPointerMove,
3358
+ onPointerDown,
3359
+ onPointerUp,
3360
+ onPointerCancel,
3361
+ className: "qa-fixed qa-inset-0 qa-z-10090",
3362
+ style: {
3363
+ cursor: phase === "selecting" && !coarse ? "crosshair" : "default",
3364
+ touchAction: coarse ? regionMode ? "none" : "pan-x pan-y" : "auto",
3365
+ background: "rgba(58,42,46,0.18)"
3366
+ }
3367
+ }
3368
+ ),
3369
+ phase === "selecting" && !coarse && /* @__PURE__ */ jsxs(
3370
+ "div",
3371
+ {
3372
+ 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",
3373
+ style: { background: theme.primary },
3374
+ children: [
3375
+ /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
3376
+ /* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
3377
+ t("cap_click")
3378
+ ] }),
3379
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "\xB7" }),
3380
+ /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
3381
+ /* @__PURE__ */ jsx(Icon, { name: "Square", size: 16 }),
3382
+ t("cap_drag")
3383
+ ] }),
3384
+ /* @__PURE__ */ jsx(
3385
+ "button",
3386
+ {
3387
+ onClick: () => endCapture(),
3388
+ 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",
3389
+ style: { background: "transparent", color: "#fff", cursor: "pointer" },
3390
+ children: "Esc"
3391
+ }
3392
+ )
3393
+ ]
3394
+ }
3395
+ ),
3396
+ phase === "selecting" && coarse && /* @__PURE__ */ jsxs(
3397
+ "div",
3398
+ {
3399
+ 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",
3400
+ style: { background: theme.primary },
3401
+ children: [
3402
+ /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
3403
+ /* @__PURE__ */ jsx(Icon, { name: "MousePointerClick", size: 16 }),
3404
+ t("tap_element")
3405
+ ] }),
3406
+ /* @__PURE__ */ jsx("span", { className: "qa-opacity-50", children: "\xB7" }),
3407
+ /* @__PURE__ */ jsxs(
3408
+ "button",
3409
+ {
3410
+ onClick: () => setRegionMode((v) => !v),
3411
+ "aria-pressed": regionMode,
3412
+ className: "qa-tap qa-flex qa-items-center qa-gap-1.5 qa-rounded-full qa-px-2 qa-py-0.5 qa-text-xs",
3413
+ style: {
3414
+ border: "1px solid rgba(255,255,255,0.4)",
3415
+ background: regionMode ? "rgba(255,255,255,0.35)" : "transparent",
3416
+ color: "#fff",
3417
+ cursor: "pointer"
3418
+ },
3419
+ children: [
3420
+ /* @__PURE__ */ jsx(Icon, { name: "Square", size: 16 }),
3421
+ t("draw_region")
3422
+ ]
3423
+ }
3424
+ ),
3425
+ /* @__PURE__ */ jsx(
3426
+ "button",
3427
+ {
3428
+ onClick: () => endCapture(),
3429
+ 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",
3430
+ style: { background: "transparent", color: "#fff", cursor: "pointer" },
3431
+ children: "Esc"
3432
+ }
3433
+ )
3434
+ ]
3435
+ }
3436
+ ),
3437
+ activeRect && /* @__PURE__ */ jsxs(
3438
+ "div",
3439
+ {
3440
+ className: "qa-fixed qa-z-10092 qa-rounded",
3441
+ style: {
3442
+ top: activeRect.top,
3443
+ left: activeRect.left,
3444
+ width: activeRect.width,
3445
+ height: activeRect.height,
3446
+ pointerEvents: confirmingRegion ? "auto" : "none",
3447
+ outline: `2px ${isRegion ? "dashed" : "solid"} ${theme.accent}`,
3448
+ outlineOffset: "1px",
3449
+ background: `${theme.accent}1f`,
3450
+ boxShadow: phase === "annotating" ? "0 0 0 9999px rgba(58,42,46,0.28)" : "none"
3451
+ },
3452
+ children: [
3453
+ (phase === "selecting" || phase === "confirming") && hover?.selector && !drag && /* @__PURE__ */ jsx(
3454
+ "span",
3455
+ {
3456
+ className: "qa-absolute qa-rounded qa-px-1.5 qa-py-0.5 qa-text-11 qa-text-white qa-truncate",
3457
+ style: {
3458
+ top: "-1.5rem",
3459
+ left: 0,
3460
+ maxWidth: "260px",
3461
+ background: theme.primary
3462
+ },
3463
+ children: hover.selector
3464
+ }
3465
+ ),
3466
+ drag?.rect && /* @__PURE__ */ jsxs(
3467
+ "span",
3468
+ {
3469
+ className: "qa-absolute qa-rounded qa-px-1.5 qa-py-0.5 qa-text-11 qa-text-white",
3470
+ style: {
3471
+ bottom: "-1.5rem",
3472
+ right: 0,
3473
+ background: theme.accentDark
3474
+ },
3475
+ children: [
3476
+ Math.round(drag.rect.width),
3477
+ " \xD7 ",
3478
+ Math.round(drag.rect.height)
3479
+ ]
3480
+ }
3481
+ ),
3482
+ confirmingRegion && /* @__PURE__ */ jsxs(Fragment, { children: [
3483
+ /* @__PURE__ */ jsx(
3484
+ "div",
3485
+ {
3486
+ className: "qa-absolute qa-inset-0 qa-z-10093",
3487
+ onPointerDown: onHandlePointerDown("move"),
3488
+ onPointerMove: onHandlePointerMove,
3489
+ onPointerUp: onHandlePointerUp,
3490
+ onPointerCancel: onHandlePointerUp,
3491
+ style: { touchAction: "none", cursor: "move" }
3492
+ }
3493
+ ),
3494
+ REGION_HANDLES.map(({ edge, top, left, cursor }) => /* @__PURE__ */ jsx(
3495
+ "div",
3496
+ {
3497
+ role: "button",
3498
+ "aria-label": t("resize"),
3499
+ className: "qa-tap-icon qa-z-10094 qa-absolute qa-rounded-full",
3500
+ onPointerDown: onHandlePointerDown(edge),
3501
+ onPointerMove: onHandlePointerMove,
3502
+ onPointerUp: onHandlePointerUp,
3503
+ onPointerCancel: onHandlePointerUp,
3504
+ style: {
3505
+ top,
3506
+ left,
3507
+ transform: "translate(-50%, -50%)",
3508
+ touchAction: "none",
3509
+ cursor,
3510
+ background: `${theme.accent}33`
3511
+ },
3512
+ children: /* @__PURE__ */ jsx(
3513
+ "span",
3514
+ {
3515
+ className: "qa-rounded-full",
3516
+ style: {
3517
+ width: 16,
3518
+ height: 16,
3519
+ background: theme.accent,
3520
+ border: "2px solid #fff",
3521
+ boxShadow: "0 1px 3px rgba(0,0,0,0.35)",
3522
+ pointerEvents: "none"
3523
+ }
3524
+ }
3525
+ )
3526
+ },
3527
+ edge
3528
+ ))
3529
+ ] })
3530
+ ]
3531
+ }
3532
+ ),
3533
+ phase === "confirming" && candidate && coarse && /* @__PURE__ */ jsxs(
3534
+ "div",
3535
+ {
3536
+ "data-qa-overlay": "true",
3537
+ dir,
3538
+ role: "group",
3539
+ "aria-label": candidate.kind === "region" ? t("confirm_region") : t("use_this"),
3540
+ 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",
3541
+ style: {
3542
+ ...confirmPopStyle,
3543
+ background: theme.surface,
3544
+ borderColor: `${theme.primary}22`
3545
+ },
3546
+ children: [
3547
+ /* @__PURE__ */ jsxs(
3548
+ "button",
3549
+ {
3550
+ onClick: () => void beginAnnotation(candidate),
3551
+ 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",
3552
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
3553
+ children: [
3554
+ /* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
3555
+ t("use_this")
3556
+ ]
3557
+ }
3558
+ ),
3559
+ /* @__PURE__ */ jsx(
3560
+ "button",
3561
+ {
3562
+ onClick: () => {
3563
+ setCandidate(null);
3564
+ setHover(null);
3565
+ setPhase("selecting");
3566
+ },
3567
+ className: "qa-tap qa-rounded-full qa-border qa-px-3 qa-py-2 qa-text-sm",
3568
+ style: {
3569
+ borderColor: `${theme.primary}33`,
3570
+ color: theme.primary,
3571
+ background: "transparent",
3572
+ cursor: "pointer"
3573
+ },
3574
+ children: t("adjust")
3575
+ }
3576
+ )
3577
+ ]
3578
+ }
3579
+ ),
3580
+ phase === "annotating" && selection && /* @__PURE__ */ jsxs(
3581
+ "div",
3582
+ {
3583
+ "data-qa-overlay": "true",
3584
+ dir,
3585
+ 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" : ""}`,
3586
+ style: {
3587
+ ...popStyle,
3588
+ background: theme.surface,
3589
+ borderColor: `${theme.primary}22`,
3590
+ fontFamily: dir === "rtl" ? "'Tajawal', sans-serif" : "'Nunito', system-ui, sans-serif"
3591
+ },
3592
+ children: [
3593
+ /* @__PURE__ */ jsxs(
3594
+ "div",
3595
+ {
3596
+ className: "qa-flex qa-items-center qa-gap-2 qa-px-3 qa-py-2 qa-text-white",
3597
+ style: { background: theme.primary },
3598
+ children: [
3599
+ /* @__PURE__ */ jsx(
3600
+ Icon,
3601
+ {
3602
+ name: selection.kind === "region" ? "Square" : "MousePointerClick",
3603
+ size: 16
3604
+ }
3605
+ ),
3606
+ /* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-font-semibold", children: selection.kind === "region" ? t("sel_region") : t("sel_element") }),
3607
+ /* @__PURE__ */ jsx(
3608
+ "button",
3609
+ {
3610
+ onClick: () => endCapture(),
3611
+ className: "qa-tap-icon qa-ms-auto qa-opacity-80 qa-hover-opacity-100",
3612
+ style: { background: "transparent", border: "none", cursor: "pointer", color: "#fff" },
3613
+ children: /* @__PURE__ */ jsx(Icon, { name: "X", size: 16 })
3614
+ }
3615
+ )
3616
+ ]
3617
+ }
3618
+ ),
3619
+ /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
3620
+ /* @__PURE__ */ jsx(
3621
+ "div",
3622
+ {
3623
+ className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
3624
+ style: {
3625
+ borderColor: `${theme.primary}1a`,
3626
+ background: theme.cream
3627
+ },
3628
+ children: capturing ? /* @__PURE__ */ jsxs(
3629
+ "span",
3630
+ {
3631
+ className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs",
3632
+ style: { color: theme.primary },
3633
+ children: [
3634
+ /* @__PURE__ */ jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
3635
+ t("capturing")
3636
+ ]
3637
+ }
3638
+ ) : shotUrl ? /* @__PURE__ */ jsx(
3639
+ "img",
3640
+ {
3641
+ src: shotUrl,
3642
+ alt: "capture",
3643
+ className: "qa-max-h-32 qa-rounded-md"
3644
+ }
3645
+ ) : /* @__PURE__ */ jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
3646
+ }
3647
+ ),
3648
+ /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
3649
+ /* @__PURE__ */ jsx(
3650
+ "textarea",
3651
+ {
3652
+ ref: taRef,
3653
+ value: description,
3654
+ onChange: (e) => setDescription(e.target.value),
3655
+ onKeyDown: (e) => {
3656
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
3657
+ },
3658
+ rows: 3,
3659
+ placeholder: t("annotate_placeholder"),
3660
+ 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",
3661
+ style: { borderColor: `${theme.primary}33`, background: "#fff", color: "inherit" }
3662
+ }
3663
+ ),
3664
+ /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
3665
+ /* @__PURE__ */ jsxs(
3666
+ "button",
3667
+ {
3668
+ onClick: () => void save(),
3669
+ disabled: !description.trim(),
3670
+ 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",
3671
+ style: { background: theme.accent, border: "none", cursor: "pointer" },
3672
+ children: [
3673
+ /* @__PURE__ */ jsx(Icon, { name: "Check", size: 16 }),
3674
+ t("save_point")
3675
+ ]
3676
+ }
3677
+ ),
3678
+ /* @__PURE__ */ jsx(
3679
+ "button",
3680
+ {
3681
+ onClick: () => {
3682
+ setPhase("selecting");
3683
+ setSelection(null);
3684
+ setShot(null);
3685
+ setDescription("");
3686
+ },
3687
+ className: "qa-tap qa-rounded-lg qa-border qa-px-3 qa-py-2 qa-text-sm",
3688
+ style: {
3689
+ borderColor: `${theme.primary}33`,
3690
+ color: theme.primary,
3691
+ background: "transparent",
3692
+ cursor: "pointer"
3693
+ },
3694
+ children: t("reselect")
3695
+ }
3696
+ )
3697
+ ] }),
3698
+ /* @__PURE__ */ jsx("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: t("save_hint") })
3699
+ ] })
3700
+ ]
3701
+ }
3702
+ )
3703
+ ] });
3704
+ }
3705
+ function isProduction() {
3706
+ try {
3707
+ const proc = globalThis.process;
3708
+ return proc?.env?.["NODE_ENV"] === "production";
3709
+ } catch {
3710
+ return false;
3711
+ }
3712
+ }
3713
+ function parseHotkey(hotkey) {
3714
+ if (!hotkey) return null;
3715
+ const parts = hotkey.toLowerCase().split("+");
3716
+ const key = parts[parts.length - 1];
3717
+ if (!key) return null;
3718
+ return {
3719
+ shift: parts.includes("shift"),
3720
+ alt: parts.includes("alt"),
3721
+ ctrl: parts.includes("ctrl") || parts.includes("control"),
3722
+ meta: parts.includes("meta") || parts.includes("cmd"),
3723
+ key
3724
+ };
3725
+ }
3726
+ var QaErrorBoundary = class extends Component {
3727
+ constructor(props) {
3728
+ super(props);
3729
+ this.state = { caught: false };
3730
+ }
3731
+ static getDerivedStateFromError() {
3732
+ return { caught: true };
3733
+ }
3734
+ componentDidCatch(err, info) {
3735
+ console.error("[Qapture] Caught error in overlay:", err, info);
3736
+ }
3737
+ render() {
3738
+ if (this.state.caught) return null;
3739
+ return this.props.children;
3740
+ }
3741
+ };
3742
+ function CaptureGate() {
3743
+ const { captureActive } = useQa();
3744
+ return captureActive ? /* @__PURE__ */ jsx(CaptureMode, {}) : null;
3745
+ }
3746
+ function QaRootInner({ config }) {
3747
+ const shouldShowInitially = config.alwaysVisible === true || config.visible === true || config.visible === void 0 && !isProduction();
3748
+ const [visible, setVisible] = useState(shouldShowInitially);
3749
+ useEffect(() => {
3750
+ if (typeof document === "undefined") return;
3751
+ const hk = parseHotkey(config.hotkey);
3752
+ if (!hk) return;
3753
+ const handler = (e) => {
3754
+ if (e.key.toLowerCase() === hk.key && !!e.shiftKey === hk.shift && !!e.altKey === hk.alt && !!e.ctrlKey === hk.ctrl && !!e.metaKey === hk.meta) {
3755
+ e.preventDefault();
3756
+ setVisible((v) => !v);
3757
+ }
3758
+ };
3759
+ document.addEventListener("keydown", handler);
3760
+ return () => document.removeEventListener("keydown", handler);
3761
+ }, [config.hotkey]);
3762
+ if (!visible) return null;
3763
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3764
+ /* @__PURE__ */ jsx(QaFab, {}),
3765
+ /* @__PURE__ */ jsx(QaPanel, {}),
3766
+ /* @__PURE__ */ jsx(CaptureGate, {})
3767
+ ] });
3768
+ }
3769
+ function QaRoot({ config }) {
3770
+ return /* @__PURE__ */ jsx(QaErrorBoundary, { children: /* @__PURE__ */ jsx(QaProvider, { config, children: /* @__PURE__ */ jsx(QaRootInner, { config }) }) });
3771
+ }
3772
+
3773
+ // src/mount/ShadowMount.ts
3774
+ function mountQaStudio(config) {
3775
+ if (typeof window === "undefined" || typeof document === "undefined") {
3776
+ return { destroy() {
3777
+ } };
3778
+ }
3779
+ const host = document.createElement("qapture-overlay");
3780
+ host.setAttribute("data-qa-overlay", "true");
3781
+ document.body.appendChild(host);
3782
+ const shadow = host.attachShadow({ mode: "open" });
3783
+ injectStyles(shadow);
3784
+ applyThemeVars(host, config.theme);
3785
+ const root = ReactDOM.createRoot(shadow);
3786
+ root.render(React.createElement(QaRoot, { config }));
3787
+ return {
3788
+ destroy() {
3789
+ try {
3790
+ root.unmount();
3791
+ } catch {
3792
+ }
3793
+ if (host.parentNode) host.remove();
3794
+ if (typeof document !== "undefined") {
3795
+ document.body.querySelectorAll(":scope > [data-qa-overlay]").forEach((el) => el.remove());
3796
+ }
3797
+ }
3798
+ };
3799
+ }
3800
+
3801
+ // src/index.ts
3802
+ function initQaStudio(config) {
3803
+ if (typeof window === "undefined") {
3804
+ return { destroy() {
3805
+ } };
3806
+ }
3807
+ const { config: resolved, warnings } = validateConfig(config);
3808
+ for (const w of warnings) {
3809
+ console.warn("[Qapture]", w);
3810
+ }
3811
+ return mountQaStudio(resolved);
3812
+ }
3813
+ function Qapture({ config }) {
3814
+ useEffect(() => {
3815
+ const instance = initQaStudio(config);
3816
+ return () => instance.destroy();
3817
+ }, []);
3818
+ return null;
3819
+ }
3820
+
3821
+ export { Qapture, initQaStudio };
3822
+ //# sourceMappingURL=chunk-OPZF4IVW.js.map
3823
+ //# sourceMappingURL=chunk-OPZF4IVW.js.map