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
|
@@ -169,6 +169,7 @@ var DEFAULTS = {
|
|
|
169
169
|
brandLabel: "Qapture",
|
|
170
170
|
loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
|
|
171
171
|
rtl: false,
|
|
172
|
+
beta: false,
|
|
172
173
|
visible: void 0,
|
|
173
174
|
alwaysVisible: false,
|
|
174
175
|
hotkey: "shift+alt+q",
|
|
@@ -313,9 +314,9 @@ function warnMissingArabic(loginField, credentials, journey, warnings) {
|
|
|
313
314
|
if (c.hint !== void 0 && !hasAr(c.hint)) missing.push(`credentials role="${c.role}" (hint.ar)`);
|
|
314
315
|
}
|
|
315
316
|
if (!missing.length) return;
|
|
316
|
-
const
|
|
317
|
-
const shown = missing.slice(0,
|
|
318
|
-
const rest = missing.length >
|
|
317
|
+
const LIMIT2 = 6;
|
|
318
|
+
const shown = missing.slice(0, LIMIT2).join("; ");
|
|
319
|
+
const rest = missing.length > LIMIT2 ? ` (+${missing.length - LIMIT2} more)` : "";
|
|
319
320
|
warnings.push(
|
|
320
321
|
`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.`
|
|
321
322
|
);
|
|
@@ -340,6 +341,7 @@ function validateConfig(input) {
|
|
|
340
341
|
journey: [],
|
|
341
342
|
preamble: null,
|
|
342
343
|
rtl: DEFAULTS.rtl,
|
|
344
|
+
beta: DEFAULTS.beta,
|
|
343
345
|
visible: DEFAULTS.visible,
|
|
344
346
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
345
347
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -360,6 +362,7 @@ function validateConfig(input) {
|
|
|
360
362
|
journey: [],
|
|
361
363
|
preamble: null,
|
|
362
364
|
rtl: DEFAULTS.rtl,
|
|
365
|
+
beta: DEFAULTS.beta,
|
|
363
366
|
visible: DEFAULTS.visible,
|
|
364
367
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
365
368
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -395,6 +398,7 @@ function validateConfig(input) {
|
|
|
395
398
|
const journey = raw["journey"] !== void 0 ? coerceJourney(raw["journey"], warnings) : [];
|
|
396
399
|
const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
|
|
397
400
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
401
|
+
const beta = typeof raw["beta"] === "boolean" ? raw["beta"] : DEFAULTS.beta;
|
|
398
402
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
399
403
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
400
404
|
const captureHotkey = isNonEmptyString(raw["captureHotkey"]) ? raw["captureHotkey"].trim() : DEFAULTS.captureHotkey;
|
|
@@ -417,6 +421,7 @@ function validateConfig(input) {
|
|
|
417
421
|
journey,
|
|
418
422
|
preamble,
|
|
419
423
|
rtl,
|
|
424
|
+
beta,
|
|
420
425
|
visible,
|
|
421
426
|
alwaysVisible,
|
|
422
427
|
hotkey,
|
|
@@ -1728,6 +1733,32 @@ var STR = {
|
|
|
1728
1733
|
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.",
|
|
1729
1734
|
no_shot: "no screenshot (location saved)",
|
|
1730
1735
|
annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
|
|
1736
|
+
// Asked as two plain questions, never as jargon. "Expected behaviour" is
|
|
1737
|
+
// a phrase from a bug tracker; "what should have happened" is a question
|
|
1738
|
+
// anybody can answer.
|
|
1739
|
+
q_observed: "What happened?",
|
|
1740
|
+
q_observed_hint: "What you saw. Plain words are fine.",
|
|
1741
|
+
q_wanted: "What should have happened?",
|
|
1742
|
+
q_wanted_hint: "What you expected instead. This is the important one.",
|
|
1743
|
+
q_why: "Why does it matter? (optional)",
|
|
1744
|
+
q_why_hint: "What you were trying to get done.",
|
|
1745
|
+
q_fix: "Suggested fix (optional)",
|
|
1746
|
+
q_fix_hint: "Passed on as a suggestion, not an instruction.",
|
|
1747
|
+
voice_start: "Speak",
|
|
1748
|
+
voice_stop: "Stop",
|
|
1749
|
+
voice_failed: "Dictation did not start",
|
|
1750
|
+
voice_denied: "Microphone blocked",
|
|
1751
|
+
dev_mode_label: "Developer mode",
|
|
1752
|
+
diag_title: "Diagnostics",
|
|
1753
|
+
diag_hint: "Checks the handful of things that actually stop screenshots working on a page. Run it if something looks wrong.",
|
|
1754
|
+
diag_run: "Check this page",
|
|
1755
|
+
diag_running: "Checking\u2026",
|
|
1756
|
+
diag_version: "Version {v}",
|
|
1757
|
+
diag_outdated: "Update available: {v} \u2014",
|
|
1758
|
+
diag_current: "up to date",
|
|
1759
|
+
diag_faults: "Recorded problems: {n}",
|
|
1760
|
+
dup_notice: "You already said something like this at {when}:",
|
|
1761
|
+
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.",
|
|
1731
1762
|
save_point: "Save point",
|
|
1732
1763
|
save_next: "Save + next",
|
|
1733
1764
|
save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
|
|
@@ -1943,6 +1974,29 @@ var STR = {
|
|
|
1943
1974
|
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.",
|
|
1944
1975
|
no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
|
|
1945
1976
|
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)",
|
|
1977
|
+
q_observed: "\u0645\u0627 \u0627\u0644\u0630\u064A \u062D\u062F\u062B\u061F",
|
|
1978
|
+
q_observed_hint: "\u0645\u0627 \u0631\u0623\u064A\u062A\u0647. \u0628\u0643\u0644\u0645\u0627\u062A\u0643 \u0627\u0644\u0639\u0627\u062F\u064A\u0629.",
|
|
1979
|
+
q_wanted: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0627\u0646 \u064A\u062C\u0628 \u0623\u0646 \u064A\u062D\u062F\u062B\u061F",
|
|
1980
|
+
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.",
|
|
1981
|
+
q_why: "\u0644\u0645\u0627\u0630\u0627 \u064A\u0647\u0645\u0651\u0643\u061F (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
1982
|
+
q_why_hint: "\u0645\u0627 \u0627\u0644\u0630\u064A \u0643\u0646\u062A \u062A\u062D\u0627\u0648\u0644 \u0625\u0646\u062C\u0627\u0632\u0647.",
|
|
1983
|
+
q_fix: "\u0627\u0642\u062A\u0631\u0627\u062D \u0644\u0644\u062D\u0644 (\u0627\u062E\u062A\u064A\u0627\u0631\u064A)",
|
|
1984
|
+
q_fix_hint: "\u064A\u064F\u0645\u0631\u064E\u0651\u0631 \u0643\u0627\u0642\u062A\u0631\u0627\u062D\u060C \u0644\u0627 \u0643\u0623\u0645\u0631.",
|
|
1985
|
+
voice_start: "\u062A\u062D\u062F\u0651\u062B",
|
|
1986
|
+
voice_stop: "\u0625\u064A\u0642\u0627\u0641",
|
|
1987
|
+
voice_failed: "\u0644\u0645 \u064A\u0628\u062F\u0623 \u0627\u0644\u0625\u0645\u0644\u0627\u0621 \u0627\u0644\u0635\u0648\u062A\u064A",
|
|
1988
|
+
voice_denied: "\u0627\u0644\u0645\u064A\u0643\u0631\u0648\u0641\u0648\u0646 \u0645\u062D\u062C\u0648\u0628",
|
|
1989
|
+
dev_mode_label: "\u0648\u0636\u0639 \u0627\u0644\u0645\u0637\u0648\u0651\u0631",
|
|
1990
|
+
diag_title: "\u0627\u0644\u062A\u0634\u062E\u064A\u0635",
|
|
1991
|
+
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.",
|
|
1992
|
+
diag_run: "\u0627\u0641\u062D\u0635 \u0647\u0630\u0647 \u0627\u0644\u0635\u0641\u062D\u0629",
|
|
1993
|
+
diag_running: "\u062C\u0627\u0631\u064D \u0627\u0644\u0641\u062D\u0635\u2026",
|
|
1994
|
+
diag_version: "\u0627\u0644\u0625\u0635\u062F\u0627\u0631 {v}",
|
|
1995
|
+
diag_outdated: "\u064A\u062A\u0648\u0641\u0631 \u062A\u062D\u062F\u064A\u062B: {v} \u2014",
|
|
1996
|
+
diag_current: "\u0645\u062D\u062F\u0651\u062B",
|
|
1997
|
+
diag_faults: "\u0645\u0634\u0643\u0644\u0627\u062A \u0645\u064F\u0633\u062C\u0651\u0644\u0629: {n}",
|
|
1998
|
+
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}:",
|
|
1999
|
+
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.",
|
|
1946
2000
|
save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
|
|
1947
2001
|
save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
|
|
1948
2002
|
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.",
|
|
@@ -2383,6 +2437,25 @@ function getExactCaptureStatus() {
|
|
|
2383
2437
|
function getFrozenFrame() {
|
|
2384
2438
|
return frozen;
|
|
2385
2439
|
}
|
|
2440
|
+
var STILL_REUSE_MS = 9e4;
|
|
2441
|
+
var frozenAtScrollX = 0;
|
|
2442
|
+
var frozenAtScrollY = 0;
|
|
2443
|
+
var frozenAtPath = "";
|
|
2444
|
+
function stillIsCurrent() {
|
|
2445
|
+
if (!frozen) return false;
|
|
2446
|
+
if (typeof window === "undefined") return false;
|
|
2447
|
+
if (Date.now() - frozen.takenAt > STILL_REUSE_MS) return false;
|
|
2448
|
+
if (window.scrollX !== frozenAtScrollX || window.scrollY !== frozenAtScrollY) return false;
|
|
2449
|
+
if (window.location.pathname + window.location.search !== frozenAtPath) return false;
|
|
2450
|
+
const vw = document.documentElement.clientWidth || window.innerWidth;
|
|
2451
|
+
const vh = document.documentElement.clientHeight || window.innerHeight;
|
|
2452
|
+
if (vw !== frozen.viewportWidth || vh !== frozen.viewportHeight) return false;
|
|
2453
|
+
return true;
|
|
2454
|
+
}
|
|
2455
|
+
async function freezeOrReuse() {
|
|
2456
|
+
if (stillIsCurrent()) return frozen;
|
|
2457
|
+
return freezeViewport();
|
|
2458
|
+
}
|
|
2386
2459
|
function releaseFrozenFrame() {
|
|
2387
2460
|
if (frozen) {
|
|
2388
2461
|
frozen.canvas.width = 0;
|
|
@@ -2627,6 +2700,9 @@ async function freezeViewport() {
|
|
|
2627
2700
|
viewportHeight: vh,
|
|
2628
2701
|
takenAt: Date.now()
|
|
2629
2702
|
};
|
|
2703
|
+
frozenAtScrollX = window.scrollX;
|
|
2704
|
+
frozenAtScrollY = window.scrollY;
|
|
2705
|
+
frozenAtPath = window.location.pathname + window.location.search;
|
|
2630
2706
|
lastMode = mode;
|
|
2631
2707
|
return frozen;
|
|
2632
2708
|
} catch {
|
|
@@ -2798,6 +2874,38 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2798
2874
|
return touched;
|
|
2799
2875
|
}
|
|
2800
2876
|
|
|
2877
|
+
// src/lib/faultLog.ts
|
|
2878
|
+
var LIMIT = 40;
|
|
2879
|
+
var faults = [];
|
|
2880
|
+
function recordFault(where, err) {
|
|
2881
|
+
const what = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : (() => {
|
|
2882
|
+
try {
|
|
2883
|
+
return JSON.stringify(err);
|
|
2884
|
+
} catch {
|
|
2885
|
+
return String(err);
|
|
2886
|
+
}
|
|
2887
|
+
})();
|
|
2888
|
+
faults.push({ at: Date.now(), where, what: what.slice(0, 500) });
|
|
2889
|
+
if (faults.length > LIMIT) faults.splice(0, faults.length - LIMIT);
|
|
2890
|
+
console.warn(`[QA] ${where}:`, err);
|
|
2891
|
+
}
|
|
2892
|
+
function readFaults() {
|
|
2893
|
+
return [...faults].reverse();
|
|
2894
|
+
}
|
|
2895
|
+
function clearFaults() {
|
|
2896
|
+
faults.length = 0;
|
|
2897
|
+
}
|
|
2898
|
+
function faultsAsText(version) {
|
|
2899
|
+
if (!faults.length) return "No faults recorded.";
|
|
2900
|
+
const head = [
|
|
2901
|
+
`qapture ${version}`,
|
|
2902
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
2903
|
+
typeof location !== "undefined" ? location.href.split("?")[0] : "",
|
|
2904
|
+
""
|
|
2905
|
+
].filter(Boolean).join("\n");
|
|
2906
|
+
return head + readFaults().map((f) => `${new Date(f.at).toISOString()} [${f.where}] ${f.what}`).join("\n");
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2801
2909
|
// src/lib/capture.ts
|
|
2802
2910
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2803
2911
|
var CHUNK_TIMEOUT_MS = 8e3;
|
|
@@ -3150,12 +3258,13 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3150
3258
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
3151
3259
|
}
|
|
3152
3260
|
} catch (err) {
|
|
3153
|
-
|
|
3261
|
+
recordFault("screenshot/crop", err);
|
|
3154
3262
|
}
|
|
3155
3263
|
if (prefer === "exact") return { status: "failed" };
|
|
3156
3264
|
}
|
|
3157
3265
|
const weight = documentWeight();
|
|
3158
3266
|
if (weight > TOO_HEAVY_NODES) {
|
|
3267
|
+
recordFault("screenshot/too-heavy", `page has ${weight} elements; the redraw engine was refused`);
|
|
3159
3268
|
return { status: "too-heavy", nodes: weight };
|
|
3160
3269
|
}
|
|
3161
3270
|
try {
|
|
@@ -3164,14 +3273,14 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
3164
3273
|
const blob = await encodeShot(canvas);
|
|
3165
3274
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3166
3275
|
} catch (err) {
|
|
3167
|
-
|
|
3276
|
+
recordFault("screenshot/render", err);
|
|
3168
3277
|
try {
|
|
3169
3278
|
const canvas = await captureViaDom(rect, sx, sy, true);
|
|
3170
3279
|
if (!canvas) return { status: "failed" };
|
|
3171
3280
|
const blob = await encodeShot(canvas);
|
|
3172
3281
|
return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
|
|
3173
3282
|
} catch (retryErr) {
|
|
3174
|
-
|
|
3283
|
+
recordFault("screenshot/render-retry", retryErr);
|
|
3175
3284
|
return { status: "failed" };
|
|
3176
3285
|
}
|
|
3177
3286
|
}
|
|
@@ -3215,10 +3324,32 @@ function formatEvent(ev, t0) {
|
|
|
3215
3324
|
}
|
|
3216
3325
|
return `[${rel}] uncaught: ${oneLine(ev.message)}`;
|
|
3217
3326
|
}
|
|
3327
|
+
var EVIDENCE_MARK = "<!--qa:evidence-->";
|
|
3328
|
+
function noteContextMarkdown(note, index) {
|
|
3329
|
+
const body = noteToMarkdown(note, { index, keepEvidenceMark: true });
|
|
3330
|
+
const at = body.indexOf(EVIDENCE_MARK);
|
|
3331
|
+
const header = [
|
|
3332
|
+
`# Point ${index} \u2014 runtime context`,
|
|
3333
|
+
"",
|
|
3334
|
+
`Page: ${oneLine(note.route) || "/"}`,
|
|
3335
|
+
`Captured: ${oneLine(note.timestamp)}`,
|
|
3336
|
+
"",
|
|
3337
|
+
"Everything the browser recorded around this capture. Kept out of",
|
|
3338
|
+
"`notes.md` on purpose -- it is here when it is needed, and out of the way",
|
|
3339
|
+
"when it is not.",
|
|
3340
|
+
"",
|
|
3341
|
+
"---",
|
|
3342
|
+
""
|
|
3343
|
+
].join("\n");
|
|
3344
|
+
return at === -1 ? `${header}_(nothing was recorded)_
|
|
3345
|
+
` : header + body.slice(at + EVIDENCE_MARK.length).trimStart();
|
|
3346
|
+
}
|
|
3218
3347
|
function noteCheckLine(note, index) {
|
|
3219
3348
|
const where = oneLine(note.route) || "/";
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3349
|
+
const wanted = oneLine(note.wanted);
|
|
3350
|
+
const seen = oneLine(note.description) || "(not described)";
|
|
3351
|
+
const claim = wanted || `${seen} \u2014 _no expectation was given; ask before assuming one_`;
|
|
3352
|
+
const trimmed = claim.length > 180 ? `${claim.slice(0, 177)}...` : claim;
|
|
3222
3353
|
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3223
3354
|
}
|
|
3224
3355
|
function noteToMarkdown(note, opts) {
|
|
@@ -3248,8 +3379,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3248
3379
|
);
|
|
3249
3380
|
}
|
|
3250
3381
|
}
|
|
3382
|
+
if (note.origin?.component) lines.push(`- **Component:** \`${oneLine(note.origin.component)}\``);
|
|
3383
|
+
if (note.origin?.file) {
|
|
3384
|
+
lines.push(`- **Source:** \`${oneLine(note.origin.file)}${note.origin.line ? `:${note.origin.line}` : ""}\``);
|
|
3385
|
+
}
|
|
3251
3386
|
if (idx != null && note.screenshot) {
|
|
3252
3387
|
lines.push(`- **Screenshot:** screenshots/point-${idx}.${shotExtension(note.screenshot)}`);
|
|
3388
|
+
if (note.shotEngine === "dom") {
|
|
3389
|
+
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.");
|
|
3390
|
+
}
|
|
3253
3391
|
}
|
|
3254
3392
|
if (idx != null && note.afterScreenshot) {
|
|
3255
3393
|
lines.push(
|
|
@@ -3257,7 +3395,29 @@ function noteToMarkdown(note, opts) {
|
|
|
3257
3395
|
);
|
|
3258
3396
|
}
|
|
3259
3397
|
lines.push("");
|
|
3260
|
-
lines.push(
|
|
3398
|
+
lines.push("### Observed");
|
|
3399
|
+
lines.push("");
|
|
3400
|
+
lines.push(oneLine(note.description) ? note.description.trim() : "_(not described)_");
|
|
3401
|
+
lines.push("");
|
|
3402
|
+
lines.push("### Expected");
|
|
3403
|
+
lines.push("");
|
|
3404
|
+
lines.push(
|
|
3405
|
+
note.wanted && oneLine(note.wanted) ? note.wanted.trim() : "_(the tester did not say what they expected instead -- ask rather than assume)_"
|
|
3406
|
+
);
|
|
3407
|
+
if (note.why && oneLine(note.why)) {
|
|
3408
|
+
lines.push("");
|
|
3409
|
+
lines.push("### Why it matters");
|
|
3410
|
+
lines.push("");
|
|
3411
|
+
lines.push(note.why.trim());
|
|
3412
|
+
}
|
|
3413
|
+
if (note.fixHint && oneLine(note.fixHint)) {
|
|
3414
|
+
lines.push("");
|
|
3415
|
+
lines.push("### Suggested fix (a suggestion, not an instruction)");
|
|
3416
|
+
lines.push("");
|
|
3417
|
+
lines.push(note.fixHint.trim());
|
|
3418
|
+
lines.push("");
|
|
3419
|
+
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.");
|
|
3420
|
+
}
|
|
3261
3421
|
if (note.followUp && oneLine(note.followUp)) {
|
|
3262
3422
|
lines.push("");
|
|
3263
3423
|
lines.push(
|
|
@@ -3268,6 +3428,15 @@ function noteToMarkdown(note, opts) {
|
|
|
3268
3428
|
lines.push("");
|
|
3269
3429
|
lines.push(note.followUp.trim());
|
|
3270
3430
|
}
|
|
3431
|
+
if (idx != null) {
|
|
3432
|
+
lines.push("");
|
|
3433
|
+
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3434
|
+
lines.push("");
|
|
3435
|
+
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?*");
|
|
3436
|
+
lines.push("");
|
|
3437
|
+
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\`.`);
|
|
3438
|
+
}
|
|
3439
|
+
lines.push(EVIDENCE_MARK);
|
|
3271
3440
|
const recordedSteps = note.context?.steps ?? [];
|
|
3272
3441
|
if (recordedSteps.length) {
|
|
3273
3442
|
const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
|
|
@@ -3278,14 +3447,6 @@ function noteToMarkdown(note, opts) {
|
|
|
3278
3447
|
lines.push(`${i + 1}. ${formatStep(step, t0)}`);
|
|
3279
3448
|
});
|
|
3280
3449
|
}
|
|
3281
|
-
if (idx != null) {
|
|
3282
|
-
lines.push("");
|
|
3283
|
-
lines.push(`**Check ${idx} \u2014 how this will be graded**`);
|
|
3284
|
-
lines.push("");
|
|
3285
|
-
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?*");
|
|
3286
|
-
lines.push("");
|
|
3287
|
-
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\`.`);
|
|
3288
|
-
}
|
|
3289
3450
|
const ctx = note.context;
|
|
3290
3451
|
if (ctx) {
|
|
3291
3452
|
const env = ctx.env;
|
|
@@ -3332,7 +3493,67 @@ function noteToMarkdown(note, opts) {
|
|
|
3332
3493
|
lines.push("");
|
|
3333
3494
|
lines.push("</details>");
|
|
3334
3495
|
}
|
|
3335
|
-
|
|
3496
|
+
const whole = lines.join("\n");
|
|
3497
|
+
if (opts?.keepEvidenceMark) return whole;
|
|
3498
|
+
if (!opts?.contextFile) return whole.replace(`${EVIDENCE_MARK}
|
|
3499
|
+
`, "").replace(EVIDENCE_MARK, "");
|
|
3500
|
+
const at = whole.indexOf(EVIDENCE_MARK);
|
|
3501
|
+
const report = at === -1 ? whole : whole.slice(0, at).trimEnd();
|
|
3502
|
+
return `${report}
|
|
3503
|
+
|
|
3504
|
+
<sub>Steps, console, network, environment and element forensics: \`${opts.contextFile}\` \u2014 open it only if the words above leave something genuinely unresolved.</sub>`;
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// src/lib/reproSpec.ts
|
|
3508
|
+
function lit(value) {
|
|
3509
|
+
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/\r?\n/g, " ");
|
|
3510
|
+
}
|
|
3511
|
+
function line(value, limit = 200) {
|
|
3512
|
+
const one = (value ?? "").replace(/\s+/g, " ").trim();
|
|
3513
|
+
if (!one) return "";
|
|
3514
|
+
return one.length > limit ? `${one.slice(0, limit - 3)}...` : one;
|
|
3515
|
+
}
|
|
3516
|
+
function reproSpec(note, index) {
|
|
3517
|
+
const selector = note.target?.selector;
|
|
3518
|
+
const route = note.route || "/";
|
|
3519
|
+
if (!selector && !note.route) return null;
|
|
3520
|
+
const observed = line(note.description);
|
|
3521
|
+
const expected = line(note.wanted);
|
|
3522
|
+
const out = [];
|
|
3523
|
+
out.push(`// check-${index} -- DRAFT. Delete this file if the change is cosmetic.`);
|
|
3524
|
+
out.push("//");
|
|
3525
|
+
out.push("// The URL and the selector below were captured from the live page, so");
|
|
3526
|
+
out.push("// they are the tedious part already done. The assertion is yours: only");
|
|
3527
|
+
out.push("// you know whether this point is worth pinning down with a test.");
|
|
3528
|
+
out.push("//");
|
|
3529
|
+
if (observed) out.push(`// Observed: ${observed}`);
|
|
3530
|
+
if (expected) out.push(`// Expected: ${expected}`);
|
|
3531
|
+
else out.push("// Expected: (the tester did not say -- ask before asserting anything)");
|
|
3532
|
+
if (note.origin?.file) {
|
|
3533
|
+
out.push(`// Rendered by: ${note.origin.component ?? "?"} (${note.origin.file}${note.origin.line ? `:${note.origin.line}` : ""})`);
|
|
3534
|
+
}
|
|
3535
|
+
out.push("");
|
|
3536
|
+
out.push("import { test, expect } from '@playwright/test';");
|
|
3537
|
+
out.push("");
|
|
3538
|
+
out.push(`test('check-${index}: ${lit(line(note.description, 60) || "reported point")}', async ({ page }) => {`);
|
|
3539
|
+
out.push(` await page.goto('${lit(route)}');`);
|
|
3540
|
+
if (selector) {
|
|
3541
|
+
out.push("");
|
|
3542
|
+
out.push(` const target = page.locator('${lit(selector)}');`);
|
|
3543
|
+
out.push(" await expect(target).toBeVisible();");
|
|
3544
|
+
out.push("");
|
|
3545
|
+
out.push(" // TODO: assert the EXPECTED behaviour quoted above.");
|
|
3546
|
+
out.push(" // Being visible only proves the element is still there -- it does not");
|
|
3547
|
+
out.push(" // prove the thing the tester asked for actually happened.");
|
|
3548
|
+
} else {
|
|
3549
|
+
out.push("");
|
|
3550
|
+
out.push(" // No element was picked for this point -- it was a region or a plain");
|
|
3551
|
+
out.push(" // note. Drive the page to the state described above, then assert.");
|
|
3552
|
+
out.push(" // TODO");
|
|
3553
|
+
}
|
|
3554
|
+
out.push("});");
|
|
3555
|
+
out.push("");
|
|
3556
|
+
return out.join("\n");
|
|
3336
3557
|
}
|
|
3337
3558
|
|
|
3338
3559
|
// src/lib/exportZip.ts
|
|
@@ -3468,7 +3689,15 @@ This archive is not a list of suggestions. Every point in \`notes.md\` is an acc
|
|
|
3468
3689
|
|
|
3469
3690
|
**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.
|
|
3470
3691
|
|
|
3471
|
-
Hand \`verify.md\` back with the work
|
|
3692
|
+
Hand \`verify.md\` back with the work.
|
|
3693
|
+
|
|
3694
|
+
### What is in this archive
|
|
3695
|
+
|
|
3696
|
+
- \`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**.
|
|
3697
|
+
- \`verify.md\` \u2014 the checklist, one unticked box per point.
|
|
3698
|
+
- \`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.
|
|
3699
|
+
- \`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.
|
|
3700
|
+
- \`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.`
|
|
3472
3701
|
);
|
|
3473
3702
|
const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
|
|
3474
3703
|
const runArr = toStrings(p.runCommands);
|
|
@@ -3608,7 +3837,14 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3608
3837
|
""
|
|
3609
3838
|
].join("\n");
|
|
3610
3839
|
const noteBlocks = notes.map(
|
|
3611
|
-
(n, i) => noteToMarkdown(n, {
|
|
3840
|
+
(n, i) => noteToMarkdown(n, {
|
|
3841
|
+
brand: brandLabel,
|
|
3842
|
+
index: i + 1,
|
|
3843
|
+
// Runtime evidence goes to its own file and is pointed at from here.
|
|
3844
|
+
// See the contextFile branch in noteMarkdown.ts for why: a long report
|
|
3845
|
+
// measurably lowers an agent's chance of fixing the thing.
|
|
3846
|
+
contextFile: n.context ? `context/point-${i + 1}.md` : void 0
|
|
3847
|
+
})
|
|
3612
3848
|
);
|
|
3613
3849
|
const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
|
|
3614
3850
|
|
|
@@ -3649,6 +3885,43 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
|
|
|
3649
3885
|
"Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
|
|
3650
3886
|
""
|
|
3651
3887
|
].join("\n"));
|
|
3888
|
+
const contextDir = zip.folder("context");
|
|
3889
|
+
notes.forEach((n, i) => {
|
|
3890
|
+
if (n.context && contextDir) {
|
|
3891
|
+
contextDir.file(`point-${i + 1}.md`, noteContextMarkdown(n, i + 1));
|
|
3892
|
+
}
|
|
3893
|
+
});
|
|
3894
|
+
const reproDir = zip.folder("repro");
|
|
3895
|
+
let reproCount = 0;
|
|
3896
|
+
notes.forEach((n, i) => {
|
|
3897
|
+
const spec = reproSpec(n, i + 1);
|
|
3898
|
+
if (spec && reproDir) {
|
|
3899
|
+
reproDir.file(`check-${i + 1}.spec.ts`, spec);
|
|
3900
|
+
reproCount++;
|
|
3901
|
+
}
|
|
3902
|
+
});
|
|
3903
|
+
if (reproCount && reproDir) {
|
|
3904
|
+
reproDir.file("README.md", [
|
|
3905
|
+
"# Reproduction drafts",
|
|
3906
|
+
"",
|
|
3907
|
+
"One per point, and every one of them is optional.",
|
|
3908
|
+
"",
|
|
3909
|
+
"**Use one** where the point is behavioural and worth pinning down, so it",
|
|
3910
|
+
"cannot quietly come back later. An executable check is worth far more to",
|
|
3911
|
+
"you than another paragraph of steps written in English.",
|
|
3912
|
+
"",
|
|
3913
|
+
"**Delete it** where the point is cosmetic -- a colour, a spacing, a word.",
|
|
3914
|
+
"A test asserting that a heading is visible proves nothing anybody wanted",
|
|
3915
|
+
"proved, and it is one more file to maintain forever.",
|
|
3916
|
+
"",
|
|
3917
|
+
"That call is yours. The tester was not asked to make it and could not.",
|
|
3918
|
+
"",
|
|
3919
|
+
"What is already done for you in each file: the URL, a selector verified",
|
|
3920
|
+
"against the live DOM at capture time, and the observed and expected",
|
|
3921
|
+
"behaviour quoted in place. What is left is the assertion, marked TODO.",
|
|
3922
|
+
""
|
|
3923
|
+
].join("\n"));
|
|
3924
|
+
}
|
|
3652
3925
|
notes.forEach((n, i) => {
|
|
3653
3926
|
if (n.screenshot && shots) {
|
|
3654
3927
|
shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
|
|
@@ -4118,7 +4391,7 @@ function reportMarkdownText(allNotes) {
|
|
|
4118
4391
|
"",
|
|
4119
4392
|
"---",
|
|
4120
4393
|
""
|
|
4121
|
-
].filter((
|
|
4394
|
+
].filter((line2) => line2 !== "").join("\n");
|
|
4122
4395
|
const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
|
|
4123
4396
|
return `${header}
|
|
4124
4397
|
${body}`;
|
|
@@ -4293,6 +4566,7 @@ function exactShotsWanted(store) {
|
|
|
4293
4566
|
}
|
|
4294
4567
|
var SIMPLE_MODE_KEY = "simpleMode";
|
|
4295
4568
|
var COMPACT_KEY = "compactCapture";
|
|
4569
|
+
var DEV_MODE_KEY = "developerMode";
|
|
4296
4570
|
var LAST_CAMPAIGN_KEY = "lastCampaign";
|
|
4297
4571
|
var AUTO_BACKUP_KEY = "autoBackup";
|
|
4298
4572
|
var AUTO_BACKUP_AT_KEY = "autoBackupAt";
|
|
@@ -4331,6 +4605,7 @@ function QaProvider({
|
|
|
4331
4605
|
useEffect(() => {
|
|
4332
4606
|
notesRef.current = notes;
|
|
4333
4607
|
}, [notes]);
|
|
4608
|
+
const staleStillTimer = useRef(null);
|
|
4334
4609
|
const applyNotes = useCallback((updater) => {
|
|
4335
4610
|
const next = updater(notesRef.current);
|
|
4336
4611
|
notesRef.current = next;
|
|
@@ -4357,7 +4632,15 @@ function QaProvider({
|
|
|
4357
4632
|
const [lang, setLangState] = useState(() => {
|
|
4358
4633
|
const saved = storage.getItem(LANG_KEY);
|
|
4359
4634
|
if (saved === "ar" || saved === "en") return saved;
|
|
4360
|
-
|
|
4635
|
+
if (config.rtl) return "ar";
|
|
4636
|
+
try {
|
|
4637
|
+
const html = document.documentElement;
|
|
4638
|
+
if (html.getAttribute("dir") === "rtl") return "ar";
|
|
4639
|
+
if ((html.getAttribute("lang") || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4640
|
+
if ((navigator.language || "").toLowerCase().startsWith("ar")) return "ar";
|
|
4641
|
+
} catch {
|
|
4642
|
+
}
|
|
4643
|
+
return "en";
|
|
4361
4644
|
});
|
|
4362
4645
|
const [guideChecked, setGuideChecked] = useState(
|
|
4363
4646
|
() => new Set(storage.getJSON(GUIDE_KEY, []))
|
|
@@ -4395,6 +4678,9 @@ function QaProvider({
|
|
|
4395
4678
|
const [simpleMode, setSimpleModeState] = useState(
|
|
4396
4679
|
() => storage.getItem(SIMPLE_MODE_KEY) === "1"
|
|
4397
4680
|
);
|
|
4681
|
+
const [developerMode, setDeveloperModeState] = useState(
|
|
4682
|
+
() => storage.getItem(DEV_MODE_KEY) === "1"
|
|
4683
|
+
);
|
|
4398
4684
|
const [compactCapture, setCompactCaptureState] = useState(
|
|
4399
4685
|
() => storage.getItem(COMPACT_KEY) === "1"
|
|
4400
4686
|
);
|
|
@@ -4724,6 +5010,11 @@ function QaProvider({
|
|
|
4724
5010
|
target: input.target ?? void 0,
|
|
4725
5011
|
severity: input.severity,
|
|
4726
5012
|
status: input.status,
|
|
5013
|
+
wanted: (input.wanted || "").trim() || void 0,
|
|
5014
|
+
why: (input.why || "").trim() || void 0,
|
|
5015
|
+
fixHint: (input.fixHint || "").trim() || void 0,
|
|
5016
|
+
shotEngine: input.shotEngine,
|
|
5017
|
+
origin: input.origin,
|
|
4727
5018
|
journeyRef,
|
|
4728
5019
|
context
|
|
4729
5020
|
};
|
|
@@ -4874,17 +5165,22 @@ function QaProvider({
|
|
|
4874
5165
|
flushPendingDeletes();
|
|
4875
5166
|
for (const timer of noticeTimers.current.values()) clearTimeout(timer);
|
|
4876
5167
|
noticeTimers.current.clear();
|
|
5168
|
+
if (staleStillTimer.current) {
|
|
5169
|
+
clearTimeout(staleStillTimer.current);
|
|
5170
|
+
staleStillTimer.current = null;
|
|
5171
|
+
}
|
|
4877
5172
|
releaseFrozenFrame();
|
|
4878
5173
|
};
|
|
4879
5174
|
}, [flushPendingDeletes]);
|
|
5175
|
+
const STILL_HOLD_MS = 1e5;
|
|
4880
5176
|
const startCapture = useCallback((prefill) => {
|
|
4881
5177
|
setIsOpen(false);
|
|
4882
5178
|
setCapturePrefill(prefill ?? "");
|
|
4883
5179
|
setCaptureActive(true);
|
|
4884
|
-
setFrozenAt(null);
|
|
5180
|
+
if (!stillIsCurrent()) setFrozenAt(null);
|
|
4885
5181
|
if (exactShotsWanted(storage) && isExactCaptureSupported()) {
|
|
4886
5182
|
resetExactCaptureDecline();
|
|
4887
|
-
void
|
|
5183
|
+
void freezeOrReuse().then((frame) => {
|
|
4888
5184
|
setFrozenAt(frame?.takenAt ?? null);
|
|
4889
5185
|
setExactStatus(getExactCaptureStatus());
|
|
4890
5186
|
});
|
|
@@ -4892,8 +5188,11 @@ function QaProvider({
|
|
|
4892
5188
|
}, [storage]);
|
|
4893
5189
|
const endCapture = useCallback((reopen = true) => {
|
|
4894
5190
|
setCaptureActive(false);
|
|
4895
|
-
|
|
4896
|
-
|
|
5191
|
+
if (staleStillTimer.current) clearTimeout(staleStillTimer.current);
|
|
5192
|
+
staleStillTimer.current = setTimeout(() => {
|
|
5193
|
+
releaseFrozenFrame();
|
|
5194
|
+
staleStillTimer.current = null;
|
|
5195
|
+
}, STILL_HOLD_MS);
|
|
4897
5196
|
if (reopen) setIsOpen(true);
|
|
4898
5197
|
}, []);
|
|
4899
5198
|
const toggleGuide = useCallback((key) => {
|
|
@@ -5118,6 +5417,10 @@ function QaProvider({
|
|
|
5118
5417
|
setCompactCaptureState(on);
|
|
5119
5418
|
storage.setItem(COMPACT_KEY, on ? "1" : "0");
|
|
5120
5419
|
}, [storage]);
|
|
5420
|
+
const setDeveloperMode = useCallback((on) => {
|
|
5421
|
+
setDeveloperModeState(on);
|
|
5422
|
+
storage.setItem(DEV_MODE_KEY, on ? "1" : "0");
|
|
5423
|
+
}, [storage]);
|
|
5121
5424
|
const setFilter = useCallback((patch) => {
|
|
5122
5425
|
setFilterState((prev) => ({ ...prev, ...patch }));
|
|
5123
5426
|
}, []);
|
|
@@ -5457,6 +5760,7 @@ function QaProvider({
|
|
|
5457
5760
|
// Config passthrough
|
|
5458
5761
|
namespace: config.namespace,
|
|
5459
5762
|
brand: config.brand,
|
|
5763
|
+
beta: config.beta === true,
|
|
5460
5764
|
loginField: config.loginField,
|
|
5461
5765
|
credentials: config.credentials,
|
|
5462
5766
|
// Never the raw config value: an unconfigured project falls back to the
|
|
@@ -5565,6 +5869,8 @@ function QaProvider({
|
|
|
5565
5869
|
simpleMode,
|
|
5566
5870
|
setSimpleMode,
|
|
5567
5871
|
compactCapture,
|
|
5872
|
+
developerMode,
|
|
5873
|
+
setDeveloperMode,
|
|
5568
5874
|
setCompactCapture,
|
|
5569
5875
|
exportZip: exportZipFn
|
|
5570
5876
|
};
|
|
@@ -5632,6 +5938,16 @@ var ICONS = {
|
|
|
5632
5938
|
Square: [
|
|
5633
5939
|
["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
|
|
5634
5940
|
],
|
|
5941
|
+
Info: [
|
|
5942
|
+
["circle", { cx: "12", cy: "12", r: "10" }],
|
|
5943
|
+
["path", { d: "M12 16v-4" }],
|
|
5944
|
+
["path", { d: "M12 8h.01" }]
|
|
5945
|
+
],
|
|
5946
|
+
Mic: [
|
|
5947
|
+
["path", { d: "M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" }],
|
|
5948
|
+
["path", { d: "M19 10v2a7 7 0 0 1-14 0v-2" }],
|
|
5949
|
+
["line", { x1: "12", x2: "12", y1: "19", y2: "22" }]
|
|
5950
|
+
],
|
|
5635
5951
|
ImagePlus: [
|
|
5636
5952
|
["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
|
|
5637
5953
|
["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
|
|
@@ -5865,7 +6181,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
|
|
|
5865
6181
|
};
|
|
5866
6182
|
}
|
|
5867
6183
|
function QaFab() {
|
|
5868
|
-
const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
|
|
6184
|
+
const { isOpen, setIsOpen, notes, captureActive, namespace, beta, t } = useQa();
|
|
5869
6185
|
const [pos, setPos] = useState(() => loadFabPos(namespace));
|
|
5870
6186
|
const dragRef = useRef(null);
|
|
5871
6187
|
const didDragRef = useRef(false);
|
|
@@ -5978,6 +6294,15 @@ function QaFab() {
|
|
|
5978
6294
|
}
|
|
5979
6295
|
),
|
|
5980
6296
|
/* @__PURE__ */ jsx(Icon, { name: isOpen ? "X" : "ClipboardList", size: 24 }),
|
|
6297
|
+
beta && !isOpen && /* @__PURE__ */ jsx(
|
|
6298
|
+
"span",
|
|
6299
|
+
{
|
|
6300
|
+
className: "qa-absolute qa-rounded-full qa-text-10 qa-font-bold qa-bg-1 qa-text-mid qa-border qa-border-subtle",
|
|
6301
|
+
style: { bottom: "-6px", left: "50%", transform: "translateX(-50%)", padding: "0 5px", lineHeight: "1.3" },
|
|
6302
|
+
"aria-hidden": "true",
|
|
6303
|
+
children: "beta"
|
|
6304
|
+
}
|
|
6305
|
+
),
|
|
5981
6306
|
!isOpen && notes.length > 0 && /* @__PURE__ */ jsx(
|
|
5982
6307
|
"span",
|
|
5983
6308
|
{
|
|
@@ -7671,6 +7996,154 @@ function NoteFilterBar() {
|
|
|
7671
7996
|
) })
|
|
7672
7997
|
] });
|
|
7673
7998
|
}
|
|
7999
|
+
|
|
8000
|
+
// src/lib/doctor.ts
|
|
8001
|
+
var HEAVY_PAGE = 6e3;
|
|
8002
|
+
function canvasIsReadable() {
|
|
8003
|
+
try {
|
|
8004
|
+
const c = document.createElement("canvas");
|
|
8005
|
+
c.width = 1;
|
|
8006
|
+
c.height = 1;
|
|
8007
|
+
const ctx = c.getContext("2d");
|
|
8008
|
+
if (!ctx) return false;
|
|
8009
|
+
ctx.fillStyle = "#000";
|
|
8010
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
8011
|
+
c.toDataURL();
|
|
8012
|
+
return true;
|
|
8013
|
+
} catch {
|
|
8014
|
+
return false;
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
function taintingImages() {
|
|
8018
|
+
let count = 0;
|
|
8019
|
+
const here = location.origin;
|
|
8020
|
+
for (const img of Array.from(document.images)) {
|
|
8021
|
+
const src = img.currentSrc || img.src;
|
|
8022
|
+
if (!src || src.startsWith("data:") || src.startsWith("blob:")) continue;
|
|
8023
|
+
try {
|
|
8024
|
+
if (new URL(src, here).origin !== here && !img.crossOrigin) count++;
|
|
8025
|
+
} catch {
|
|
8026
|
+
}
|
|
8027
|
+
}
|
|
8028
|
+
return count;
|
|
8029
|
+
}
|
|
8030
|
+
async function runDoctor() {
|
|
8031
|
+
const out = [];
|
|
8032
|
+
if (typeof document === "undefined") return out;
|
|
8033
|
+
const supported = isExactCaptureSupported();
|
|
8034
|
+
const status = getExactCaptureStatus();
|
|
8035
|
+
out.push(
|
|
8036
|
+
!supported ? {
|
|
8037
|
+
label: "Screenshot engine",
|
|
8038
|
+
verdict: "warn",
|
|
8039
|
+
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."
|
|
8040
|
+
} : status === "live" ? { label: "Screenshot engine", verdict: "ok", detail: "Real photographs. This is the accurate one." } : {
|
|
8041
|
+
label: "Screenshot engine",
|
|
8042
|
+
verdict: "warn",
|
|
8043
|
+
detail: "Photographs are switched off, so screenshots are re-drawn and may not match the page."
|
|
8044
|
+
}
|
|
8045
|
+
);
|
|
8046
|
+
const readable = canvasIsReadable();
|
|
8047
|
+
const risky = taintingImages();
|
|
8048
|
+
out.push(
|
|
8049
|
+
!readable ? {
|
|
8050
|
+
label: "Screenshot encoding",
|
|
8051
|
+
verdict: "bad",
|
|
8052
|
+
detail: "This page cannot turn a drawing into an image at all. Re-drawn screenshots will fail here; use photographs."
|
|
8053
|
+
} : risky > 0 ? {
|
|
8054
|
+
label: "Screenshot encoding",
|
|
8055
|
+
verdict: "info",
|
|
8056
|
+
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.`
|
|
8057
|
+
} : { label: "Screenshot encoding", verdict: "ok", detail: "Nothing on this page blocks screenshot encoding." }
|
|
8058
|
+
);
|
|
8059
|
+
const nodes = document.getElementsByTagName("*").length;
|
|
8060
|
+
out.push({
|
|
8061
|
+
label: "Page size",
|
|
8062
|
+
verdict: nodes > HEAVY_PAGE ? "warn" : "ok",
|
|
8063
|
+
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.`
|
|
8064
|
+
});
|
|
8065
|
+
try {
|
|
8066
|
+
await import('html2canvas');
|
|
8067
|
+
out.push({ label: "Screenshot library", verdict: "ok", detail: "Loads correctly." });
|
|
8068
|
+
} catch {
|
|
8069
|
+
out.push({
|
|
8070
|
+
label: "Screenshot library",
|
|
8071
|
+
verdict: "bad",
|
|
8072
|
+
detail: "Blocked or unreachable \u2014 often a strict Content-Security-Policy. Re-drawn screenshots cannot work here."
|
|
8073
|
+
});
|
|
8074
|
+
}
|
|
8075
|
+
try {
|
|
8076
|
+
const est = await navigator.storage?.estimate?.();
|
|
8077
|
+
if (est?.quota) {
|
|
8078
|
+
const usedPct = Math.round((est.usage ?? 0) / est.quota * 100);
|
|
8079
|
+
out.push({
|
|
8080
|
+
label: "Storage",
|
|
8081
|
+
verdict: usedPct > 90 ? "bad" : usedPct > 70 ? "warn" : "ok",
|
|
8082
|
+
detail: `${usedPct}% of this site's storage used. Notes are kept in this browser until you export them.`
|
|
8083
|
+
});
|
|
8084
|
+
}
|
|
8085
|
+
} catch {
|
|
8086
|
+
}
|
|
8087
|
+
const voice = typeof window !== "undefined" && !!(window.SpeechRecognition || window.webkitSpeechRecognition);
|
|
8088
|
+
out.push({
|
|
8089
|
+
label: "Voice input",
|
|
8090
|
+
verdict: voice ? "ok" : "info",
|
|
8091
|
+
detail: voice ? "Available \u2014 you can speak your notes." : "Not available in this browser. Typing still works."
|
|
8092
|
+
});
|
|
8093
|
+
out.push({
|
|
8094
|
+
label: "Secure connection",
|
|
8095
|
+
verdict: window.isSecureContext ? "ok" : "bad",
|
|
8096
|
+
detail: window.isSecureContext ? "https \u2014 photographs and voice are allowed." : "Not https. Photographs and voice are blocked by the browser on an insecure page."
|
|
8097
|
+
});
|
|
8098
|
+
return out;
|
|
8099
|
+
}
|
|
8100
|
+
|
|
8101
|
+
// src/lib/versionCheck.ts
|
|
8102
|
+
var CACHE_KEY = "qa.versionCheck";
|
|
8103
|
+
var A_DAY = 24 * 60 * 60 * 1e3;
|
|
8104
|
+
var REGISTRY = "https://registry.npmjs.org/qapture2/latest";
|
|
8105
|
+
function isNewer(latest, current) {
|
|
8106
|
+
const norm = (v) => v.replace(/^[^\d]*/, "").split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
|
|
8107
|
+
const a = norm(latest);
|
|
8108
|
+
const b = norm(current);
|
|
8109
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
8110
|
+
const x = a[i] ?? 0;
|
|
8111
|
+
const y = b[i] ?? 0;
|
|
8112
|
+
if (x !== y) return x > y;
|
|
8113
|
+
}
|
|
8114
|
+
return false;
|
|
8115
|
+
}
|
|
8116
|
+
async function latestVersion() {
|
|
8117
|
+
if (typeof window === "undefined") return null;
|
|
8118
|
+
try {
|
|
8119
|
+
const raw = window.localStorage.getItem(CACHE_KEY);
|
|
8120
|
+
if (raw) {
|
|
8121
|
+
const cached = JSON.parse(raw);
|
|
8122
|
+
if (Date.now() - cached.at < A_DAY && cached.latest) return cached.latest;
|
|
8123
|
+
}
|
|
8124
|
+
} catch {
|
|
8125
|
+
}
|
|
8126
|
+
try {
|
|
8127
|
+
const res = await fetch(REGISTRY, { headers: { accept: "application/json" } });
|
|
8128
|
+
if (!res.ok) return null;
|
|
8129
|
+
const body = await res.json();
|
|
8130
|
+
const latest = body.version;
|
|
8131
|
+
if (!latest) return null;
|
|
8132
|
+
try {
|
|
8133
|
+
window.localStorage.setItem(CACHE_KEY, JSON.stringify({ at: Date.now(), latest }));
|
|
8134
|
+
} catch {
|
|
8135
|
+
}
|
|
8136
|
+
return latest;
|
|
8137
|
+
} catch {
|
|
8138
|
+
return null;
|
|
8139
|
+
}
|
|
8140
|
+
}
|
|
8141
|
+
function upgradeHint(latest) {
|
|
8142
|
+
return `npm i qapture2@${latest}`;
|
|
8143
|
+
}
|
|
8144
|
+
|
|
8145
|
+
// src/version.ts
|
|
8146
|
+
var QA_VERSION = "0.9.0" ;
|
|
7674
8147
|
function Section({
|
|
7675
8148
|
icon,
|
|
7676
8149
|
title,
|
|
@@ -7733,8 +8206,24 @@ function SettingsSheet({ onClose }) {
|
|
|
7733
8206
|
setSimpleMode,
|
|
7734
8207
|
compactCapture,
|
|
7735
8208
|
setCompactCapture,
|
|
8209
|
+
developerMode,
|
|
8210
|
+
setDeveloperMode,
|
|
7736
8211
|
notes
|
|
7737
8212
|
} = useQa();
|
|
8213
|
+
const [checks, setChecks] = useState([]);
|
|
8214
|
+
const [running, setRunning] = useState(false);
|
|
8215
|
+
const [faults2, setFaults] = useState(() => readFaults());
|
|
8216
|
+
const [latest, setLatest] = useState(null);
|
|
8217
|
+
useEffect(() => {
|
|
8218
|
+
let alive = true;
|
|
8219
|
+
void latestVersion().then((v) => {
|
|
8220
|
+
if (alive) setLatest(v);
|
|
8221
|
+
});
|
|
8222
|
+
setFaults(readFaults());
|
|
8223
|
+
return () => {
|
|
8224
|
+
alive = false;
|
|
8225
|
+
};
|
|
8226
|
+
}, []);
|
|
7738
8227
|
const [project, setProject] = useState(lastCampaign.project);
|
|
7739
8228
|
const [campaign2, setCampaign] = useState(lastCampaign.campaign || suggestCampaignName());
|
|
7740
8229
|
const [tester, setTester] = useState(lastCampaign.tester);
|
|
@@ -8009,6 +8498,20 @@ function SettingsSheet({ onClose }) {
|
|
|
8009
8498
|
] }),
|
|
8010
8499
|
/* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8011
8500
|
/* @__PURE__ */ jsxs(Section, { icon: "Settings", title: t("settings"), children: [
|
|
8501
|
+
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8502
|
+
/* @__PURE__ */ jsx(
|
|
8503
|
+
"input",
|
|
8504
|
+
{
|
|
8505
|
+
type: "checkbox",
|
|
8506
|
+
checked: developerMode,
|
|
8507
|
+
onChange: (e) => setDeveloperMode(e.target.checked)
|
|
8508
|
+
}
|
|
8509
|
+
),
|
|
8510
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
8511
|
+
t("dev_mode_label"),
|
|
8512
|
+
/* @__PURE__ */ jsx("span", { className: "qa-block qa-text-10 qa-text-mid", children: t("dev_mode_hint") })
|
|
8513
|
+
] })
|
|
8514
|
+
] }),
|
|
8012
8515
|
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-center qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8013
8516
|
/* @__PURE__ */ jsx(
|
|
8014
8517
|
"input",
|
|
@@ -8031,6 +8534,91 @@ function SettingsSheet({ onClose }) {
|
|
|
8031
8534
|
),
|
|
8032
8535
|
t("compact_mode")
|
|
8033
8536
|
] })
|
|
8537
|
+
] }),
|
|
8538
|
+
/* @__PURE__ */ jsxs(Section, { icon: "Info", title: t("diag_title"), children: [
|
|
8539
|
+
/* @__PURE__ */ jsx("p", { className: "qa-text-10 qa-text-mid", children: t("diag_hint") }),
|
|
8540
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8541
|
+
/* @__PURE__ */ jsxs(
|
|
8542
|
+
"button",
|
|
8543
|
+
{
|
|
8544
|
+
type: "button",
|
|
8545
|
+
onClick: () => void (async () => {
|
|
8546
|
+
setRunning(true);
|
|
8547
|
+
try {
|
|
8548
|
+
setChecks(await runDoctor());
|
|
8549
|
+
} finally {
|
|
8550
|
+
setRunning(false);
|
|
8551
|
+
}
|
|
8552
|
+
})(),
|
|
8553
|
+
disabled: running,
|
|
8554
|
+
"data-qa-doctor": "true",
|
|
8555
|
+
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",
|
|
8556
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8557
|
+
children: [
|
|
8558
|
+
/* @__PURE__ */ jsx(Icon, { name: "Check", size: 13 }),
|
|
8559
|
+
running ? t("diag_running") : t("diag_run")
|
|
8560
|
+
]
|
|
8561
|
+
}
|
|
8562
|
+
),
|
|
8563
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-text-10 qa-text-mid", children: [
|
|
8564
|
+
t("diag_version", { v: QA_VERSION }),
|
|
8565
|
+
latest && isNewer(latest, QA_VERSION) ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8566
|
+
" \xB7 ",
|
|
8567
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-warn", children: t("diag_outdated", { v: latest }) }),
|
|
8568
|
+
/* @__PURE__ */ jsx("code", { className: "qa-ms-1", children: upgradeHint(latest) })
|
|
8569
|
+
] }) : latest ? ` \xB7 ${t("diag_current")}` : ""
|
|
8570
|
+
] })
|
|
8571
|
+
] }),
|
|
8572
|
+
checks.length > 0 && /* @__PURE__ */ jsx("ul", { className: "qa-space-y-1", "data-qa-doctor-results": "true", children: checks.map((c) => /* @__PURE__ */ jsxs("li", { className: "qa-text-11 qa-text-hi", children: [
|
|
8573
|
+
/* @__PURE__ */ jsx(
|
|
8574
|
+
"span",
|
|
8575
|
+
{
|
|
8576
|
+
"aria-hidden": true,
|
|
8577
|
+
className: c.verdict === "ok" ? "qa-text-success" : c.verdict === "bad" ? "qa-text-danger" : c.verdict === "warn" ? "qa-text-warn" : "qa-text-mid",
|
|
8578
|
+
children: c.verdict === "ok" ? "\u25CF" : c.verdict === "bad" ? "\u25B2" : c.verdict === "warn" ? "\u25B2" : "\u25CB"
|
|
8579
|
+
}
|
|
8580
|
+
),
|
|
8581
|
+
" ",
|
|
8582
|
+
/* @__PURE__ */ jsx("strong", { children: c.label }),
|
|
8583
|
+
/* @__PURE__ */ jsx("span", { className: "qa-block qa-text-10 qa-text-mid qa-ms-3", children: c.detail })
|
|
8584
|
+
] }, c.label)) }),
|
|
8585
|
+
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2 qa-flex-wrap", children: [
|
|
8586
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-text-mid", children: t("diag_faults", { n: faults2.length }) }),
|
|
8587
|
+
faults2.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
8588
|
+
/* @__PURE__ */ jsx(
|
|
8589
|
+
"button",
|
|
8590
|
+
{
|
|
8591
|
+
type: "button",
|
|
8592
|
+
onClick: () => void navigator.clipboard?.writeText(faultsAsText(QA_VERSION)),
|
|
8593
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-hi",
|
|
8594
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8595
|
+
children: t("copy_prompt")
|
|
8596
|
+
}
|
|
8597
|
+
),
|
|
8598
|
+
/* @__PURE__ */ jsx(
|
|
8599
|
+
"button",
|
|
8600
|
+
{
|
|
8601
|
+
type: "button",
|
|
8602
|
+
onClick: () => {
|
|
8603
|
+
clearFaults();
|
|
8604
|
+
setFaults([]);
|
|
8605
|
+
},
|
|
8606
|
+
className: "qa-tap qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-0.5 qa-text-10 qa-text-mid",
|
|
8607
|
+
style: { background: "transparent", cursor: "pointer" },
|
|
8608
|
+
children: t("clear_all")
|
|
8609
|
+
}
|
|
8610
|
+
)
|
|
8611
|
+
] })
|
|
8612
|
+
] }),
|
|
8613
|
+
faults2.length > 0 && /* @__PURE__ */ jsx("ul", { className: "qa-space-y-0.5", children: faults2.slice(0, 5).map((f, i) => /* @__PURE__ */ jsxs("li", { className: "qa-text-10 qa-text-mid", children: [
|
|
8614
|
+
/* @__PURE__ */ jsxs("code", { children: [
|
|
8615
|
+
"[",
|
|
8616
|
+
f.where,
|
|
8617
|
+
"]"
|
|
8618
|
+
] }),
|
|
8619
|
+
" ",
|
|
8620
|
+
f.what
|
|
8621
|
+
] }, `${f.at}-${i}`)) })
|
|
8034
8622
|
] })
|
|
8035
8623
|
] })
|
|
8036
8624
|
]
|
|
@@ -8365,17 +8953,17 @@ function QaPanel() {
|
|
|
8365
8953
|
if (typeof window === "undefined") return 0;
|
|
8366
8954
|
const vv = window.visualViewport;
|
|
8367
8955
|
if (!vv) return 0;
|
|
8368
|
-
const
|
|
8369
|
-
if (
|
|
8956
|
+
const overlap2 = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
|
|
8957
|
+
if (overlap2 <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
|
|
8370
8958
|
const panel = panelRef.current;
|
|
8371
8959
|
if (!panel) return 0;
|
|
8372
8960
|
const root2 = panel.getRootNode();
|
|
8373
8961
|
const active = root2.activeElement;
|
|
8374
8962
|
if (!active || !panel.contains(active)) return 0;
|
|
8375
8963
|
const tag = active.tagName;
|
|
8376
|
-
if (tag === "TEXTAREA") return Math.round(
|
|
8964
|
+
if (tag === "TEXTAREA") return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8377
8965
|
if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
|
|
8378
|
-
return Math.round(
|
|
8966
|
+
return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
|
|
8379
8967
|
}
|
|
8380
8968
|
return 0;
|
|
8381
8969
|
} catch {
|
|
@@ -9315,6 +9903,209 @@ function unlockPageScroll() {
|
|
|
9315
9903
|
window.removeEventListener("touchmove", preventScroll, LISTENER_OPTIONS);
|
|
9316
9904
|
}
|
|
9317
9905
|
}
|
|
9906
|
+
|
|
9907
|
+
// src/lib/origin.ts
|
|
9908
|
+
function fiberOf(el) {
|
|
9909
|
+
for (const key in el) {
|
|
9910
|
+
if (key.startsWith("__reactFiber$") || key.startsWith("__reactInternalInstance$")) {
|
|
9911
|
+
return el[key] ?? null;
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
return null;
|
|
9915
|
+
}
|
|
9916
|
+
function nameOf(fiber) {
|
|
9917
|
+
const type = fiber.type;
|
|
9918
|
+
if (!type || typeof type === "string") return null;
|
|
9919
|
+
const direct = type.displayName || type.name;
|
|
9920
|
+
if (direct) return direct;
|
|
9921
|
+
const inner = type.render?.displayName || type.render?.name;
|
|
9922
|
+
return inner || null;
|
|
9923
|
+
}
|
|
9924
|
+
function tidyPath(file) {
|
|
9925
|
+
const marks = ["/src/", "/app/", "/components/", "/pages/", "/lib/"];
|
|
9926
|
+
for (const mark of marks) {
|
|
9927
|
+
const at = file.lastIndexOf(mark);
|
|
9928
|
+
if (at !== -1) return file.slice(at + 1);
|
|
9929
|
+
}
|
|
9930
|
+
return file.split("/").slice(-2).join("/");
|
|
9931
|
+
}
|
|
9932
|
+
var MAX_WALK = 30;
|
|
9933
|
+
function resolveOrigin(el) {
|
|
9934
|
+
if (!el || typeof window === "undefined") return void 0;
|
|
9935
|
+
let fiber;
|
|
9936
|
+
try {
|
|
9937
|
+
fiber = fiberOf(el);
|
|
9938
|
+
} catch {
|
|
9939
|
+
return void 0;
|
|
9940
|
+
}
|
|
9941
|
+
if (!fiber) return void 0;
|
|
9942
|
+
const out = {};
|
|
9943
|
+
let hops = 0;
|
|
9944
|
+
for (let f = fiber; f && hops < MAX_WALK; f = f.return ?? null, hops++) {
|
|
9945
|
+
if (!out.component) {
|
|
9946
|
+
const name = nameOf(f);
|
|
9947
|
+
if (name) out.component = name;
|
|
9948
|
+
}
|
|
9949
|
+
if (!out.file) {
|
|
9950
|
+
const src = f._debugSource;
|
|
9951
|
+
if (src?.fileName) {
|
|
9952
|
+
out.file = tidyPath(src.fileName);
|
|
9953
|
+
if (typeof src.lineNumber === "number") out.line = src.lineNumber;
|
|
9954
|
+
}
|
|
9955
|
+
}
|
|
9956
|
+
if (out.component && out.file) break;
|
|
9957
|
+
}
|
|
9958
|
+
return out.component || out.file ? out : void 0;
|
|
9959
|
+
}
|
|
9960
|
+
|
|
9961
|
+
// src/lib/duplicate.ts
|
|
9962
|
+
var NOISE = /* @__PURE__ */ new Set([
|
|
9963
|
+
"the",
|
|
9964
|
+
"this",
|
|
9965
|
+
"that",
|
|
9966
|
+
"a",
|
|
9967
|
+
"an",
|
|
9968
|
+
"is",
|
|
9969
|
+
"it",
|
|
9970
|
+
"to",
|
|
9971
|
+
"and",
|
|
9972
|
+
"of",
|
|
9973
|
+
"in",
|
|
9974
|
+
"on",
|
|
9975
|
+
"for",
|
|
9976
|
+
"be",
|
|
9977
|
+
"should",
|
|
9978
|
+
"i",
|
|
9979
|
+
"we",
|
|
9980
|
+
"please",
|
|
9981
|
+
"here",
|
|
9982
|
+
"\u0647\u0630\u0627",
|
|
9983
|
+
"\u0647\u0630\u0647",
|
|
9984
|
+
"\u0641\u064A",
|
|
9985
|
+
"\u0645\u0646",
|
|
9986
|
+
"\u0639\u0644\u0649",
|
|
9987
|
+
"\u0627\u0644\u0649",
|
|
9988
|
+
"\u0625\u0644\u0649",
|
|
9989
|
+
"\u0627\u0646",
|
|
9990
|
+
"\u0623\u0646",
|
|
9991
|
+
"\u0648",
|
|
9992
|
+
"\u0627\u0644"
|
|
9993
|
+
]);
|
|
9994
|
+
function words(text) {
|
|
9995
|
+
return new Set(
|
|
9996
|
+
(text || "").toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").split(/\s+/).filter((w) => w.length > 1 && !NOISE.has(w))
|
|
9997
|
+
);
|
|
9998
|
+
}
|
|
9999
|
+
function overlap(a, b) {
|
|
10000
|
+
if (!a.size || !b.size) return 0;
|
|
10001
|
+
let shared = 0;
|
|
10002
|
+
for (const w of a) if (b.has(w)) shared++;
|
|
10003
|
+
return shared / (a.size + b.size - shared);
|
|
10004
|
+
}
|
|
10005
|
+
var SAME_ENOUGH = 0.7;
|
|
10006
|
+
function findDuplicate(text, selector, existing) {
|
|
10007
|
+
const typed = words(text);
|
|
10008
|
+
if (typed.size < 3) return null;
|
|
10009
|
+
for (const note of existing) {
|
|
10010
|
+
if (selector && note.target?.selector && note.target.selector !== selector) continue;
|
|
10011
|
+
if (selector && !note.target?.selector) continue;
|
|
10012
|
+
if (!selector && note.target?.selector) continue;
|
|
10013
|
+
if (overlap(typed, words(note.description)) >= SAME_ENOUGH) return note;
|
|
10014
|
+
}
|
|
10015
|
+
return null;
|
|
10016
|
+
}
|
|
10017
|
+
function recogniser() {
|
|
10018
|
+
if (typeof window === "undefined") return null;
|
|
10019
|
+
const w = window;
|
|
10020
|
+
return w.SpeechRecognition ?? w.webkitSpeechRecognition ?? null;
|
|
10021
|
+
}
|
|
10022
|
+
function VoiceButton({
|
|
10023
|
+
onText,
|
|
10024
|
+
onInterim
|
|
10025
|
+
}) {
|
|
10026
|
+
const { lang, t } = useQa();
|
|
10027
|
+
const [listening, setListening] = useState(false);
|
|
10028
|
+
const [failed, setFailed] = useState(null);
|
|
10029
|
+
const ref = useRef(null);
|
|
10030
|
+
useEffect(() => () => {
|
|
10031
|
+
ref.current?.abort();
|
|
10032
|
+
ref.current = null;
|
|
10033
|
+
}, []);
|
|
10034
|
+
const Ctor = recogniser();
|
|
10035
|
+
if (!Ctor) return null;
|
|
10036
|
+
const stop = () => {
|
|
10037
|
+
ref.current?.stop();
|
|
10038
|
+
ref.current = null;
|
|
10039
|
+
setListening(false);
|
|
10040
|
+
onInterim?.("");
|
|
10041
|
+
};
|
|
10042
|
+
const start = () => {
|
|
10043
|
+
if (listening) {
|
|
10044
|
+
stop();
|
|
10045
|
+
return;
|
|
10046
|
+
}
|
|
10047
|
+
setFailed(null);
|
|
10048
|
+
let rec;
|
|
10049
|
+
try {
|
|
10050
|
+
rec = new Ctor();
|
|
10051
|
+
} catch {
|
|
10052
|
+
setFailed(t("voice_failed"));
|
|
10053
|
+
return;
|
|
10054
|
+
}
|
|
10055
|
+
rec.lang = lang === "ar" ? "ar-IQ" : "en-US";
|
|
10056
|
+
rec.continuous = true;
|
|
10057
|
+
rec.interimResults = true;
|
|
10058
|
+
rec.onresult = (e) => {
|
|
10059
|
+
let done = "";
|
|
10060
|
+
let partial = "";
|
|
10061
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
10062
|
+
const r = e.results[i];
|
|
10063
|
+
const text = r[0]?.transcript ?? "";
|
|
10064
|
+
if (r.isFinal) done += text;
|
|
10065
|
+
else partial += text;
|
|
10066
|
+
}
|
|
10067
|
+
if (done.trim()) onText(done.trim());
|
|
10068
|
+
onInterim?.(partial);
|
|
10069
|
+
};
|
|
10070
|
+
rec.onerror = (e) => {
|
|
10071
|
+
if (e.error && e.error !== "no-speech" && e.error !== "aborted") {
|
|
10072
|
+
setFailed(e.error === "not-allowed" ? t("voice_denied") : t("voice_failed"));
|
|
10073
|
+
}
|
|
10074
|
+
setListening(false);
|
|
10075
|
+
onInterim?.("");
|
|
10076
|
+
};
|
|
10077
|
+
rec.onend = () => {
|
|
10078
|
+
setListening(false);
|
|
10079
|
+
onInterim?.("");
|
|
10080
|
+
};
|
|
10081
|
+
try {
|
|
10082
|
+
rec.start();
|
|
10083
|
+
ref.current = rec;
|
|
10084
|
+
setListening(true);
|
|
10085
|
+
} catch {
|
|
10086
|
+
setFailed(t("voice_failed"));
|
|
10087
|
+
}
|
|
10088
|
+
};
|
|
10089
|
+
return /* @__PURE__ */ jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1.5", children: [
|
|
10090
|
+
/* @__PURE__ */ jsxs(
|
|
10091
|
+
"button",
|
|
10092
|
+
{
|
|
10093
|
+
type: "button",
|
|
10094
|
+
onClick: start,
|
|
10095
|
+
"aria-pressed": listening,
|
|
10096
|
+
title: listening ? t("voice_stop") : t("voice_start"),
|
|
10097
|
+
"data-qa-voice": listening ? "listening" : "idle",
|
|
10098
|
+
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"}`,
|
|
10099
|
+
style: { cursor: "pointer" },
|
|
10100
|
+
children: [
|
|
10101
|
+
/* @__PURE__ */ jsx(Icon, { name: listening ? "Square" : "Mic", size: 12 }),
|
|
10102
|
+
listening ? t("voice_stop") : t("voice_start")
|
|
10103
|
+
]
|
|
10104
|
+
}
|
|
10105
|
+
),
|
|
10106
|
+
failed && /* @__PURE__ */ jsx("span", { className: "qa-text-10 qa-text-danger", children: failed })
|
|
10107
|
+
] });
|
|
10108
|
+
}
|
|
9318
10109
|
var DRAG_THRESHOLD2 = 6;
|
|
9319
10110
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
9320
10111
|
var MIN_REGION_SIZE = 8;
|
|
@@ -9369,6 +10160,8 @@ function CaptureMode() {
|
|
|
9369
10160
|
setCompactCapture,
|
|
9370
10161
|
exactShots,
|
|
9371
10162
|
photographNow,
|
|
10163
|
+
developerMode,
|
|
10164
|
+
notes,
|
|
9372
10165
|
capturePrefill
|
|
9373
10166
|
} = useQa();
|
|
9374
10167
|
const coarse = useCoarsePointer();
|
|
@@ -9384,12 +10177,21 @@ function CaptureMode() {
|
|
|
9384
10177
|
const [shot, setShot] = useState(null);
|
|
9385
10178
|
const stillRef = useRef(null);
|
|
9386
10179
|
const [notesFromThisShot, setNotesFromThisShot] = useState(0);
|
|
10180
|
+
const [wanted, setWanted] = useState("");
|
|
10181
|
+
const [why, setWhy] = useState("");
|
|
10182
|
+
const [fixHint, setFixHint] = useState("");
|
|
10183
|
+
const [origin, setOrigin] = useState(void 0);
|
|
10184
|
+
const [spoken, setSpoken] = useState("");
|
|
9387
10185
|
const [shotEngine, setShotEngine] = useState(null);
|
|
9388
10186
|
const [shotUrl, setShotUrl] = useState(null);
|
|
9389
10187
|
const [capturing, setCapturing] = useState(false);
|
|
9390
10188
|
const [captureError, setCaptureError] = useState(false);
|
|
9391
10189
|
const [tooHeavy, setTooHeavy] = useState(false);
|
|
9392
10190
|
const [description, setDescription] = useState(capturePrefill);
|
|
10191
|
+
const twin = useMemo(
|
|
10192
|
+
() => description.trim().length > 8 ? findDuplicate(description, selection?.selector, notes) : null,
|
|
10193
|
+
[description, selection, notes]
|
|
10194
|
+
);
|
|
9393
10195
|
const [severity, setSeverity] = useState("bug");
|
|
9394
10196
|
const [targetForensics, setTargetForensics] = useState(void 0);
|
|
9395
10197
|
const taRef = useRef(null);
|
|
@@ -9531,6 +10333,7 @@ function CaptureMode() {
|
|
|
9531
10333
|
if (regionRect) {
|
|
9532
10334
|
const sel2 = { kind: "region", rect: regionRect };
|
|
9533
10335
|
setTargetForensics(void 0);
|
|
10336
|
+
setOrigin(void 0);
|
|
9534
10337
|
if (coarse) {
|
|
9535
10338
|
setCandidate(sel2);
|
|
9536
10339
|
setPhase("confirming");
|
|
@@ -9553,6 +10356,7 @@ function CaptureMode() {
|
|
|
9553
10356
|
tagName: el.tagName.toLowerCase()
|
|
9554
10357
|
};
|
|
9555
10358
|
setTargetForensics(collectTargetForensics(el));
|
|
10359
|
+
setOrigin(resolveOrigin(el));
|
|
9556
10360
|
if (coarse) {
|
|
9557
10361
|
setCandidate(sel);
|
|
9558
10362
|
setHover({ rect: sel.rect, selector: sel.selector || "" });
|
|
@@ -9697,8 +10501,12 @@ function CaptureMode() {
|
|
|
9697
10501
|
setShot(null);
|
|
9698
10502
|
setShotEngine(null);
|
|
9699
10503
|
setDescription("");
|
|
10504
|
+
setWanted("");
|
|
10505
|
+
setWhy("");
|
|
10506
|
+
setFixHint("");
|
|
9700
10507
|
setSeverity("bug");
|
|
9701
10508
|
setTargetForensics(void 0);
|
|
10509
|
+
setOrigin(void 0);
|
|
9702
10510
|
setCaptureError(false);
|
|
9703
10511
|
}, []);
|
|
9704
10512
|
const save = async (keepGoing = false) => {
|
|
@@ -9718,9 +10526,14 @@ function CaptureMode() {
|
|
|
9718
10526
|
};
|
|
9719
10527
|
await addNote({
|
|
9720
10528
|
description,
|
|
10529
|
+
wanted,
|
|
10530
|
+
why,
|
|
10531
|
+
fixHint,
|
|
9721
10532
|
screenshot: shot ?? void 0,
|
|
10533
|
+
shotEngine: shotEngine ?? void 0,
|
|
9722
10534
|
target,
|
|
9723
10535
|
severity,
|
|
10536
|
+
origin,
|
|
9724
10537
|
forensics: selection.kind === "element" ? targetForensics : void 0
|
|
9725
10538
|
});
|
|
9726
10539
|
if (keepGoing) {
|
|
@@ -10350,8 +11163,8 @@ function CaptureMode() {
|
|
|
10350
11163
|
]
|
|
10351
11164
|
}
|
|
10352
11165
|
),
|
|
10353
|
-
/* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
10354
|
-
/* @__PURE__ */ jsxs(
|
|
11166
|
+
developerMode && /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
11167
|
+
developerMode && /* @__PURE__ */ jsxs(
|
|
10355
11168
|
"div",
|
|
10356
11169
|
{
|
|
10357
11170
|
role: "group",
|
|
@@ -10381,24 +11194,98 @@ function CaptureMode() {
|
|
|
10381
11194
|
]
|
|
10382
11195
|
}
|
|
10383
11196
|
),
|
|
10384
|
-
/* @__PURE__ */
|
|
10385
|
-
"
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
11197
|
+
twin && /* @__PURE__ */ jsxs("p", { className: "qa-text-10 qa-text-warn", "data-qa-duplicate": "true", children: [
|
|
11198
|
+
t("dup_notice", { when: new Date(twin.timestamp).toLocaleTimeString() }),
|
|
11199
|
+
" \u201C",
|
|
11200
|
+
twin.description.slice(0, 70),
|
|
11201
|
+
twin.description.length > 70 ? "\u2026" : "",
|
|
11202
|
+
"\u201D"
|
|
11203
|
+
] }),
|
|
11204
|
+
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11205
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11206
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_observed") }),
|
|
11207
|
+
/* @__PURE__ */ jsx(
|
|
11208
|
+
VoiceButton,
|
|
11209
|
+
{
|
|
11210
|
+
onText: (text) => setDescription((d) => d ? `${d} ${text}` : text),
|
|
11211
|
+
onInterim: setSpoken
|
|
10395
11212
|
}
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
|
|
10401
|
-
|
|
11213
|
+
)
|
|
11214
|
+
] }),
|
|
11215
|
+
/* @__PURE__ */ jsx(
|
|
11216
|
+
"textarea",
|
|
11217
|
+
{
|
|
11218
|
+
ref: taRef,
|
|
11219
|
+
value: description + (spoken ? ` ${spoken}` : ""),
|
|
11220
|
+
onChange: (e) => setDescription(e.target.value),
|
|
11221
|
+
onKeyDown: (e) => {
|
|
11222
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11223
|
+
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
11224
|
+
e.preventDefault();
|
|
11225
|
+
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
11226
|
+
}
|
|
11227
|
+
},
|
|
11228
|
+
rows: 2,
|
|
11229
|
+
placeholder: t("q_observed_hint"),
|
|
11230
|
+
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"
|
|
11231
|
+
}
|
|
11232
|
+
)
|
|
11233
|
+
] }),
|
|
11234
|
+
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11235
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11236
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_wanted") }),
|
|
11237
|
+
/* @__PURE__ */ jsx(VoiceButton, { onText: (x) => setWanted((d) => d ? `${d} ${x}` : x) })
|
|
11238
|
+
] }),
|
|
11239
|
+
/* @__PURE__ */ jsx(
|
|
11240
|
+
"textarea",
|
|
11241
|
+
{
|
|
11242
|
+
value: wanted,
|
|
11243
|
+
onChange: (e) => setWanted(e.target.value),
|
|
11244
|
+
onKeyDown: (e) => {
|
|
11245
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11246
|
+
},
|
|
11247
|
+
rows: 2,
|
|
11248
|
+
placeholder: t("q_wanted_hint"),
|
|
11249
|
+
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"
|
|
11250
|
+
}
|
|
11251
|
+
)
|
|
11252
|
+
] }),
|
|
11253
|
+
/* @__PURE__ */ jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11254
|
+
/* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
|
|
11255
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_why") }),
|
|
11256
|
+
/* @__PURE__ */ jsx(VoiceButton, { onText: (x) => setWhy((d) => d ? `${d} ${x}` : x) })
|
|
11257
|
+
] }),
|
|
11258
|
+
/* @__PURE__ */ jsx(
|
|
11259
|
+
"textarea",
|
|
11260
|
+
{
|
|
11261
|
+
value: why,
|
|
11262
|
+
onChange: (e) => setWhy(e.target.value),
|
|
11263
|
+
onKeyDown: (e) => {
|
|
11264
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11265
|
+
},
|
|
11266
|
+
rows: 1,
|
|
11267
|
+
placeholder: t("q_why_hint"),
|
|
11268
|
+
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"
|
|
11269
|
+
}
|
|
11270
|
+
)
|
|
11271
|
+
] }),
|
|
11272
|
+
developerMode && /* @__PURE__ */ jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
|
|
11273
|
+
/* @__PURE__ */ jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_fix") }),
|
|
11274
|
+
/* @__PURE__ */ jsx(
|
|
11275
|
+
"textarea",
|
|
11276
|
+
{
|
|
11277
|
+
value: fixHint,
|
|
11278
|
+
onChange: (e) => setFixHint(e.target.value),
|
|
11279
|
+
rows: 1,
|
|
11280
|
+
placeholder: t("q_fix_hint"),
|
|
11281
|
+
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"
|
|
11282
|
+
}
|
|
11283
|
+
)
|
|
11284
|
+
] }),
|
|
11285
|
+
developerMode && origin && (origin.component || origin.file) && /* @__PURE__ */ jsxs("p", { className: "qa-text-10 qa-text-mid", "data-qa-origin": "true", children: [
|
|
11286
|
+
origin.component ?? "\u2014",
|
|
11287
|
+
origin.file ? ` \xB7 ${origin.file}${origin.line ? `:${origin.line}` : ""}` : ""
|
|
11288
|
+
] }),
|
|
10402
11289
|
/* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
|
|
10403
11290
|
/* @__PURE__ */ jsxs(
|
|
10404
11291
|
"button",
|
|
@@ -10440,8 +11327,7 @@ function CaptureMode() {
|
|
|
10440
11327
|
] }),
|
|
10441
11328
|
/* @__PURE__ */ jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
|
|
10442
11329
|
t("save_hint"),
|
|
10443
|
-
|
|
10444
|
-
t("severity_keys")
|
|
11330
|
+
developerMode ? ` \xB7 ${t("severity_keys")}` : ""
|
|
10445
11331
|
] })
|
|
10446
11332
|
] })
|
|
10447
11333
|
]
|
|
@@ -10647,5 +11533,5 @@ function Qapture({ config }) {
|
|
|
10647
11533
|
}
|
|
10648
11534
|
|
|
10649
11535
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
10650
|
-
//# sourceMappingURL=chunk-
|
|
10651
|
-
//# sourceMappingURL=chunk-
|
|
11536
|
+
//# sourceMappingURL=chunk-B22X5Y6U.js.map
|
|
11537
|
+
//# sourceMappingURL=chunk-B22X5Y6U.js.map
|