qapture2 0.8.2 → 0.9.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.
- package/dist/{chunk-IYQ5JZJX.cjs → chunk-5EWNGJ2N.cjs} +940 -54
- package/dist/{chunk-JGSKPW7Y.js → chunk-B22X5Y6U.js} +940 -54
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +13 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +1 -1
|
@@ -176,6 +176,7 @@ var DEFAULTS = {
|
|
|
176
176
|
brandLabel: "Qapture",
|
|
177
177
|
loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
|
|
178
178
|
rtl: false,
|
|
179
|
+
beta: false,
|
|
179
180
|
visible: void 0,
|
|
180
181
|
alwaysVisible: false,
|
|
181
182
|
hotkey: "shift+alt+q",
|
|
@@ -320,9 +321,9 @@ function warnMissingArabic(loginField, credentials, journey, warnings) {
|
|
|
320
321
|
if (c.hint !== void 0 && !hasAr(c.hint)) missing.push(`credentials role="${c.role}" (hint.ar)`);
|
|
321
322
|
}
|
|
322
323
|
if (!missing.length) return;
|
|
323
|
-
const
|
|
324
|
-
const shown = missing.slice(0,
|
|
325
|
-
const rest = missing.length >
|
|
324
|
+
const LIMIT2 = 6;
|
|
325
|
+
const shown = missing.slice(0, LIMIT2).join("; ");
|
|
326
|
+
const rest = missing.length > LIMIT2 ? ` (+${missing.length - LIMIT2} more)` : "";
|
|
326
327
|
warnings.push(
|
|
327
328
|
`Arabic is used elsewhere in this config, but ${missing.length} bilingual field(s) have no "ar" and will show English to an Arabic-language tester: ${shown}${rest}. Every {en, ar} pair needs BOTH filled in \u2014 a plain string or an {en}-only object reads identically to a bilingual field that was simply never translated.`
|
|
328
329
|
);
|
|
@@ -347,6 +348,7 @@ function validateConfig(input) {
|
|
|
347
348
|
journey: [],
|
|
348
349
|
preamble: null,
|
|
349
350
|
rtl: DEFAULTS.rtl,
|
|
351
|
+
beta: DEFAULTS.beta,
|
|
350
352
|
visible: DEFAULTS.visible,
|
|
351
353
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
352
354
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -367,6 +369,7 @@ function validateConfig(input) {
|
|
|
367
369
|
journey: [],
|
|
368
370
|
preamble: null,
|
|
369
371
|
rtl: DEFAULTS.rtl,
|
|
372
|
+
beta: DEFAULTS.beta,
|
|
370
373
|
visible: DEFAULTS.visible,
|
|
371
374
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
372
375
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -402,6 +405,7 @@ function validateConfig(input) {
|
|
|
402
405
|
const journey = raw["journey"] !== void 0 ? coerceJourney(raw["journey"], warnings) : [];
|
|
403
406
|
const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
|
|
404
407
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
408
|
+
const beta = typeof raw["beta"] === "boolean" ? raw["beta"] : DEFAULTS.beta;
|
|
405
409
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
406
410
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
407
411
|
const captureHotkey = isNonEmptyString(raw["captureHotkey"]) ? raw["captureHotkey"].trim() : DEFAULTS.captureHotkey;
|
|
@@ -424,6 +428,7 @@ function validateConfig(input) {
|
|
|
424
428
|
journey,
|
|
425
429
|
preamble,
|
|
426
430
|
rtl,
|
|
431
|
+
beta,
|
|
427
432
|
visible,
|
|
428
433
|
alwaysVisible,
|
|
429
434
|
hotkey,
|
|
@@ -1735,6 +1740,32 @@ var STR = {
|
|
|
1735
1740
|
too_heavy: "This page is too big to redraw \u2014 the tab would freeze. Photograph it instead: one frame, and maps and charts come out right.",
|
|
1736
1741
|
no_shot: "no screenshot (location saved)",
|
|
1737
1742
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1743
|
+
// Asked as two plain questions, never as jargon. "Expected behaviour" is
|
|
1744
|
+
// a phrase from a bug tracker; "what should have happened" is a question
|
|
1745
|
+
// anybody can answer.
|
|
1746
|
+
q_observed: "What happened?",
|
|
1747
|
+
q_observed_hint: "What you saw. Plain words are fine.",
|
|
1748
|
+
q_wanted: "What should have happened?",
|
|
1749
|
+
q_wanted_hint: "What you expected instead. This is the important one.",
|
|
1750
|
+
q_why: "Why does it matter? (optional)",
|
|
1751
|
+
q_why_hint: "What you were trying to get done.",
|
|
1752
|
+
q_fix: "Suggested fix (optional)",
|
|
1753
|
+
q_fix_hint: "Passed on as a suggestion, not an instruction.",
|
|
1754
|
+
voice_start: "Speak",
|
|
1755
|
+
voice_stop: "Stop",
|
|
1756
|
+
voice_failed: "Dictation did not start",
|
|
1757
|
+
voice_denied: "Microphone blocked",
|
|
1758
|
+
dev_mode_label: "Developer mode",
|
|
1759
|
+
diag_title: "Diagnostics",
|
|
1760
|
+
diag_hint: "Checks the handful of things that actually stop screenshots working on a page. Run it if something looks wrong.",
|
|
1761
|
+
diag_run: "Check this page",
|
|
1762
|
+
diag_running: "Checking\u2026",
|
|
1763
|
+
diag_version: "Version {v}",
|
|
1764
|
+
diag_outdated: "Update available: {v} \u2014",
|
|
1765
|
+
diag_current: "up to date",
|
|
1766
|
+
diag_faults: "Recorded problems: {n}",
|
|
1767
|
+
dup_notice: "You already said something like this at {when}:",
|
|
1768
|
+
dev_mode_hint: "Adds severity, the element selector, a suggested-fix field and the source location. Off by default so the person reporting a problem is asked only what they can actually answer.",
|
|
1738
1769
|
save_point: "Save point",
|
|
1739
1770
|
save_next: "Save + next",
|
|
1740
1771
|
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
@@ -1950,6 +1981,29 @@ var STR = {
|
|
|
1950
1981
|
too_heavy: "\u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629 \u0623\u0643\u0628\u0631 \u0645\u0646 \u0623\u0646 \u064A\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627 \u2014 \u0633\u064A\u062A\u062C\u0645\u0651\u062F \u0627\u0644\u062A\u0628\u0648\u064A\u0628. \u0635\u0648\u0651\u0631\u0647\u0627 \u0628\u062F\u0644 \u0630\u0644\u0643: \u0625\u0637\u0627\u0631 \u0648\u0627\u062D\u062F\u060C \u0648\u062A\u0638\u0647\u0631 \u0627\u0644\u062E\u0631\u0627\u0626\u0637 \u0648\u0627\u0644\u0631\u0633\u0648\u0645 \u0643\u0645\u0627 \u0647\u064A.",
|
|
1951
1982
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1952
1983
|
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)",
|
|
1984
|
+
q_observed: "\u0645\u0627 \u0627\u0644\u0630\u064A \u062D\u062F\u062B\u061F",
|
|
1985
|
+
q_observed_hint: "\u0645\u0627 \u0631\u0623\u064A\u062A\u0647. \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0627\u0644\u0639\u0627\u062F\u064A\u0629.",
|
|
1986
|
+
q_wanted: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0627\u0646 \u064A\u062C\u0628 \u0623\u0646 \u064A\u062D\u062F\u062B\u061F",
|
|
1987
|
+
q_wanted_hint: "\u0645\u0627 \u062A\u0648\u0642\u0651\u0639\u062A\u0647 \u0628\u062F\u0644 \u0630\u0644\u0643. \u0647\u0630\u0627 \u0647\u0648 \u0627\u0644\u0623\u0647\u0645.",
|
|
1988
|
+
q_why: "\u0644\u0645\u0627\u0630\u0627 \u064A\u0647\u0645\u0651\u0643\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
1989
|
+
q_why_hint: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0646\u062A \u062A\u062D\u0627\u0648\u0644 \u0625\u0646\u062C\u0627\u0632\u0647.",
|
|
1990
|
+
q_fix: "\u0627\u0642\u062A\u0631\u0627\u062D \u0644\u0644\u062D\u0644 (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
1991
|
+
q_fix_hint: "\u064A\u064F\u0645\u0631\u064E\u0651\u0631 \u0643\u0627\u0642\u062A\u0631\u0627\u062D\u060C \u0644\u0627 \u0643\u0623\u0645\u0631.",
|
|
1992
|
+
voice_start: "\u062A\u062D\u062F\u0651\u062B",
|
|
1993
|
+
voice_stop: "\u0625\u064A\u0642\u0627\u0641",
|
|
1994
|
+
voice_failed: "\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0625\u0645\u0644\u0627\u0621 \u0627\u0644\u0635\u0648\u062A\u064A",
|
|
1995
|
+
voice_denied: "\u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0645\u062D\u062C\u0648\u0628",
|
|
1996
|
+
dev_mode_label: "\u0648\u0636\u0639 \u0627\u0644\u0645\u0637\u0648\u0651\u0631",
|
|
1997
|
+
diag_title: "\u0627\u0644\u062A\u0634\u062E\u064A\u0635",
|
|
1998
|
+
diag_hint: "\u064A\u0641\u062D\u0635 \u0627\u0644\u0623\u0634\u064A\u0627\u0621 \u0627\u0644\u0642\u0644\u064A\u0644\u0629 \u0627\u0644\u062A\u064A \u062A\u0645\u0646\u0639 \u0639\u0645\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0641\u0639\u0644\u064A\u064B\u0627 \u0641\u064A \u0627\u0644\u0635\u0641\u062D\u0629. \u0634\u063A\u0651\u0644\u0647 \u0625\u0630\u0627 \u0628\u062F\u0627 \u0634\u064A\u0621 \u063A\u064A\u0631 \u0633\u0644\u064A\u0645.",
|
|
1999
|
+
diag_run: "\u0627\u0641\u062D\u0635 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
2000
|
+
diag_running: "\u062C\u0627\u0631\u064D \u0627\u0644\u0641\u062D\u0635\u2026",
|
|
2001
|
+
diag_version: "\u0627\u0644\u0625\u0635\u062F\u0627\u0631 {v}",
|
|
2002
|
+
diag_outdated: "\u064A\u062A\u0648\u0641\u0631 \u062A\u062D\u062F\u064A\u062B: {v} \u2014",
|
|
2003
|
+
diag_current: "\u0645\u062D\u062F\u0651\u062B",
|
|
2004
|
+
diag_faults: "\u0645\u0634\u0643\u0644\u0627\u062A \u0645\u064F\u0633\u062C\u0651\u0644\u0629: {n}",
|
|
2005
|
+
dup_notice: "\u0633\u0628\u0642 \u0623\u0646 \u0643\u062A\u0628\u062A \u0634\u064A\u0626\u064B\u0627 \u0645\u0634\u0627\u0628\u0647\u064B\u0627 \u0627\u0644\u0633\u0627\u0639\u0629 {when}:",
|
|
2006
|
+
dev_mode_hint: "\u064A\u0636\u064A\u0641 \u0627\u0644\u0623\u0647\u0645\u064A\u0629 \u0648\u0645\u064F\u062D\u062F\u0650\u0651\u062F \u0627\u0644\u0639\u0646\u0635\u0631 \u0648\u062D\u0642\u0644 \u0627\u0642\u062A\u0631\u0627\u062D \u0627\u0644\u062D\u0644 \u0648\u0645\u0648\u0642\u0639 \u0627\u0644\u0634\u064A\u0641\u0631\u0629. \u0645\u064F\u0637\u0641\u0623 \u0627\u0641\u062A\u0631\u0627\u0636\u064A\u064B\u0627 \u062D\u062A\u0649 \u0644\u0627 \u064A\u064F\u0633\u0623\u0644 \u0645\u064E\u0646 \u064A\u064F\u0628\u0644\u0651\u063A \u0639\u0646 \u0645\u0634\u0643\u0644\u0629 \u0625\u0644\u0627 \u0639\u0645\u0651\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0627\u0644\u0625\u062C\u0627\u0628\u0629 \u0639\u0646\u0647.",
|
|
1953
2007
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1954
2008
|
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1955
2009
|
save_next_hint: "\u0627\u062D\u0641\u0638 \u0647\u0630\u0647 \u0648\u062D\u062F\u0651\u062F \u062C\u0632\u0621\u064B\u0627 \u0622\u062E\u0631 \u0645\u0646 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629 \u2014 \u0628\u062F\u0648\u0646 \u0637\u0644\u0628 \u0625\u0630\u0646 \u062C\u062F\u064A\u062F.",
|
|
@@ -2390,6 +2444,25 @@ function getExactCaptureStatus() {
|
|
|
2390
2444
|
function getFrozenFrame() {
|
|
2391
2445
|
return frozen;
|
|
2392
2446
|
}
|
|
2447
|
+
var STILL_REUSE_MS = 9e4;
|
|
2448
|
+
var frozenAtScrollX = 0;
|
|
2449
|
+
var frozenAtScrollY = 0;
|
|
2450
|
+
var frozenAtPath = "";
|
|
2451
|
+
function stillIsCurrent() {
|
|
2452
|
+
if (!frozen) return false;
|
|
2453
|
+
if (typeof window === "undefined") return false;
|
|
2454
|
+
if (Date.now() - frozen.takenAt > STILL_REUSE_MS) return false;
|
|
2455
|
+
if (window.scrollX !== frozenAtScrollX || window.scrollY !== frozenAtScrollY) return false;
|
|
2456
|
+
if (window.location.pathname + window.location.search !== frozenAtPath) return false;
|
|
2457
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
2458
|
+
const vh = document.documentElement.clientHeight || window.innerHeight;
|
|
2459
|
+
if (vw !== frozen.viewportWidth || vh !== frozen.viewportHeight) return false;
|
|
2460
|
+
return true;
|
|
2461
|
+
}
|
|
2462
|
+
async function freezeOrReuse() {
|
|
2463
|
+
if (stillIsCurrent()) return frozen;
|
|
2464
|
+
return freezeViewport();
|
|
2465
|
+
}
|
|
2393
2466
|
function releaseFrozenFrame() {
|
|
2394
2467
|
if (frozen) {
|
|
2395
2468
|
frozen.canvas.width = 0;
|
|
@@ -2634,6 +2707,9 @@ async function freezeViewport() {
|
|
|
2634
2707
|
viewportHeight: vh,
|
|
2635
2708
|
takenAt: Date.now()
|
|
2636
2709
|
};
|
|
2710
|
+
frozenAtScrollX = window.scrollX;
|
|
2711
|
+
frozenAtScrollY = window.scrollY;
|
|
2712
|
+
frozenAtPath = window.location.pathname + window.location.search;
|
|
2637
2713
|
lastMode = mode;
|
|
2638
2714
|
return frozen;
|
|
2639
2715
|
} catch {
|
|
@@ -2805,6 +2881,38 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2805
2881
|
return touched;
|
|
2806
2882
|
}
|
|
2807
2883
|
|
|
2884
|
+
// src/lib/faultLog.ts
|
|
2885
|
+
var LIMIT = 40;
|
|
2886
|
+
var faults = [];
|
|
2887
|
+
function recordFault(where, err) {
|
|
2888
|
+
const what = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : (() => {
|
|
2889
|
+
try {
|
|
2890
|
+
return JSON.stringify(err);
|
|
2891
|
+
} catch {
|
|
2892
|
+
return String(err);
|
|
2893
|
+
}
|
|
2894
|
+
})();
|
|
2895
|
+
faults.push({ at: Date.now(), where, what: what.slice(0, 500) });
|
|
2896
|
+
if (faults.length > LIMIT) faults.splice(0, faults.length - LIMIT);
|
|
2897
|
+
console.warn(`[QA] ${where}:`, err);
|
|
2898
|
+
}
|
|
2899
|
+
function readFaults() {
|
|
2900
|
+
return [...faults].reverse();
|
|
2901
|
+
}
|
|
2902
|
+
function clearFaults() {
|
|
2903
|
+
faults.length = 0;
|
|
2904
|
+
}
|
|
2905
|
+
function faultsAsText(version) {
|
|
2906
|
+
if (!faults.length) return "No faults recorded.";
|
|
2907
|
+
const head = [
|
|
2908
|
+
`qapture ${version}`,
|
|
2909
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
2910
|
+
typeof location !== "undefined" ? location.href.split("?")[0] : "",
|
|
2911
|
+
""
|
|
2912
|
+
].filter(Boolean).join("\n");
|
|
2913
|
+
return head + readFaults().map((f) => `${new Date(f.at).toISOString()} [${f.where}] ${f.what}`).join("\n");
|
|
2914
|
+
}
|
|
2915
|
+
|
|
2808
2916
|
// src/lib/capture.ts
|
|
2809
2917
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2810
2918
|
var CHUNK_TIMEOUT_MS = 8e3;
|
|
@@ -3157,12 +3265,13 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3157
3265
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3158
3266
|
}
|
|
3159
3267
|
} catch (err) {
|
|
3160
|
-
|
|
3268
|
+
recordFault("screenshot/crop", err);
|
|
3161
3269
|
}
|
|
3162
3270
|
if (prefer === "exact") return { status: "failed" };
|
|
3163
3271
|
}
|
|
3164
3272
|
const weight = documentWeight();
|
|
3165
3273
|
if (weight > TOO_HEAVY_NODES) {
|
|
3274
|
+
recordFault("screenshot/too-heavy", `page has ${weight} elements; the redraw engine was refused`);
|
|
3166
3275
|
return { status: "too-heavy", nodes: weight };
|
|
3167
3276
|
}
|
|
3168
3277
|
try {
|
|
@@ -3171,14 +3280,14 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3171
3280
|
const blob = await encodeShot(canvas);
|
|
3172
3281
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3173
3282
|
} catch (err) {
|
|
3174
|
-
|
|
3283
|
+
recordFault("screenshot/render", err);
|
|
3175
3284
|
try {
|
|
3176
3285
|
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
3177
3286
|
if (!canvas) return { status: "failed" };
|
|
3178
3287
|
const blob = await encodeShot(canvas);
|
|
3179
3288
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3180
3289
|
} catch (retryErr) {
|
|
3181
|
-
|
|
3290
|
+
recordFault("screenshot/render-retry", retryErr);
|
|
3182
3291
|
return { status: "failed" };
|
|
3183
3292
|
}
|
|
3184
3293
|
}
|
|
@@ -3222,10 +3331,32 @@ function formatEvent(ev, t0) {
|
|
|
3222
3331
|
}
|
|
3223
3332
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3224
3333
|
}
|
|
3334
|
+
var EVIDENCE_MARK = "<!--qa:evidence-->";
|
|
3335
|
+
function noteContextMarkdown(note, index) {
|
|
3336
|
+
const body = noteToMarkdown(note, { index, keepEvidenceMark: true });
|
|
3337
|
+
const at = body.indexOf(EVIDENCE_MARK);
|
|
3338
|
+
const header = [
|
|
3339
|
+
`# Point ${index} \u2014 runtime context`,
|
|
3340
|
+
"",
|
|
3341
|
+
`Page: ${oneLine(note.route) || "/"}`,
|
|
3342
|
+
`Captured: ${oneLine(note.timestamp)}`,
|
|
3343
|
+
"",
|
|
3344
|
+
"Everything the browser recorded around this capture. Kept out of",
|
|
3345
|
+
"`notes.md` on purpose -- it is here when it is needed, and out of the way",
|
|
3346
|
+
"when it is not.",
|
|
3347
|
+
"",
|
|
3348
|
+
"---",
|
|
3349
|
+
""
|
|
3350
|
+
].join("\n");
|
|
3351
|
+
return at === -1 ? `${header}_(nothing was recorded)_
|
|
3352
|
+
` : header + body.slice(at + EVIDENCE_MARK.length).trimStart();
|
|
3353
|
+
}
|
|
3225
3354
|
function noteCheckLine(note, index) {
|
|
3226
3355
|
const where = oneLine(note.route) || "/";
|
|
3227
|
-
const
|
|
3228
|
-
const
|
|
3356
|
+
const wanted = oneLine(note.wanted);
|
|
3357
|
+
const seen = oneLine(note.description) || "(not described)";
|
|
3358
|
+
const claim = wanted || `${seen} \u2014 _no expectation was given; ask before assuming one_`;
|
|
3359
|
+
const trimmed = claim.length > 180 ? `${claim.slice(0, 177)}...` : claim;
|
|
3229
3360
|
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3230
3361
|
}
|
|
3231
3362
|
function noteToMarkdown(note, opts) {
|
|
@@ -3255,8 +3386,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3255
3386
|
);
|
|
3256
3387
|
}
|
|
3257
3388
|
}
|
|
3389
|
+
if (note.origin?.component) lines.push(`- **Component:** \`${oneLine(note.origin.component)}\``);
|
|
3390
|
+
if (note.origin?.file) {
|
|
3391
|
+
lines.push(`- **Source:** \`${oneLine(note.origin.file)}${note.origin.line ? `:${note.origin.line}` : ""}\``);
|
|
3392
|
+
}
|
|
3258
3393
|
if (idx != null && note.screenshot) {
|
|
3259
3394
|
lines.push(`- **Screenshot:** screenshots/point-${idx}.${shotExtension(note.screenshot)}`);
|
|
3395
|
+
if (note.shotEngine === "dom") {
|
|
3396
|
+
lines.push("- **Screenshot caveat:** this is a re-drawing of the page, not a photograph. Canvas, WebGL and cross-origin images may be blank or missing in it. Trust the words over the picture where they disagree.");
|
|
3397
|
+
}
|
|
3260
3398
|
}
|
|
3261
3399
|
if (idx != null && note.afterScreenshot) {
|
|
3262
3400
|
lines.push(
|
|
@@ -3264,7 +3402,29 @@ function noteToMarkdown(note, opts) {
|
|
|
3264
3402
|
);
|
|
3265
3403
|
}
|
|
3266
3404
|
lines.push("");
|
|
3267
|
-
lines.push(
|
|
3405
|
+
lines.push("### Observed");
|
|
3406
|
+
lines.push("");
|
|
3407
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(not described)_");
|
|
3408
|
+
lines.push("");
|
|
3409
|
+
lines.push("### Expected");
|
|
3410
|
+
lines.push("");
|
|
3411
|
+
lines.push(
|
|
3412
|
+
note.wanted && oneLine(note.wanted) ? note.wanted.trim() : "_(the tester did not say what they expected instead -- ask rather than assume)_"
|
|
3413
|
+
);
|
|
3414
|
+
if (note.why && oneLine(note.why)) {
|
|
3415
|
+
lines.push("");
|
|
3416
|
+
lines.push("### Why it matters");
|
|
3417
|
+
lines.push("");
|
|
3418
|
+
lines.push(note.why.trim());
|
|
3419
|
+
}
|
|
3420
|
+
if (note.fixHint && oneLine(note.fixHint)) {
|
|
3421
|
+
lines.push("");
|
|
3422
|
+
lines.push("### Suggested fix (a suggestion, not an instruction)");
|
|
3423
|
+
lines.push("");
|
|
3424
|
+
lines.push(note.fixHint.trim());
|
|
3425
|
+
lines.push("");
|
|
3426
|
+
lines.push("> Weigh this. It came from a person looking at the symptom, not at the code, and following a wrong suggestion costs more than ignoring it.");
|
|
3427
|
+
}
|
|
3268
3428
|
if (note.followUp && oneLine(note.followUp)) {
|
|
3269
3429
|
lines.push("");
|
|
3270
3430
|
lines.push(
|
|
@@ -3275,6 +3435,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3275
3435
|
lines.push("");
|
|
3276
3436
|
lines.push(note.followUp.trim());
|
|
3277
3437
|
}
|
|
3438
|
+
if (idx != null) {
|
|
3439
|
+
lines.push("");
|
|
3440
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3441
|
+
lines.push("");
|
|
3442
|
+
lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
|
|
3443
|
+
lines.push("");
|
|
3444
|
+
lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
|
|
3445
|
+
}
|
|
3446
|
+
lines.push(EVIDENCE_MARK);
|
|
3278
3447
|
const recordedSteps = note.context?.steps ?? [];
|
|
3279
3448
|
if (recordedSteps.length) {
|
|
3280
3449
|
const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
|
|
@@ -3285,14 +3454,6 @@ function noteToMarkdown(note, opts) {
|
|
|
3285
3454
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3286
3455
|
});
|
|
3287
3456
|
}
|
|
3288
|
-
if (idx != null) {
|
|
3289
|
-
lines.push("");
|
|
3290
|
-
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3291
|
-
lines.push("");
|
|
3292
|
-
lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
|
|
3293
|
-
lines.push("");
|
|
3294
|
-
lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
|
|
3295
|
-
}
|
|
3296
3457
|
const ctx = note.context;
|
|
3297
3458
|
if (ctx) {
|
|
3298
3459
|
const env = ctx.env;
|
|
@@ -3339,7 +3500,67 @@ function noteToMarkdown(note, opts) {
|
|
|
3339
3500
|
lines.push("");
|
|
3340
3501
|
lines.push("</details>");
|
|
3341
3502
|
}
|
|
3342
|
-
|
|
3503
|
+
const whole = lines.join("\n");
|
|
3504
|
+
if (opts?.keepEvidenceMark) return whole;
|
|
3505
|
+
if (!opts?.contextFile) return whole.replace(`${EVIDENCE_MARK}
|
|
3506
|
+
`, "").replace(EVIDENCE_MARK, "");
|
|
3507
|
+
const at = whole.indexOf(EVIDENCE_MARK);
|
|
3508
|
+
const report = at === -1 ? whole : whole.slice(0, at).trimEnd();
|
|
3509
|
+
return `${report}
|
|
3510
|
+
|
|
3511
|
+
<sub>Steps, console, network, environment and element forensics: \`${opts.contextFile}\` \u2014 open it only if the words above leave something genuinely unresolved.</sub>`;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// src/lib/reproSpec.ts
|
|
3515
|
+
function lit(value) {
|
|
3516
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r?\n/g, " ");
|
|
3517
|
+
}
|
|
3518
|
+
function line(value, limit = 200) {
|
|
3519
|
+
const one = (value ?? "").replace(/\s+/g, " ").trim();
|
|
3520
|
+
if (!one) return "";
|
|
3521
|
+
return one.length > limit ? `${one.slice(0, limit - 3)}...` : one;
|
|
3522
|
+
}
|
|
3523
|
+
function reproSpec(note, index) {
|
|
3524
|
+
const selector = note.target?.selector;
|
|
3525
|
+
const route = note.route || "/";
|
|
3526
|
+
if (!selector && !note.route) return null;
|
|
3527
|
+
const observed = line(note.description);
|
|
3528
|
+
const expected = line(note.wanted);
|
|
3529
|
+
const out = [];
|
|
3530
|
+
out.push(`// check-${index} -- DRAFT. Delete this file if the change is cosmetic.`);
|
|
3531
|
+
out.push("//");
|
|
3532
|
+
out.push("// The URL and the selector below were captured from the live page, so");
|
|
3533
|
+
out.push("// they are the tedious part already done. The assertion is yours: only");
|
|
3534
|
+
out.push("// you know whether this point is worth pinning down with a test.");
|
|
3535
|
+
out.push("//");
|
|
3536
|
+
if (observed) out.push(`// Observed: ${observed}`);
|
|
3537
|
+
if (expected) out.push(`// Expected: ${expected}`);
|
|
3538
|
+
else out.push("// Expected: (the tester did not say -- ask before asserting anything)");
|
|
3539
|
+
if (note.origin?.file) {
|
|
3540
|
+
out.push(`// Rendered by: ${note.origin.component ?? "?"} (${note.origin.file}${note.origin.line ? `:${note.origin.line}` : ""})`);
|
|
3541
|
+
}
|
|
3542
|
+
out.push("");
|
|
3543
|
+
out.push("import { test, expect } from '@playwright/test';");
|
|
3544
|
+
out.push("");
|
|
3545
|
+
out.push(`test('check-${index}: ${lit(line(note.description, 60) || "reported point")}', async ({ page }) => {`);
|
|
3546
|
+
out.push(` await page.goto('${lit(route)}');`);
|
|
3547
|
+
if (selector) {
|
|
3548
|
+
out.push("");
|
|
3549
|
+
out.push(` const target = page.locator('${lit(selector)}');`);
|
|
3550
|
+
out.push(" await expect(target).toBeVisible();");
|
|
3551
|
+
out.push("");
|
|
3552
|
+
out.push(" // TODO: assert the EXPECTED behaviour quoted above.");
|
|
3553
|
+
out.push(" // Being visible only proves the element is still there -- it does not");
|
|
3554
|
+
out.push(" // prove the thing the tester asked for actually happened.");
|
|
3555
|
+
} else {
|
|
3556
|
+
out.push("");
|
|
3557
|
+
out.push(" // No element was picked for this point -- it was a region or a plain");
|
|
3558
|
+
out.push(" // note. Drive the page to the state described above, then assert.");
|
|
3559
|
+
out.push(" // TODO");
|
|
3560
|
+
}
|
|
3561
|
+
out.push("});");
|
|
3562
|
+
out.push("");
|
|
3563
|
+
return out.join("\n");
|
|
3343
3564
|
}
|
|
3344
3565
|
|
|
3345
3566
|
// src/lib/exportZip.ts
|
|
@@ -3475,7 +3696,15 @@ This archive is not a list of suggestions. Every point in \`notes.md\` is an acc
|
|
|
3475
3696
|
|
|
3476
3697
|
**What to do.** Do the work, then fill in \`verify.md\`: tick a box only when the check is true on a fresh load of the page named beside it, with the tester doing nothing extra. Where you could not do something, or you think it is the wrong thing to do, leave the box unticked and write one line saying why. An unticked box with a reason is a good answer. A ticked box that does not hold up is the only bad one \u2014 it costs the tester the trip to find out.
|
|
3477
3698
|
|
|
3478
|
-
Hand \`verify.md\` back with the work
|
|
3699
|
+
Hand \`verify.md\` back with the work.
|
|
3700
|
+
|
|
3701
|
+
### What is in this archive
|
|
3702
|
+
|
|
3703
|
+
- \`notes.md\` \u2014 the points themselves. Each one carries **Observed** and **Expected** under those exact headings. They are separate on purpose: where a report leaves the expectation out, agents do not stop and ask, they pick a reading and commit to it. Where you see Expected marked as not given, **ask rather than assume**.
|
|
3704
|
+
- \`verify.md\` \u2014 the checklist, one unticked box per point.
|
|
3705
|
+
- \`screenshots/\` \u2014 one per point. A point marked with a screenshot caveat was re-drawn rather than photographed, so canvases, charts and maps may be blank in it; trust the words over the picture there.
|
|
3706
|
+
- \`context/\` \u2014 console, network, environment and element forensics, one file per point. Deliberately **not** in \`notes.md\`: a longer report measurably lowers the chance of the right thing getting fixed, because the two sentences that matter get buried. Open these only when something is genuinely unresolved.
|
|
3707
|
+
- \`repro/\` \u2014 Playwright drafts, one per point, all optional. Finish the ones where the point is behavioural and worth pinning down; delete the ones where it is cosmetic. That judgement is yours.`
|
|
3479
3708
|
);
|
|
3480
3709
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3481
3710
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3615,7 +3844,14 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3615
3844
|
""
|
|
3616
3845
|
].join("\n");
|
|
3617
3846
|
const noteBlocks = notes.map(
|
|
3618
|
-
(n, i) => noteToMarkdown(n, {
|
|
3847
|
+
(n, i) => noteToMarkdown(n, {
|
|
3848
|
+
brand: brandLabel,
|
|
3849
|
+
index: i + 1,
|
|
3850
|
+
// Runtime evidence goes to its own file and is pointed at from here.
|
|
3851
|
+
// See the contextFile branch in noteMarkdown.ts for why: a long report
|
|
3852
|
+
// measurably lowers an agent's chance of fixing the thing.
|
|
3853
|
+
contextFile: n.context ? `context/point-${i + 1}.md` : void 0
|
|
3854
|
+
})
|
|
3619
3855
|
);
|
|
3620
3856
|
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
3621
3857
|
|
|
@@ -3656,6 +3892,43 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3656
3892
|
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3657
3893
|
""
|
|
3658
3894
|
].join("\n"));
|
|
3895
|
+
const contextDir = zip.folder("context");
|
|
3896
|
+
notes.forEach((n, i) => {
|
|
3897
|
+
if (n.context && contextDir) {
|
|
3898
|
+
contextDir.file(`point-${i + 1}.md`, noteContextMarkdown(n, i + 1));
|
|
3899
|
+
}
|
|
3900
|
+
});
|
|
3901
|
+
const reproDir = zip.folder("repro");
|
|
3902
|
+
let reproCount = 0;
|
|
3903
|
+
notes.forEach((n, i) => {
|
|
3904
|
+
const spec = reproSpec(n, i + 1);
|
|
3905
|
+
if (spec && reproDir) {
|
|
3906
|
+
reproDir.file(`check-${i + 1}.spec.ts`, spec);
|
|
3907
|
+
reproCount++;
|
|
3908
|
+
}
|
|
3909
|
+
});
|
|
3910
|
+
if (reproCount && reproDir) {
|
|
3911
|
+
reproDir.file("README.md", [
|
|
3912
|
+
"# Reproduction drafts",
|
|
3913
|
+
"",
|
|
3914
|
+
"One per point, and every one of them is optional.",
|
|
3915
|
+
"",
|
|
3916
|
+
"**Use one** where the point is behavioural and worth pinning down, so it",
|
|
3917
|
+
"cannot quietly come back later. An executable check is worth far more to",
|
|
3918
|
+
"you than another paragraph of steps written in English.",
|
|
3919
|
+
"",
|
|
3920
|
+
"**Delete it** where the point is cosmetic -- a colour, a spacing, a word.",
|
|
3921
|
+
"A test asserting that a heading is visible proves nothing anybody wanted",
|
|
3922
|
+
"proved, and it is one more file to maintain forever.",
|
|
3923
|
+
"",
|
|
3924
|
+
"That call is yours. The tester was not asked to make it and could not.",
|
|
3925
|
+
"",
|
|
3926
|
+
"What is already done for you in each file: the URL, a selector verified",
|
|
3927
|
+
"against the live DOM at capture time, and the observed and expected",
|
|
3928
|
+
"behaviour quoted in place. What is left is the assertion, marked TODO.",
|
|
3929
|
+
""
|
|
3930
|
+
].join("\n"));
|
|
3931
|
+
}
|
|
3659
3932
|
notes.forEach((n, i) => {
|
|
3660
3933
|
if (n.screenshot && shots) {
|
|
3661
3934
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4125,7 +4398,7 @@ function reportMarkdownText(allNotes) {
|
|
|
4125
4398
|
"",
|
|
4126
4399
|
"---",
|
|
4127
4400
|
""
|
|
4128
|
-
].filter((
|
|
4401
|
+
].filter((line2) => line2 !== "").join("\n");
|
|
4129
4402
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
4130
4403
|
return `${header}
|
|
4131
4404
|
${body}`;
|
|
@@ -4300,6 +4573,7 @@ function exactShotsWanted(store) {
|
|
|
4300
4573
|
}
|
|
4301
4574
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4302
4575
|
var COMPACT_KEY = "compactCapture";
|
|
4576
|
+
var DEV_MODE_KEY = "developerMode";
|
|
4303
4577
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
4304
4578
|
var AUTO_BACKUP_KEY = "autoBackup";
|
|
4305
4579
|
var AUTO_BACKUP_AT_KEY = "autoBackupAt";
|
|
@@ -4338,6 +4612,7 @@ function QaProvider({
|
|
|
4338
4612
|
React.useEffect(() => {
|
|
4339
4613
|
notesRef.current = notes;
|
|
4340
4614
|
}, [notes]);
|
|
4615
|
+
const staleStillTimer = React.useRef(null);
|
|
4341
4616
|
const applyNotes = React.useCallback((updater) => {
|
|
4342
4617
|
const next = updater(notesRef.current);
|
|
4343
4618
|
notesRef.current = next;
|
|
@@ -4364,7 +4639,15 @@ function QaProvider({
|
|
|
4364
4639
|
const [lang, setLangState] = React.useState(() => {
|
|
4365
4640
|
const saved = storage.getItem(LANG_KEY);
|
|
4366
4641
|
if (saved === "ar" || saved === "en") return saved;
|
|
4367
|
-
|
|
4642
|
+
if (config.rtl) return "ar";
|
|
4643
|
+
try {
|
|
4644
|
+
const html = document.documentElement;
|
|
4645
|
+
if (html.getAttribute("dir") === "rtl") return "ar";
|
|
4646
|
+
if ((html.getAttribute("lang") || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4647
|
+
if ((navigator.language || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4648
|
+
} catch {
|
|
4649
|
+
}
|
|
4650
|
+
return "en";
|
|
4368
4651
|
});
|
|
4369
4652
|
const [guideChecked, setGuideChecked] = React.useState(
|
|
4370
4653
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
@@ -4402,6 +4685,9 @@ function QaProvider({
|
|
|
4402
4685
|
const [simpleMode, setSimpleModeState] = React.useState(
|
|
4403
4686
|
() => storage.getItem(SIMPLE_MODE_KEY) === "1"
|
|
4404
4687
|
);
|
|
4688
|
+
const [developerMode, setDeveloperModeState] = React.useState(
|
|
4689
|
+
() => storage.getItem(DEV_MODE_KEY) === "1"
|
|
4690
|
+
);
|
|
4405
4691
|
const [compactCapture, setCompactCaptureState] = React.useState(
|
|
4406
4692
|
() => storage.getItem(COMPACT_KEY) === "1"
|
|
4407
4693
|
);
|
|
@@ -4731,6 +5017,11 @@ function QaProvider({
|
|
|
4731
5017
|
target: input.target ?? void 0,
|
|
4732
5018
|
severity: input.severity,
|
|
4733
5019
|
status: input.status,
|
|
5020
|
+
wanted: (input.wanted || "").trim() || void 0,
|
|
5021
|
+
why: (input.why || "").trim() || void 0,
|
|
5022
|
+
fixHint: (input.fixHint || "").trim() || void 0,
|
|
5023
|
+
shotEngine: input.shotEngine,
|
|
5024
|
+
origin: input.origin,
|
|
4734
5025
|
journeyRef,
|
|
4735
5026
|
context
|
|
4736
5027
|
};
|
|
@@ -4881,17 +5172,22 @@ function QaProvider({
|
|
|
4881
5172
|
flushPendingDeletes();
|
|
4882
5173
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4883
5174
|
noticeTimers.current.clear();
|
|
5175
|
+
if (staleStillTimer.current) {
|
|
5176
|
+
clearTimeout(staleStillTimer.current);
|
|
5177
|
+
staleStillTimer.current = null;
|
|
5178
|
+
}
|
|
4884
5179
|
releaseFrozenFrame();
|
|
4885
5180
|
};
|
|
4886
5181
|
}, [flushPendingDeletes]);
|
|
5182
|
+
const STILL_HOLD_MS = 1e5;
|
|
4887
5183
|
const startCapture = React.useCallback((prefill) => {
|
|
4888
5184
|
setIsOpen(false);
|
|
4889
5185
|
setCapturePrefill(prefill ?? "");
|
|
4890
5186
|
setCaptureActive(true);
|
|
4891
|
-
setFrozenAt(null);
|
|
5187
|
+
if (!stillIsCurrent()) setFrozenAt(null);
|
|
4892
5188
|
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4893
5189
|
resetExactCaptureDecline();
|
|
4894
|
-
void
|
|
5190
|
+
void freezeOrReuse().then((frame) => {
|
|
4895
5191
|
setFrozenAt(frame?.takenAt ?? null);
|
|
4896
5192
|
setExactStatus(getExactCaptureStatus());
|
|
4897
5193
|
});
|
|
@@ -4899,8 +5195,11 @@ function QaProvider({
|
|
|
4899
5195
|
}, [storage]);
|
|
4900
5196
|
const endCapture = React.useCallback((reopen = true) => {
|
|
4901
5197
|
setCaptureActive(false);
|
|
4902
|
-
|
|
4903
|
-
|
|
5198
|
+
if (staleStillTimer.current) clearTimeout(staleStillTimer.current);
|
|
5199
|
+
staleStillTimer.current = setTimeout(() => {
|
|
5200
|
+
releaseFrozenFrame();
|
|
5201
|
+
staleStillTimer.current = null;
|
|
5202
|
+
}, STILL_HOLD_MS);
|
|
4904
5203
|
if (reopen) setIsOpen(true);
|
|
4905
5204
|
}, []);
|
|
4906
5205
|
const toggleGuide = React.useCallback((key) => {
|
|
@@ -5125,6 +5424,10 @@ function QaProvider({
|
|
|
5125
5424
|
setCompactCaptureState(on);
|
|
5126
5425
|
storage.setItem(COMPACT_KEY, on ? "1" : "0");
|
|
5127
5426
|
}, [storage]);
|
|
5427
|
+
const setDeveloperMode = React.useCallback((on) => {
|
|
5428
|
+
setDeveloperModeState(on);
|
|
5429
|
+
storage.setItem(DEV_MODE_KEY, on ? "1" : "0");
|
|
5430
|
+
}, [storage]);
|
|
5128
5431
|
const setFilter = React.useCallback((patch) => {
|
|
5129
5432
|
setFilterState((prev) => ({ ...prev, ...patch }));
|
|
5130
5433
|
}, []);
|
|
@@ -5464,6 +5767,7 @@ function QaProvider({
|
|
|
5464
5767
|
// Config passthrough
|
|
5465
5768
|
namespace: config.namespace,
|
|
5466
5769
|
brand: config.brand,
|
|
5770
|
+
beta: config.beta === true,
|
|
5467
5771
|
loginField: config.loginField,
|
|
5468
5772
|
credentials: config.credentials,
|
|
5469
5773
|
// Never the raw config value: an unconfigured project falls back to the
|
|
@@ -5572,6 +5876,8 @@ function QaProvider({
|
|
|
5572
5876
|
simpleMode,
|
|
5573
5877
|
setSimpleMode,
|
|
5574
5878
|
compactCapture,
|
|
5879
|
+
developerMode,
|
|
5880
|
+
setDeveloperMode,
|
|
5575
5881
|
setCompactCapture,
|
|
5576
5882
|
exportZip: exportZipFn
|
|
5577
5883
|
};
|
|
@@ -5639,6 +5945,16 @@ var ICONS = {
|
|
|
5639
5945
|
Square: [
|
|
5640
5946
|
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
|
|
5641
5947
|
],
|
|
5948
|
+
Info: [
|
|
5949
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
5950
|
+
["path", { d: "M12 16v-4" }],
|
|
5951
|
+
["path", { d: "M12 8h.01" }]
|
|
5952
|
+
],
|
|
5953
|
+
Mic: [
|
|
5954
|
+
["path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }],
|
|
5955
|
+
["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }],
|
|
5956
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22" }]
|
|
5957
|
+
],
|
|
5642
5958
|
ImagePlus: [
|
|
5643
5959
|
["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
|
|
5644
5960
|
["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
|
|
@@ -5872,7 +6188,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5872
6188
|
};
|
|
5873
6189
|
}
|
|
5874
6190
|
function QaFab() {
|
|
5875
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
6191
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, beta, t } = useQa();
|
|
5876
6192
|
const [pos, setPos] = React.useState(() => loadFabPos(namespace));
|
|
5877
6193
|
const dragRef = React.useRef(null);
|
|
5878
6194
|
const didDragRef = React.useRef(false);
|
|
@@ -5985,6 +6301,15 @@ function QaFab() {
|
|
|
5985
6301
|
}
|
|
5986
6302
|
),
|
|
5987
6303
|
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: isOpen ? "X" : "ClipboardList", size: 24 }),
|
|
6304
|
+
beta && !isOpen && /* @__PURE__ */ jsxRuntime.jsx(
|
|
6305
|
+
"span",
|
|
6306
|
+
{
|
|
6307
|
+
className: "qa-absolute qa-rounded-full qa-text-10 qa-font-bold qa-bg-1 qa-text-mid qa-border qa-border-subtle",
|
|
6308
|
+
style: { bottom: "-6px", left: "50%", transform: "translateX(-50%)", padding: "0 5px", lineHeight: "1.3" },
|
|
6309
|
+
"aria-hidden": "true",
|
|
6310
|
+
children: "beta"
|
|
6311
|
+
}
|
|
6312
|
+
),
|
|
5988
6313
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
5989
6314
|
"span",
|
|
5990
6315
|
{
|
|
@@ -7678,6 +8003,154 @@ function NoteFilterBar() {
|
|
|
7678
8003
|
) })
|
|
7679
8004
|
] });
|
|
7680
8005
|
}
|
|
8006
|
+
|
|
8007
|
+
// src/lib/doctor.ts
|
|
8008
|
+
var HEAVY_PAGE = 6e3;
|
|
8009
|
+
function canvasIsReadable() {
|
|
8010
|
+
try {
|
|
8011
|
+
const c = document.createElement("canvas");
|
|
8012
|
+
c.width = 1;
|
|
8013
|
+
c.height = 1;
|
|
8014
|
+
const ctx = c.getContext("2d");
|
|
8015
|
+
if (!ctx) return false;
|
|
8016
|
+
ctx.fillStyle = "#000";
|
|
8017
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
8018
|
+
c.toDataURL();
|
|
8019
|
+
return true;
|
|
8020
|
+
} catch {
|
|
8021
|
+
return false;
|
|
8022
|
+
}
|
|
8023
|
+
}
|
|
8024
|
+
function taintingImages() {
|
|
8025
|
+
let count = 0;
|
|
8026
|
+
const here = location.origin;
|
|
8027
|
+
for (const img of Array.from(document.images)) {
|
|
8028
|
+
const src = img.currentSrc || img.src;
|
|
8029
|
+
if (!src || src.startsWith("data:") || src.startsWith("blob:")) continue;
|
|
8030
|
+
try {
|
|
8031
|
+
if (new URL(src, here).origin !== here && !img.crossOrigin) count++;
|
|
8032
|
+
} catch {
|
|
8033
|
+
}
|
|
8034
|
+
}
|
|
8035
|
+
return count;
|
|
8036
|
+
}
|
|
8037
|
+
async function runDoctor() {
|
|
8038
|
+
const out = [];
|
|
8039
|
+
if (typeof document === "undefined") return out;
|
|
8040
|
+
const supported = isExactCaptureSupported();
|
|
8041
|
+
const status = getExactCaptureStatus();
|
|
8042
|
+
out.push(
|
|
8043
|
+
!supported ? {
|
|
8044
|
+
label: "Screenshot engine",
|
|
8045
|
+
verdict: "warn",
|
|
8046
|
+
detail: "This browser cannot photograph the tab, so screenshots are re-drawn. Charts, maps and anything on a canvas may come out blank. Chrome or Edge can photograph."
|
|
8047
|
+
} : status === "live" ? { label: "Screenshot engine", verdict: "ok", detail: "Real photographs. This is the accurate one." } : {
|
|
8048
|
+
label: "Screenshot engine",
|
|
8049
|
+
verdict: "warn",
|
|
8050
|
+
detail: "Photographs are switched off, so screenshots are re-drawn and may not match the page."
|
|
8051
|
+
}
|
|
8052
|
+
);
|
|
8053
|
+
const readable = canvasIsReadable();
|
|
8054
|
+
const risky = taintingImages();
|
|
8055
|
+
out.push(
|
|
8056
|
+
!readable ? {
|
|
8057
|
+
label: "Screenshot encoding",
|
|
8058
|
+
verdict: "bad",
|
|
8059
|
+
detail: "This page cannot turn a drawing into an image at all. Re-drawn screenshots will fail here; use photographs."
|
|
8060
|
+
} : risky > 0 ? {
|
|
8061
|
+
label: "Screenshot encoding",
|
|
8062
|
+
verdict: "info",
|
|
8063
|
+
detail: `${risky} image(s) come from another site without permission to be read. They are skipped and appear blank in a re-drawn screenshot; photographs are unaffected.`
|
|
8064
|
+
} : { label: "Screenshot encoding", verdict: "ok", detail: "Nothing on this page blocks screenshot encoding." }
|
|
8065
|
+
);
|
|
8066
|
+
const nodes = document.getElementsByTagName("*").length;
|
|
8067
|
+
out.push({
|
|
8068
|
+
label: "Page size",
|
|
8069
|
+
verdict: nodes > HEAVY_PAGE ? "warn" : "ok",
|
|
8070
|
+
detail: nodes > HEAVY_PAGE ? `${nodes.toLocaleString()} elements. Too many to re-draw without freezing the tab, so re-drawn screenshots are refused here. Photographs work fine.` : `${nodes.toLocaleString()} elements. Comfortable for either engine.`
|
|
8071
|
+
});
|
|
8072
|
+
try {
|
|
8073
|
+
await import('html2canvas');
|
|
8074
|
+
out.push({ label: "Screenshot library", verdict: "ok", detail: "Loads correctly." });
|
|
8075
|
+
} catch {
|
|
8076
|
+
out.push({
|
|
8077
|
+
label: "Screenshot library",
|
|
8078
|
+
verdict: "bad",
|
|
8079
|
+
detail: "Blocked or unreachable \u2014 often a strict Content-Security-Policy. Re-drawn screenshots cannot work here."
|
|
8080
|
+
});
|
|
8081
|
+
}
|
|
8082
|
+
try {
|
|
8083
|
+
const est = await navigator.storage?.estimate?.();
|
|
8084
|
+
if (est?.quota) {
|
|
8085
|
+
const usedPct = Math.round((est.usage ?? 0) / est.quota * 100);
|
|
8086
|
+
out.push({
|
|
8087
|
+
label: "Storage",
|
|
8088
|
+
verdict: usedPct > 90 ? "bad" : usedPct > 70 ? "warn" : "ok",
|
|
8089
|
+
detail: `${usedPct}% of this site's storage used. Notes are kept in this browser until you export them.`
|
|
8090
|
+
});
|
|
8091
|
+
}
|
|
8092
|
+
} catch {
|
|
8093
|
+
}
|
|
8094
|
+
const voice = typeof window !== "undefined" && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
|
|
8095
|
+
out.push({
|
|
8096
|
+
label: "Voice input",
|
|
8097
|
+
verdict: voice ? "ok" : "info",
|
|
8098
|
+
detail: voice ? "Available \u2014 you can speak your notes." : "Not available in this browser. Typing still works."
|
|
8099
|
+
});
|
|
8100
|
+
out.push({
|
|
8101
|
+
label: "Secure connection",
|
|
8102
|
+
verdict: window.isSecureContext ? "ok" : "bad",
|
|
8103
|
+
detail: window.isSecureContext ? "https \u2014 photographs and voice are allowed." : "Not https. Photographs and voice are blocked by the browser on an insecure page."
|
|
8104
|
+
});
|
|
8105
|
+
return out;
|
|
8106
|
+
}
|
|
8107
|
+
|
|
8108
|
+
// src/lib/versionCheck.ts
|
|
8109
|
+
var CACHE_KEY = "qa.versionCheck";
|
|
8110
|
+
var A_DAY = 24 * 60 * 60 * 1e3;
|
|
8111
|
+
var REGISTRY = "https://registry.npmjs.org/qapture2/latest";
|
|
8112
|
+
function isNewer(latest, current) {
|
|
8113
|
+
const norm = (v) => v.replace(/^[^\d]*/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
8114
|
+
const a = norm(latest);
|
|
8115
|
+
const b = norm(current);
|
|
8116
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
8117
|
+
const x = a[i] ?? 0;
|
|
8118
|
+
const y = b[i] ?? 0;
|
|
8119
|
+
if (x !== y) return x > y;
|
|
8120
|
+
}
|
|
8121
|
+
return false;
|
|
8122
|
+
}
|
|
8123
|
+
async function latestVersion() {
|
|
8124
|
+
if (typeof window === "undefined") return null;
|
|
8125
|
+
try {
|
|
8126
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
8127
|
+
if (raw) {
|
|
8128
|
+
const cached = JSON.parse(raw);
|
|
8129
|
+
if (Date.now() - cached.at < A_DAY && cached.latest) return cached.latest;
|
|
8130
|
+
}
|
|
8131
|
+
} catch {
|
|
8132
|
+
}
|
|
8133
|
+
try {
|
|
8134
|
+
const res = await fetch(REGISTRY, { headers: { accept: "application/json" } });
|
|
8135
|
+
if (!res.ok) return null;
|
|
8136
|
+
const body = await res.json();
|
|
8137
|
+
const latest = body.version;
|
|
8138
|
+
if (!latest) return null;
|
|
8139
|
+
try {
|
|
8140
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), latest }));
|
|
8141
|
+
} catch {
|
|
8142
|
+
}
|
|
8143
|
+
return latest;
|
|
8144
|
+
} catch {
|
|
8145
|
+
return null;
|
|
8146
|
+
}
|
|
8147
|
+
}
|
|
8148
|
+
function upgradeHint(latest) {
|
|
8149
|
+
return `npm i qapture2@${latest}`;
|
|
8150
|
+
}
|
|
8151
|
+
|
|
8152
|
+
// src/version.ts
|
|
8153
|
+
var QA_VERSION = "0.9.0" ;
|
|
7681
8154
|
function Section({
|
|
7682
8155
|
icon,
|
|
7683
8156
|
title,
|
|
@@ -7740,8 +8213,24 @@ function SettingsSheet({ onClose }) {
|
|
|
7740
8213
|
setSimpleMode,
|
|
7741
8214
|
compactCapture,
|
|
7742
8215
|
setCompactCapture,
|
|
8216
|
+
developerMode,
|
|
8217
|
+
setDeveloperMode,
|
|
7743
8218
|
notes
|
|
7744
8219
|
} = useQa();
|
|
8220
|
+
const [checks, setChecks] = React.useState([]);
|
|
8221
|
+
const [running, setRunning] = React.useState(false);
|
|
8222
|
+
const [faults2, setFaults] = React.useState(() => readFaults());
|
|
8223
|
+
const [latest, setLatest] = React.useState(null);
|
|
8224
|
+
React.useEffect(() => {
|
|
8225
|
+
let alive = true;
|
|
8226
|
+
void latestVersion().then((v) => {
|
|
8227
|
+
if (alive) setLatest(v);
|
|
8228
|
+
});
|
|
8229
|
+
setFaults(readFaults());
|
|
8230
|
+
return () => {
|
|
8231
|
+
alive = false;
|
|
8232
|
+
};
|
|
8233
|
+
}, []);
|
|
7745
8234
|
const [project, setProject] = React.useState(lastCampaign.project);
|
|
7746
8235
|
const [campaign2, setCampaign] = React.useState(lastCampaign.campaign || suggestCampaignName());
|
|
7747
8236
|
const [tester, setTester] = React.useState(lastCampaign.tester);
|
|
@@ -8016,6 +8505,20 @@ function SettingsSheet({ onClose }) {
|
|
|
8016
8505
|
] }),
|
|
8017
8506
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8018
8507
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Settings", title: t("settings"), children: [
|
|
8508
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8509
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8510
|
+
"input",
|
|
8511
|
+
{
|
|
8512
|
+
type: "checkbox",
|
|
8513
|
+
checked: developerMode,
|
|
8514
|
+
onChange: (e) => setDeveloperMode(e.target.checked)
|
|
8515
|
+
}
|
|
8516
|
+
),
|
|
8517
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
8518
|
+
t("dev_mode_label"),
|
|
8519
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-mid", children: t("dev_mode_hint") })
|
|
8520
|
+
] })
|
|
8521
|
+
] }),
|
|
8019
8522
|
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-center qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8020
8523
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8021
8524
|
"input",
|
|
@@ -8038,6 +8541,91 @@ function SettingsSheet({ onClose }) {
|
|
|
8038
8541
|
),
|
|
8039
8542
|
t("compact_mode")
|
|
8040
8543
|
] })
|
|
8544
|
+
] }),
|
|
8545
|
+
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Info", title: t("diag_title"), children: [
|
|
8546
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-text-10 qa-text-mid", children: t("diag_hint") }),
|
|
8547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8548
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
8549
|
+
"button",
|
|
8550
|
+
{
|
|
8551
|
+
type: "button",
|
|
8552
|
+
onClick: () => void (async () => {
|
|
8553
|
+
setRunning(true);
|
|
8554
|
+
try {
|
|
8555
|
+
setChecks(await runDoctor());
|
|
8556
|
+
} finally {
|
|
8557
|
+
setRunning(false);
|
|
8558
|
+
}
|
|
8559
|
+
})(),
|
|
8560
|
+
disabled: running,
|
|
8561
|
+
"data-qa-doctor": "true",
|
|
8562
|
+
className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-hi qa-focus-ring",
|
|
8563
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8564
|
+
children: [
|
|
8565
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
|
|
8566
|
+
running ? t("diag_running") : t("diag_run")
|
|
8567
|
+
]
|
|
8568
|
+
}
|
|
8569
|
+
),
|
|
8570
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-10 qa-text-mid", children: [
|
|
8571
|
+
t("diag_version", { v: QA_VERSION }),
|
|
8572
|
+
latest && isNewer(latest, QA_VERSION) ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
8573
|
+
" \xB7 ",
|
|
8574
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-warn", children: t("diag_outdated", { v: latest }) }),
|
|
8575
|
+
/* @__PURE__ */ jsxRuntime.jsx("code", { className: "qa-ms-1", children: upgradeHint(latest) })
|
|
8576
|
+
] }) : latest ? ` \xB7 ${t("diag_current")}` : ""
|
|
8577
|
+
] })
|
|
8578
|
+
] }),
|
|
8579
|
+
checks.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-1", "data-qa-doctor-results": "true", children: checks.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-text-11 qa-text-hi", children: [
|
|
8580
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8581
|
+
"span",
|
|
8582
|
+
{
|
|
8583
|
+
"aria-hidden": true,
|
|
8584
|
+
className: c.verdict === "ok" ? "qa-text-success" : c.verdict === "bad" ? "qa-text-danger" : c.verdict === "warn" ? "qa-text-warn" : "qa-text-mid",
|
|
8585
|
+
children: c.verdict === "ok" ? "\u25CF" : c.verdict === "bad" ? "\u25B2" : c.verdict === "warn" ? "\u25B2" : "\u25CB"
|
|
8586
|
+
}
|
|
8587
|
+
),
|
|
8588
|
+
" ",
|
|
8589
|
+
/* @__PURE__ */ jsxRuntime.jsx("strong", { children: c.label }),
|
|
8590
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-mid qa-ms-3", children: c.detail })
|
|
8591
|
+
] }, c.label)) }),
|
|
8592
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8593
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("diag_faults", { n: faults2.length }) }),
|
|
8594
|
+
faults2.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
8595
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8596
|
+
"button",
|
|
8597
|
+
{
|
|
8598
|
+
type: "button",
|
|
8599
|
+
onClick: () => void navigator.clipboard?.writeText(faultsAsText(QA_VERSION)),
|
|
8600
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-hi",
|
|
8601
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8602
|
+
children: t("copy_prompt")
|
|
8603
|
+
}
|
|
8604
|
+
),
|
|
8605
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8606
|
+
"button",
|
|
8607
|
+
{
|
|
8608
|
+
type: "button",
|
|
8609
|
+
onClick: () => {
|
|
8610
|
+
clearFaults();
|
|
8611
|
+
setFaults([]);
|
|
8612
|
+
},
|
|
8613
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-mid",
|
|
8614
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8615
|
+
children: t("clear_all")
|
|
8616
|
+
}
|
|
8617
|
+
)
|
|
8618
|
+
] })
|
|
8619
|
+
] }),
|
|
8620
|
+
faults2.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "qa-space-y-0.5", children: faults2.slice(0, 5).map((f, i) => /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-text-10 qa-text-mid", children: [
|
|
8621
|
+
/* @__PURE__ */ jsxRuntime.jsxs("code", { children: [
|
|
8622
|
+
"[",
|
|
8623
|
+
f.where,
|
|
8624
|
+
"]"
|
|
8625
|
+
] }),
|
|
8626
|
+
" ",
|
|
8627
|
+
f.what
|
|
8628
|
+
] }, `${f.at}-${i}`)) })
|
|
8041
8629
|
] })
|
|
8042
8630
|
] })
|
|
8043
8631
|
]
|
|
@@ -8372,17 +8960,17 @@ function QaPanel() {
|
|
|
8372
8960
|
if (typeof window === "undefined") return 0;
|
|
8373
8961
|
const vv = window.visualViewport;
|
|
8374
8962
|
if (!vv) return 0;
|
|
8375
|
-
const
|
|
8376
|
-
if (
|
|
8963
|
+
const overlap2 = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
8964
|
+
if (overlap2 <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
|
|
8377
8965
|
const panel = panelRef.current;
|
|
8378
8966
|
if (!panel) return 0;
|
|
8379
8967
|
const root2 = panel.getRootNode();
|
|
8380
8968
|
const active = root2.activeElement;
|
|
8381
8969
|
if (!active || !panel.contains(active)) return 0;
|
|
8382
8970
|
const tag = active.tagName;
|
|
8383
|
-
if (tag === "TEXTAREA") return Math.round(
|
|
8971
|
+
if (tag === "TEXTAREA") return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8384
8972
|
if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
|
|
8385
|
-
return Math.round(
|
|
8973
|
+
return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8386
8974
|
}
|
|
8387
8975
|
return 0;
|
|
8388
8976
|
} catch {
|
|
@@ -9322,6 +9910,209 @@ function unlockPageScroll() {
|
|
|
9322
9910
|
window.removeEventListener("touchmove", preventScroll, LISTENER_OPTIONS);
|
|
9323
9911
|
}
|
|
9324
9912
|
}
|
|
9913
|
+
|
|
9914
|
+
// src/lib/origin.ts
|
|
9915
|
+
function fiberOf(el) {
|
|
9916
|
+
for (const key in el) {
|
|
9917
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
9918
|
+
return el[key] ?? null;
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
return null;
|
|
9922
|
+
}
|
|
9923
|
+
function nameOf(fiber) {
|
|
9924
|
+
const type = fiber.type;
|
|
9925
|
+
if (!type || typeof type === "string") return null;
|
|
9926
|
+
const direct = type.displayName || type.name;
|
|
9927
|
+
if (direct) return direct;
|
|
9928
|
+
const inner = type.render?.displayName || type.render?.name;
|
|
9929
|
+
return inner || null;
|
|
9930
|
+
}
|
|
9931
|
+
function tidyPath(file) {
|
|
9932
|
+
const marks = ["/src/", "/app/", "/components/", "/pages/", "/lib/"];
|
|
9933
|
+
for (const mark of marks) {
|
|
9934
|
+
const at = file.lastIndexOf(mark);
|
|
9935
|
+
if (at !== -1) return file.slice(at + 1);
|
|
9936
|
+
}
|
|
9937
|
+
return file.split("/").slice(-2).join("/");
|
|
9938
|
+
}
|
|
9939
|
+
var MAX_WALK = 30;
|
|
9940
|
+
function resolveOrigin(el) {
|
|
9941
|
+
if (!el || typeof window === "undefined") return void 0;
|
|
9942
|
+
let fiber;
|
|
9943
|
+
try {
|
|
9944
|
+
fiber = fiberOf(el);
|
|
9945
|
+
} catch {
|
|
9946
|
+
return void 0;
|
|
9947
|
+
}
|
|
9948
|
+
if (!fiber) return void 0;
|
|
9949
|
+
const out = {};
|
|
9950
|
+
let hops = 0;
|
|
9951
|
+
for (let f = fiber; f && hops < MAX_WALK; f = f.return ?? null, hops++) {
|
|
9952
|
+
if (!out.component) {
|
|
9953
|
+
const name = nameOf(f);
|
|
9954
|
+
if (name) out.component = name;
|
|
9955
|
+
}
|
|
9956
|
+
if (!out.file) {
|
|
9957
|
+
const src = f._debugSource;
|
|
9958
|
+
if (src?.fileName) {
|
|
9959
|
+
out.file = tidyPath(src.fileName);
|
|
9960
|
+
if (typeof src.lineNumber === "number") out.line = src.lineNumber;
|
|
9961
|
+
}
|
|
9962
|
+
}
|
|
9963
|
+
if (out.component && out.file) break;
|
|
9964
|
+
}
|
|
9965
|
+
return out.component || out.file ? out : void 0;
|
|
9966
|
+
}
|
|
9967
|
+
|
|
9968
|
+
// src/lib/duplicate.ts
|
|
9969
|
+
var NOISE = /* @__PURE__ */ new Set([
|
|
9970
|
+
"the",
|
|
9971
|
+
"this",
|
|
9972
|
+
"that",
|
|
9973
|
+
"a",
|
|
9974
|
+
"an",
|
|
9975
|
+
"is",
|
|
9976
|
+
"it",
|
|
9977
|
+
"to",
|
|
9978
|
+
"and",
|
|
9979
|
+
"of",
|
|
9980
|
+
"in",
|
|
9981
|
+
"on",
|
|
9982
|
+
"for",
|
|
9983
|
+
"be",
|
|
9984
|
+
"should",
|
|
9985
|
+
"i",
|
|
9986
|
+
"we",
|
|
9987
|
+
"please",
|
|
9988
|
+
"here",
|
|
9989
|
+
"\u0647\u0630\u0627",
|
|
9990
|
+
"\u0647\u0630\u0647",
|
|
9991
|
+
"\u0641\u064A",
|
|
9992
|
+
"\u0645\u0646",
|
|
9993
|
+
"\u0639\u0644\u0649",
|
|
9994
|
+
"\u0627\u0644\u0649",
|
|
9995
|
+
"\u0625\u0644\u0649",
|
|
9996
|
+
"\u0627\u0646",
|
|
9997
|
+
"\u0623\u0646",
|
|
9998
|
+
"\u0648",
|
|
9999
|
+
"\u0627\u0644"
|
|
10000
|
+
]);
|
|
10001
|
+
function words(text) {
|
|
10002
|
+
return new Set(
|
|
10003
|
+
(text || "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !NOISE.has(w))
|
|
10004
|
+
);
|
|
10005
|
+
}
|
|
10006
|
+
function overlap(a, b) {
|
|
10007
|
+
if (!a.size || !b.size) return 0;
|
|
10008
|
+
let shared = 0;
|
|
10009
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
10010
|
+
return shared / (a.size + b.size - shared);
|
|
10011
|
+
}
|
|
10012
|
+
var SAME_ENOUGH = 0.7;
|
|
10013
|
+
function findDuplicate(text, selector, existing) {
|
|
10014
|
+
const typed = words(text);
|
|
10015
|
+
if (typed.size < 3) return null;
|
|
10016
|
+
for (const note of existing) {
|
|
10017
|
+
if (selector && note.target?.selector && note.target.selector !== selector) continue;
|
|
10018
|
+
if (selector && !note.target?.selector) continue;
|
|
10019
|
+
if (!selector && note.target?.selector) continue;
|
|
10020
|
+
if (overlap(typed, words(note.description)) >= SAME_ENOUGH) return note;
|
|
10021
|
+
}
|
|
10022
|
+
return null;
|
|
10023
|
+
}
|
|
10024
|
+
function recogniser() {
|
|
10025
|
+
if (typeof window === "undefined") return null;
|
|
10026
|
+
const w = window;
|
|
10027
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
10028
|
+
}
|
|
10029
|
+
function VoiceButton({
|
|
10030
|
+
onText,
|
|
10031
|
+
onInterim
|
|
10032
|
+
}) {
|
|
10033
|
+
const { lang, t } = useQa();
|
|
10034
|
+
const [listening, setListening] = React.useState(false);
|
|
10035
|
+
const [failed, setFailed] = React.useState(null);
|
|
10036
|
+
const ref = React.useRef(null);
|
|
10037
|
+
React.useEffect(() => () => {
|
|
10038
|
+
ref.current?.abort();
|
|
10039
|
+
ref.current = null;
|
|
10040
|
+
}, []);
|
|
10041
|
+
const Ctor = recogniser();
|
|
10042
|
+
if (!Ctor) return null;
|
|
10043
|
+
const stop = () => {
|
|
10044
|
+
ref.current?.stop();
|
|
10045
|
+
ref.current = null;
|
|
10046
|
+
setListening(false);
|
|
10047
|
+
onInterim?.("");
|
|
10048
|
+
};
|
|
10049
|
+
const start = () => {
|
|
10050
|
+
if (listening) {
|
|
10051
|
+
stop();
|
|
10052
|
+
return;
|
|
10053
|
+
}
|
|
10054
|
+
setFailed(null);
|
|
10055
|
+
let rec;
|
|
10056
|
+
try {
|
|
10057
|
+
rec = new Ctor();
|
|
10058
|
+
} catch {
|
|
10059
|
+
setFailed(t("voice_failed"));
|
|
10060
|
+
return;
|
|
10061
|
+
}
|
|
10062
|
+
rec.lang = lang === "ar" ? "ar-IQ" : "en-US";
|
|
10063
|
+
rec.continuous = true;
|
|
10064
|
+
rec.interimResults = true;
|
|
10065
|
+
rec.onresult = (e) => {
|
|
10066
|
+
let done = "";
|
|
10067
|
+
let partial = "";
|
|
10068
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
10069
|
+
const r = e.results[i];
|
|
10070
|
+
const text = r[0]?.transcript ?? "";
|
|
10071
|
+
if (r.isFinal) done += text;
|
|
10072
|
+
else partial += text;
|
|
10073
|
+
}
|
|
10074
|
+
if (done.trim()) onText(done.trim());
|
|
10075
|
+
onInterim?.(partial);
|
|
10076
|
+
};
|
|
10077
|
+
rec.onerror = (e) => {
|
|
10078
|
+
if (e.error && e.error !== "no-speech" && e.error !== "aborted") {
|
|
10079
|
+
setFailed(e.error === "not-allowed" ? t("voice_denied") : t("voice_failed"));
|
|
10080
|
+
}
|
|
10081
|
+
setListening(false);
|
|
10082
|
+
onInterim?.("");
|
|
10083
|
+
};
|
|
10084
|
+
rec.onend = () => {
|
|
10085
|
+
setListening(false);
|
|
10086
|
+
onInterim?.("");
|
|
10087
|
+
};
|
|
10088
|
+
try {
|
|
10089
|
+
rec.start();
|
|
10090
|
+
ref.current = rec;
|
|
10091
|
+
setListening(true);
|
|
10092
|
+
} catch {
|
|
10093
|
+
setFailed(t("voice_failed"));
|
|
10094
|
+
}
|
|
10095
|
+
};
|
|
10096
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1.5", children: [
|
|
10097
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10098
|
+
"button",
|
|
10099
|
+
{
|
|
10100
|
+
type: "button",
|
|
10101
|
+
onClick: start,
|
|
10102
|
+
"aria-pressed": listening,
|
|
10103
|
+
title: listening ? t("voice_stop") : t("voice_start"),
|
|
10104
|
+
"data-qa-voice": listening ? "listening" : "idle",
|
|
10105
|
+
className: `qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-11 qa-focus-ring ${listening ? "qa-bg-danger-tint qa-text-danger" : "qa-bg-2 qa-text-mid"}`,
|
|
10106
|
+
style: { cursor: "pointer" },
|
|
10107
|
+
children: [
|
|
10108
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: listening ? "Square" : "Mic", size: 12 }),
|
|
10109
|
+
listening ? t("voice_stop") : t("voice_start")
|
|
10110
|
+
]
|
|
10111
|
+
}
|
|
10112
|
+
),
|
|
10113
|
+
failed && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-danger", children: failed })
|
|
10114
|
+
] });
|
|
10115
|
+
}
|
|
9325
10116
|
var DRAG_THRESHOLD2 = 6;
|
|
9326
10117
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
9327
10118
|
var MIN_REGION_SIZE = 8;
|
|
@@ -9376,6 +10167,8 @@ function CaptureMode() {
|
|
|
9376
10167
|
setCompactCapture,
|
|
9377
10168
|
exactShots,
|
|
9378
10169
|
photographNow,
|
|
10170
|
+
developerMode,
|
|
10171
|
+
notes,
|
|
9379
10172
|
capturePrefill
|
|
9380
10173
|
} = useQa();
|
|
9381
10174
|
const coarse = useCoarsePointer();
|
|
@@ -9391,12 +10184,21 @@ function CaptureMode() {
|
|
|
9391
10184
|
const [shot, setShot] = React.useState(null);
|
|
9392
10185
|
const stillRef = React.useRef(null);
|
|
9393
10186
|
const [notesFromThisShot, setNotesFromThisShot] = React.useState(0);
|
|
10187
|
+
const [wanted, setWanted] = React.useState("");
|
|
10188
|
+
const [why, setWhy] = React.useState("");
|
|
10189
|
+
const [fixHint, setFixHint] = React.useState("");
|
|
10190
|
+
const [origin, setOrigin] = React.useState(void 0);
|
|
10191
|
+
const [spoken, setSpoken] = React.useState("");
|
|
9394
10192
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
9395
10193
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
9396
10194
|
const [capturing, setCapturing] = React.useState(false);
|
|
9397
10195
|
const [captureError, setCaptureError] = React.useState(false);
|
|
9398
10196
|
const [tooHeavy, setTooHeavy] = React.useState(false);
|
|
9399
10197
|
const [description, setDescription] = React.useState(capturePrefill);
|
|
10198
|
+
const twin = React.useMemo(
|
|
10199
|
+
() => description.trim().length > 8 ? findDuplicate(description, selection?.selector, notes) : null,
|
|
10200
|
+
[description, selection, notes]
|
|
10201
|
+
);
|
|
9400
10202
|
const [severity, setSeverity] = React.useState("bug");
|
|
9401
10203
|
const [targetForensics, setTargetForensics] = React.useState(void 0);
|
|
9402
10204
|
const taRef = React.useRef(null);
|
|
@@ -9538,6 +10340,7 @@ function CaptureMode() {
|
|
|
9538
10340
|
if (regionRect) {
|
|
9539
10341
|
const sel2 = { kind: "region", rect: regionRect };
|
|
9540
10342
|
setTargetForensics(void 0);
|
|
10343
|
+
setOrigin(void 0);
|
|
9541
10344
|
if (coarse) {
|
|
9542
10345
|
setCandidate(sel2);
|
|
9543
10346
|
setPhase("confirming");
|
|
@@ -9560,6 +10363,7 @@ function CaptureMode() {
|
|
|
9560
10363
|
tagName: el.tagName.toLowerCase()
|
|
9561
10364
|
};
|
|
9562
10365
|
setTargetForensics(collectTargetForensics(el));
|
|
10366
|
+
setOrigin(resolveOrigin(el));
|
|
9563
10367
|
if (coarse) {
|
|
9564
10368
|
setCandidate(sel);
|
|
9565
10369
|
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
@@ -9704,8 +10508,12 @@ function CaptureMode() {
|
|
|
9704
10508
|
setShot(null);
|
|
9705
10509
|
setShotEngine(null);
|
|
9706
10510
|
setDescription("");
|
|
10511
|
+
setWanted("");
|
|
10512
|
+
setWhy("");
|
|
10513
|
+
setFixHint("");
|
|
9707
10514
|
setSeverity("bug");
|
|
9708
10515
|
setTargetForensics(void 0);
|
|
10516
|
+
setOrigin(void 0);
|
|
9709
10517
|
setCaptureError(false);
|
|
9710
10518
|
}, []);
|
|
9711
10519
|
const save = async (keepGoing = false) => {
|
|
@@ -9725,9 +10533,14 @@ function CaptureMode() {
|
|
|
9725
10533
|
};
|
|
9726
10534
|
await addNote({
|
|
9727
10535
|
description,
|
|
10536
|
+
wanted,
|
|
10537
|
+
why,
|
|
10538
|
+
fixHint,
|
|
9728
10539
|
screenshot: shot ?? void 0,
|
|
10540
|
+
shotEngine: shotEngine ?? void 0,
|
|
9729
10541
|
target,
|
|
9730
10542
|
severity,
|
|
10543
|
+
origin,
|
|
9731
10544
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9732
10545
|
});
|
|
9733
10546
|
if (keepGoing) {
|
|
@@ -10357,8 +11170,8 @@ function CaptureMode() {
|
|
|
10357
11170
|
]
|
|
10358
11171
|
}
|
|
10359
11172
|
),
|
|
10360
|
-
/* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
10361
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
11173
|
+
developerMode && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
11174
|
+
developerMode && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
10362
11175
|
"div",
|
|
10363
11176
|
{
|
|
10364
11177
|
role: "group",
|
|
@@ -10388,24 +11201,98 @@ function CaptureMode() {
|
|
|
10388
11201
|
]
|
|
10389
11202
|
}
|
|
10390
11203
|
),
|
|
10391
|
-
/* @__PURE__ */ jsxRuntime.
|
|
10392
|
-
"
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
11204
|
+
twin && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-10 qa-text-warn", "data-qa-duplicate": "true", children: [
|
|
11205
|
+
t("dup_notice", { when: new Date(twin.timestamp).toLocaleTimeString() }),
|
|
11206
|
+
" \u201C",
|
|
11207
|
+
twin.description.slice(0, 70),
|
|
11208
|
+
twin.description.length > 70 ? "\u2026" : "",
|
|
11209
|
+
"\u201D"
|
|
11210
|
+
] }),
|
|
11211
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11212
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11213
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_observed") }),
|
|
11214
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11215
|
+
VoiceButton,
|
|
11216
|
+
{
|
|
11217
|
+
onText: (text) => setDescription((d) => d ? `${d} ${text}` : text),
|
|
11218
|
+
onInterim: setSpoken
|
|
10402
11219
|
}
|
|
10403
|
-
|
|
10404
|
-
|
|
10405
|
-
|
|
10406
|
-
|
|
10407
|
-
|
|
10408
|
-
|
|
11220
|
+
)
|
|
11221
|
+
] }),
|
|
11222
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11223
|
+
"textarea",
|
|
11224
|
+
{
|
|
11225
|
+
ref: taRef,
|
|
11226
|
+
value: description + (spoken ? ` ${spoken}` : ""),
|
|
11227
|
+
onChange: (e) => setDescription(e.target.value),
|
|
11228
|
+
onKeyDown: (e) => {
|
|
11229
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11230
|
+
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
11231
|
+
e.preventDefault();
|
|
11232
|
+
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
11233
|
+
}
|
|
11234
|
+
},
|
|
11235
|
+
rows: 2,
|
|
11236
|
+
placeholder: t("q_observed_hint"),
|
|
11237
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
11238
|
+
}
|
|
11239
|
+
)
|
|
11240
|
+
] }),
|
|
11241
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11242
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11243
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_wanted") }),
|
|
11244
|
+
/* @__PURE__ */ jsxRuntime.jsx(VoiceButton, { onText: (x) => setWanted((d) => d ? `${d} ${x}` : x) })
|
|
11245
|
+
] }),
|
|
11246
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11247
|
+
"textarea",
|
|
11248
|
+
{
|
|
11249
|
+
value: wanted,
|
|
11250
|
+
onChange: (e) => setWanted(e.target.value),
|
|
11251
|
+
onKeyDown: (e) => {
|
|
11252
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11253
|
+
},
|
|
11254
|
+
rows: 2,
|
|
11255
|
+
placeholder: t("q_wanted_hint"),
|
|
11256
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
11257
|
+
}
|
|
11258
|
+
)
|
|
11259
|
+
] }),
|
|
11260
|
+
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11261
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11262
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_why") }),
|
|
11263
|
+
/* @__PURE__ */ jsxRuntime.jsx(VoiceButton, { onText: (x) => setWhy((d) => d ? `${d} ${x}` : x) })
|
|
11264
|
+
] }),
|
|
11265
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11266
|
+
"textarea",
|
|
11267
|
+
{
|
|
11268
|
+
value: why,
|
|
11269
|
+
onChange: (e) => setWhy(e.target.value),
|
|
11270
|
+
onKeyDown: (e) => {
|
|
11271
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11272
|
+
},
|
|
11273
|
+
rows: 1,
|
|
11274
|
+
placeholder: t("q_why_hint"),
|
|
11275
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
11276
|
+
}
|
|
11277
|
+
)
|
|
11278
|
+
] }),
|
|
11279
|
+
developerMode && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11280
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_fix") }),
|
|
11281
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11282
|
+
"textarea",
|
|
11283
|
+
{
|
|
11284
|
+
value: fixHint,
|
|
11285
|
+
onChange: (e) => setFixHint(e.target.value),
|
|
11286
|
+
rows: 1,
|
|
11287
|
+
placeholder: t("q_fix_hint"),
|
|
11288
|
+
className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
|
|
11289
|
+
}
|
|
11290
|
+
)
|
|
11291
|
+
] }),
|
|
11292
|
+
developerMode && origin && (origin.component || origin.file) && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-10 qa-text-mid", "data-qa-origin": "true", children: [
|
|
11293
|
+
origin.component ?? "\u2014",
|
|
11294
|
+
origin.file ? ` \xB7 ${origin.file}${origin.line ? `:${origin.line}` : ""}` : ""
|
|
11295
|
+
] }),
|
|
10409
11296
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
10410
11297
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
10411
11298
|
"button",
|
|
@@ -10447,8 +11334,7 @@ function CaptureMode() {
|
|
|
10447
11334
|
] }),
|
|
10448
11335
|
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
|
|
10449
11336
|
t("save_hint"),
|
|
10450
|
-
|
|
10451
|
-
t("severity_keys")
|
|
11337
|
+
developerMode ? ` \xB7 ${t("severity_keys")}` : ""
|
|
10452
11338
|
] })
|
|
10453
11339
|
] })
|
|
10454
11340
|
]
|
|
@@ -10656,5 +11542,5 @@ function Qapture({ config }) {
|
|
|
10656
11542
|
exports.Qapture = Qapture;
|
|
10657
11543
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10658
11544
|
exports.initQaStudio = initQaStudio;
|
|
10659
|
-
//# sourceMappingURL=chunk-
|
|
10660
|
-
//# sourceMappingURL=chunk-
|
|
11545
|
+
//# sourceMappingURL=chunk-5EWNGJ2N.cjs.map
|
|
11546
|
+
//# sourceMappingURL=chunk-5EWNGJ2N.cjs.map
|