qapture2 0.9.0 → 0.9.1
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-B22X5Y6U.js → chunk-HEHVZCSV.js} +139 -311
- package/dist/{chunk-5EWNGJ2N.cjs → chunk-J77SGE5E.cjs} +139 -311
- package/dist/index.cjs +5 -5
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -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
|
@@ -177,6 +177,7 @@ var DEFAULTS = {
|
|
|
177
177
|
loginField: { en: "Username", ar: "\u0627\u0633\u0645 \u0627\u0644\u0645\u0633\u062A\u062E\u062F\u0645" },
|
|
178
178
|
rtl: false,
|
|
179
179
|
beta: false,
|
|
180
|
+
collector: null,
|
|
180
181
|
visible: void 0,
|
|
181
182
|
alwaysVisible: false,
|
|
182
183
|
hotkey: "shift+alt+q",
|
|
@@ -349,6 +350,7 @@ function validateConfig(input) {
|
|
|
349
350
|
preamble: null,
|
|
350
351
|
rtl: DEFAULTS.rtl,
|
|
351
352
|
beta: DEFAULTS.beta,
|
|
353
|
+
collector: null,
|
|
352
354
|
visible: DEFAULTS.visible,
|
|
353
355
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
354
356
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -370,6 +372,7 @@ function validateConfig(input) {
|
|
|
370
372
|
preamble: null,
|
|
371
373
|
rtl: DEFAULTS.rtl,
|
|
372
374
|
beta: DEFAULTS.beta,
|
|
375
|
+
collector: null,
|
|
373
376
|
visible: DEFAULTS.visible,
|
|
374
377
|
alwaysVisible: DEFAULTS.alwaysVisible,
|
|
375
378
|
hotkey: DEFAULTS.hotkey,
|
|
@@ -406,6 +409,14 @@ function validateConfig(input) {
|
|
|
406
409
|
const preamble = raw["preamble"] !== void 0 ? coercePreamble(raw["preamble"]) : null;
|
|
407
410
|
const rtl = typeof raw["rtl"] === "boolean" ? raw["rtl"] : DEFAULTS.rtl;
|
|
408
411
|
const beta = typeof raw["beta"] === "boolean" ? raw["beta"] : DEFAULTS.beta;
|
|
412
|
+
const rawCol = raw["collector"];
|
|
413
|
+
const collector = rawCol && typeof rawCol["url"] === "string" && typeof rawCol["token"] === "string" && typeof rawCol["project"] === "string" ? {
|
|
414
|
+
url: rawCol["url"],
|
|
415
|
+
token: rawCol["token"],
|
|
416
|
+
project: rawCol["project"],
|
|
417
|
+
campaign: typeof rawCol["campaign"] === "string" ? rawCol["campaign"] : void 0,
|
|
418
|
+
tester: typeof rawCol["tester"] === "string" ? rawCol["tester"] : void 0
|
|
419
|
+
} : DEFAULTS.collector;
|
|
409
420
|
const alwaysVisible = typeof raw["alwaysVisible"] === "boolean" ? raw["alwaysVisible"] : DEFAULTS.alwaysVisible;
|
|
410
421
|
const hotkey = isNonEmptyString(raw["hotkey"]) ? raw["hotkey"].trim() : DEFAULTS.hotkey;
|
|
411
422
|
const captureHotkey = isNonEmptyString(raw["captureHotkey"]) ? raw["captureHotkey"].trim() : DEFAULTS.captureHotkey;
|
|
@@ -429,6 +440,7 @@ function validateConfig(input) {
|
|
|
429
440
|
preamble,
|
|
430
441
|
rtl,
|
|
431
442
|
beta,
|
|
443
|
+
collector,
|
|
432
444
|
visible,
|
|
433
445
|
alwaysVisible,
|
|
434
446
|
hotkey,
|
|
@@ -1706,6 +1718,98 @@ function createStorage(namespace) {
|
|
|
1706
1718
|
return { getItem, setItem, getJSON, setJSON };
|
|
1707
1719
|
}
|
|
1708
1720
|
|
|
1721
|
+
// src/lib/faultLog.ts
|
|
1722
|
+
var LIMIT = 40;
|
|
1723
|
+
var faults = [];
|
|
1724
|
+
function recordFault(where, err) {
|
|
1725
|
+
const what = err instanceof Error ? `${err.name}: ${err.message}` : typeof err === "string" ? err : (() => {
|
|
1726
|
+
try {
|
|
1727
|
+
return JSON.stringify(err);
|
|
1728
|
+
} catch {
|
|
1729
|
+
return String(err);
|
|
1730
|
+
}
|
|
1731
|
+
})();
|
|
1732
|
+
faults.push({ at: Date.now(), where, what: what.slice(0, 500) });
|
|
1733
|
+
if (faults.length > LIMIT) faults.splice(0, faults.length - LIMIT);
|
|
1734
|
+
console.warn(`[QA] ${where}:`, err);
|
|
1735
|
+
}
|
|
1736
|
+
function readFaults() {
|
|
1737
|
+
return [...faults].reverse();
|
|
1738
|
+
}
|
|
1739
|
+
function clearFaults() {
|
|
1740
|
+
faults.length = 0;
|
|
1741
|
+
}
|
|
1742
|
+
function faultsAsText(version) {
|
|
1743
|
+
if (!faults.length) return "No faults recorded.";
|
|
1744
|
+
const head = [
|
|
1745
|
+
`qapture ${version}`,
|
|
1746
|
+
typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
1747
|
+
typeof location !== "undefined" ? location.href.split("?")[0] : "",
|
|
1748
|
+
""
|
|
1749
|
+
].filter(Boolean).join("\n");
|
|
1750
|
+
return head + readFaults().map((f) => `${new Date(f.at).toISOString()} [${f.where}] ${f.what}`).join("\n");
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
// src/lib/collector.ts
|
|
1754
|
+
var TIMEOUT_MS = 8e3;
|
|
1755
|
+
var MAX_SHOT_BYTES = 6 * 1024 * 1024;
|
|
1756
|
+
function blobToDataUrl(blob) {
|
|
1757
|
+
return new Promise((resolve) => {
|
|
1758
|
+
try {
|
|
1759
|
+
const reader = new FileReader();
|
|
1760
|
+
reader.onload = () => resolve(typeof reader.result === "string" ? reader.result : null);
|
|
1761
|
+
reader.onerror = () => resolve(null);
|
|
1762
|
+
reader.readAsDataURL(blob);
|
|
1763
|
+
} catch {
|
|
1764
|
+
resolve(null);
|
|
1765
|
+
}
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
async function sendToCollector(note, cfg) {
|
|
1769
|
+
if (typeof fetch === "undefined" || !cfg?.url || !cfg.token || !cfg.project) return false;
|
|
1770
|
+
let shot;
|
|
1771
|
+
if (note.screenshot && note.screenshot.size <= MAX_SHOT_BYTES) {
|
|
1772
|
+
shot = await blobToDataUrl(note.screenshot) ?? void 0;
|
|
1773
|
+
}
|
|
1774
|
+
const controller = new AbortController();
|
|
1775
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
1776
|
+
try {
|
|
1777
|
+
const res = await fetch(`${cfg.url.replace(/\/$/, "")}/notes`, {
|
|
1778
|
+
method: "POST",
|
|
1779
|
+
signal: controller.signal,
|
|
1780
|
+
headers: {
|
|
1781
|
+
"content-type": "application/json",
|
|
1782
|
+
authorization: `Bearer ${cfg.token}`
|
|
1783
|
+
},
|
|
1784
|
+
body: JSON.stringify({
|
|
1785
|
+
project: cfg.project,
|
|
1786
|
+
// A campaign per day is the shape that matches how testing actually
|
|
1787
|
+
// happens, and it means nobody has to name anything.
|
|
1788
|
+
campaign: cfg.campaign || (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1789
|
+
tester: cfg.tester,
|
|
1790
|
+
id: note.id,
|
|
1791
|
+
route: note.route,
|
|
1792
|
+
description: note.description,
|
|
1793
|
+
wanted: note.wanted,
|
|
1794
|
+
why: note.why,
|
|
1795
|
+
severity: note.severity,
|
|
1796
|
+
origin: note.origin,
|
|
1797
|
+
shot
|
|
1798
|
+
})
|
|
1799
|
+
});
|
|
1800
|
+
if (!res.ok) {
|
|
1801
|
+
recordFault("collector", `server answered ${res.status}`);
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
return true;
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
recordFault("collector", err);
|
|
1807
|
+
return false;
|
|
1808
|
+
} finally {
|
|
1809
|
+
clearTimeout(timer);
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1709
1813
|
// src/lib/strings.ts
|
|
1710
1814
|
var STR = {
|
|
1711
1815
|
en: {
|
|
@@ -2881,38 +2985,6 @@ function neutralizeDocumentColors(doc, aggressive = false) {
|
|
|
2881
2985
|
return touched;
|
|
2882
2986
|
}
|
|
2883
2987
|
|
|
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
|
-
|
|
2916
2988
|
// src/lib/capture.ts
|
|
2917
2989
|
var HTML2CANVAS_TIMEOUT_MS = 1e4;
|
|
2918
2990
|
var CHUNK_TIMEOUT_MS = 8e3;
|
|
@@ -3355,7 +3427,7 @@ function noteCheckLine(note, index) {
|
|
|
3355
3427
|
const where = oneLine(note.route) || "/";
|
|
3356
3428
|
const wanted = oneLine(note.wanted);
|
|
3357
3429
|
const seen = oneLine(note.description) || "(not described)";
|
|
3358
|
-
const claim = wanted ||
|
|
3430
|
+
const claim = wanted || seen;
|
|
3359
3431
|
const trimmed = claim.length > 180 ? `${claim.slice(0, 177)}...` : claim;
|
|
3360
3432
|
return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
|
|
3361
3433
|
}
|
|
@@ -3405,12 +3477,12 @@ function noteToMarkdown(note, opts) {
|
|
|
3405
3477
|
lines.push("### Observed");
|
|
3406
3478
|
lines.push("");
|
|
3407
3479
|
lines.push(oneLine(note.description) ? note.description.trim() : "_(not described)_");
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3480
|
+
if (note.wanted && oneLine(note.wanted)) {
|
|
3481
|
+
lines.push("");
|
|
3482
|
+
lines.push("### Expected");
|
|
3483
|
+
lines.push("");
|
|
3484
|
+
lines.push(note.wanted.trim());
|
|
3485
|
+
}
|
|
3414
3486
|
if (note.why && oneLine(note.why)) {
|
|
3415
3487
|
lines.push("");
|
|
3416
3488
|
lines.push("### Why it matters");
|
|
@@ -3700,7 +3772,8 @@ Hand \`verify.md\` back with the work.
|
|
|
3700
3772
|
|
|
3701
3773
|
### What is in this archive
|
|
3702
3774
|
|
|
3703
|
-
- \`notes.md\` \u2014 the points themselves
|
|
3775
|
+
- \`notes.md\` \u2014 the points themselves, under an **Observed** heading. Most carry only that: the tester writes one sentence about what is wrong, which is the right amount to ask of somebody who is not an engineer. An **Expected** heading appears where somebody stated one.
|
|
3776
|
+
Where a point is ambiguous, **ask \u2014 do not pick a reading and commit to it.** That failure mode is specific to you: a person would stop and check, and an agent tends to fill the gap in and carry on. One question costs a message. A confident fix to the wrong problem costs the round.
|
|
3704
3777
|
- \`verify.md\` \u2014 the checklist, one unticked box per point.
|
|
3705
3778
|
- \`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
3779
|
- \`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.
|
|
@@ -4525,18 +4598,6 @@ async function requestPersistentStorage() {
|
|
|
4525
4598
|
return false;
|
|
4526
4599
|
}
|
|
4527
4600
|
}
|
|
4528
|
-
function formatBytes(bytes) {
|
|
4529
|
-
if (!Number.isFinite(bytes) || bytes <= 0) return "0 KB";
|
|
4530
|
-
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
4531
|
-
let value = bytes;
|
|
4532
|
-
let i = 0;
|
|
4533
|
-
while (value >= 1024 && i < units.length - 1) {
|
|
4534
|
-
value /= 1024;
|
|
4535
|
-
i++;
|
|
4536
|
-
}
|
|
4537
|
-
const decimals = value < 10 && i > 1 ? 1 : 0;
|
|
4538
|
-
return `${value.toFixed(decimals)} ${units[i]}`;
|
|
4539
|
-
}
|
|
4540
4601
|
function estimateOwnBytes(notes) {
|
|
4541
4602
|
let total = 0;
|
|
4542
4603
|
for (const n of notes) {
|
|
@@ -5038,8 +5099,11 @@ function QaProvider({
|
|
|
5038
5099
|
});
|
|
5039
5100
|
}
|
|
5040
5101
|
await syncNoteThrough(note);
|
|
5102
|
+
if (config.collector) {
|
|
5103
|
+
void sendToCollector(note, config.collector);
|
|
5104
|
+
}
|
|
5041
5105
|
},
|
|
5042
|
-
[idb, config.journey, config.captureContext, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
5106
|
+
[idb, config.journey, config.captureContext, config.collector, testAlong, testAlongSteps, notify, t, syncNoteThrough, applyNotes]
|
|
5043
5107
|
);
|
|
5044
5108
|
const updateNote = React.useCallback(
|
|
5045
5109
|
async (id, patch) => {
|
|
@@ -8150,7 +8214,7 @@ function upgradeHint(latest) {
|
|
|
8150
8214
|
}
|
|
8151
8215
|
|
|
8152
8216
|
// src/version.ts
|
|
8153
|
-
var QA_VERSION = "0.9.
|
|
8217
|
+
var QA_VERSION = "0.9.1" ;
|
|
8154
8218
|
function Section({
|
|
8155
8219
|
icon,
|
|
8156
8220
|
title,
|
|
@@ -8256,8 +8320,8 @@ function SettingsSheet({ onClose }) {
|
|
|
8256
8320
|
const syncing = sync.state === "syncing";
|
|
8257
8321
|
const viaZip = sync.engine === "download";
|
|
8258
8322
|
const quotaKnown = storageHealth.supported && storageHealth.quotaBytes > 0;
|
|
8259
|
-
|
|
8260
|
-
|
|
8323
|
+
quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
|
|
8324
|
+
storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
|
|
8261
8325
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
8262
8326
|
"div",
|
|
8263
8327
|
{
|
|
@@ -8386,77 +8450,6 @@ function SettingsSheet({ onClose }) {
|
|
|
8386
8450
|
)
|
|
8387
8451
|
] }),
|
|
8388
8452
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8389
|
-
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "HardDrive", title: t("storage_title"), children: [
|
|
8390
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("storage_explain") }),
|
|
8391
|
-
quotaKnown && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
8392
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8393
|
-
"div",
|
|
8394
|
-
{
|
|
8395
|
-
className: "qa-w-full qa-rounded-full qa-overflow-hidden qa-bg-3",
|
|
8396
|
-
role: "img",
|
|
8397
|
-
"aria-label": t("storage_used", {
|
|
8398
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
8399
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
8400
|
-
}),
|
|
8401
|
-
style: { height: 6 },
|
|
8402
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { width: `${usedPct}%`, height: "100%", background: meterColor } })
|
|
8403
|
-
}
|
|
8404
|
-
),
|
|
8405
|
-
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-text-10 qa-text-mid", children: [
|
|
8406
|
-
t("storage_used", {
|
|
8407
|
-
used: formatBytes(storageHealth.usageBytes),
|
|
8408
|
-
quota: formatBytes(storageHealth.quotaBytes)
|
|
8409
|
-
}),
|
|
8410
|
-
" \xB7 ",
|
|
8411
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-lo", children: [
|
|
8412
|
-
"Qapture ",
|
|
8413
|
-
formatBytes(storageHealth.ownBytes)
|
|
8414
|
-
] })
|
|
8415
|
-
] })
|
|
8416
|
-
] }),
|
|
8417
|
-
/* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-items-start qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
|
|
8418
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
8419
|
-
"input",
|
|
8420
|
-
{
|
|
8421
|
-
type: "checkbox",
|
|
8422
|
-
checked: autoBackup,
|
|
8423
|
-
onChange: (e) => setAutoBackup(e.target.checked),
|
|
8424
|
-
style: { marginTop: 2 }
|
|
8425
|
-
}
|
|
8426
|
-
),
|
|
8427
|
-
/* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
8428
|
-
t("autosave_label"),
|
|
8429
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-block qa-text-10 qa-text-lo qa-leading-relaxed", children: t("autosave_hint", { n: autoBackupEvery }) })
|
|
8430
|
-
] })
|
|
8431
|
-
] }),
|
|
8432
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-flex-wrap qa-gap-2", children: [
|
|
8433
|
-
storageHealth.persisted ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-bg-success-tint qa-text-success qa-px-2 qa-py-0.5 qa-text-10", children: [
|
|
8434
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 11 }),
|
|
8435
|
-
t("persist_on")
|
|
8436
|
-
] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
8437
|
-
"button",
|
|
8438
|
-
{
|
|
8439
|
-
type: "button",
|
|
8440
|
-
disabled: busy,
|
|
8441
|
-
onClick: () => void run2(requestPersistentStorage2),
|
|
8442
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
8443
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
8444
|
-
children: t("persist_keep")
|
|
8445
|
-
}
|
|
8446
|
-
),
|
|
8447
|
-
notes.some((n) => n.screenshot) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
8448
|
-
"button",
|
|
8449
|
-
{
|
|
8450
|
-
type: "button",
|
|
8451
|
-
disabled: busy,
|
|
8452
|
-
onClick: () => void run2(dropAllScreenshots),
|
|
8453
|
-
className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-10 qa-text-mid",
|
|
8454
|
-
style: { background: "transparent", cursor: "pointer" },
|
|
8455
|
-
children: t("drop_shots")
|
|
8456
|
-
}
|
|
8457
|
-
)
|
|
8458
|
-
] })
|
|
8459
|
-
] }),
|
|
8460
8453
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
|
|
8461
8454
|
/* @__PURE__ */ jsxRuntime.jsxs(Section, { icon: "Camera", title: t("exact_label"), children: [
|
|
8462
8455
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-m-0 qa-text-10 qa-text-lo qa-leading-relaxed", children: t("exact_hint") }),
|
|
@@ -10021,98 +10014,6 @@ function findDuplicate(text, selector, existing) {
|
|
|
10021
10014
|
}
|
|
10022
10015
|
return null;
|
|
10023
10016
|
}
|
|
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
|
-
}
|
|
10116
10017
|
var DRAG_THRESHOLD2 = 6;
|
|
10117
10018
|
var TOUCH_DRAG_THRESHOLD = 12;
|
|
10118
10019
|
var MIN_REGION_SIZE = 8;
|
|
@@ -10184,11 +10085,7 @@ function CaptureMode() {
|
|
|
10184
10085
|
const [shot, setShot] = React.useState(null);
|
|
10185
10086
|
const stillRef = React.useRef(null);
|
|
10186
10087
|
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
10088
|
const [origin, setOrigin] = React.useState(void 0);
|
|
10191
|
-
const [spoken, setSpoken] = React.useState("");
|
|
10192
10089
|
const [shotEngine, setShotEngine] = React.useState(null);
|
|
10193
10090
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
10194
10091
|
const [capturing, setCapturing] = React.useState(false);
|
|
@@ -10508,9 +10405,6 @@ function CaptureMode() {
|
|
|
10508
10405
|
setShot(null);
|
|
10509
10406
|
setShotEngine(null);
|
|
10510
10407
|
setDescription("");
|
|
10511
|
-
setWanted("");
|
|
10512
|
-
setWhy("");
|
|
10513
|
-
setFixHint("");
|
|
10514
10408
|
setSeverity("bug");
|
|
10515
10409
|
setTargetForensics(void 0);
|
|
10516
10410
|
setOrigin(void 0);
|
|
@@ -10533,9 +10427,6 @@ function CaptureMode() {
|
|
|
10533
10427
|
};
|
|
10534
10428
|
await addNote({
|
|
10535
10429
|
description,
|
|
10536
|
-
wanted,
|
|
10537
|
-
why,
|
|
10538
|
-
fixHint,
|
|
10539
10430
|
screenshot: shot ?? void 0,
|
|
10540
10431
|
shotEngine: shotEngine ?? void 0,
|
|
10541
10432
|
target,
|
|
@@ -11208,87 +11099,24 @@ function CaptureMode() {
|
|
|
11208
11099
|
twin.description.length > 70 ? "\u2026" : "",
|
|
11209
11100
|
"\u201D"
|
|
11210
11101
|
] }),
|
|
11211
|
-
/* @__PURE__ */ jsxRuntime.
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11102
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
11103
|
+
"textarea",
|
|
11104
|
+
{
|
|
11105
|
+
ref: taRef,
|
|
11106
|
+
value: description,
|
|
11107
|
+
onChange: (e) => setDescription(e.target.value),
|
|
11108
|
+
onKeyDown: (e) => {
|
|
11109
|
+
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
|
|
11110
|
+
if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
|
|
11111
|
+
e.preventDefault();
|
|
11112
|
+
setSeverity(SEVERITIES2[Number(e.key) - 1]);
|
|
11219
11113
|
}
|
|
11220
|
-
|
|
11221
|
-
|
|
11222
|
-
|
|
11223
|
-
"
|
|
11224
|
-
|
|
11225
|
-
|
|
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
|
-
] }),
|
|
11114
|
+
},
|
|
11115
|
+
rows: 3,
|
|
11116
|
+
placeholder: t("annotate_placeholder"),
|
|
11117
|
+
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"
|
|
11118
|
+
}
|
|
11119
|
+
),
|
|
11292
11120
|
developerMode && origin && (origin.component || origin.file) && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-text-10 qa-text-mid", "data-qa-origin": "true", children: [
|
|
11293
11121
|
origin.component ?? "\u2014",
|
|
11294
11122
|
origin.file ? ` \xB7 ${origin.file}${origin.line ? `:${origin.line}` : ""}` : ""
|
|
@@ -11542,5 +11370,5 @@ function Qapture({ config }) {
|
|
|
11542
11370
|
exports.Qapture = Qapture;
|
|
11543
11371
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
11544
11372
|
exports.initQaStudio = initQaStudio;
|
|
11545
|
-
//# sourceMappingURL=chunk-
|
|
11546
|
-
//# sourceMappingURL=chunk-
|
|
11373
|
+
//# sourceMappingURL=chunk-J77SGE5E.cjs.map
|
|
11374
|
+
//# sourceMappingURL=chunk-J77SGE5E.cjs.map
|
package/dist/index.cjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkJ77SGE5E_cjs = require('./chunk-J77SGE5E.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, "QaStudio", {
|
|
8
8
|
enumerable: true,
|
|
9
|
-
get: function () { return
|
|
9
|
+
get: function () { return chunkJ77SGE5E_cjs.Qapture; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "Qapture", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkJ77SGE5E_cjs.Qapture; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "deleteQaDatabase", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkJ77SGE5E_cjs.deleteQaDatabase; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "initQaStudio", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
21
|
+
get: function () { return chunkJ77SGE5E_cjs.initQaStudio; }
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=index.cjs.map
|
|
24
24
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.d.cts
CHANGED
|
@@ -125,6 +125,24 @@ type QaConfig = {
|
|
|
125
125
|
* no interruption. It is a label, not an announcement.
|
|
126
126
|
*/
|
|
127
127
|
beta?: boolean;
|
|
128
|
+
/**
|
|
129
|
+
* Post each note to a collector as it is written, so the client never has to
|
|
130
|
+
* remember to export and send anything.
|
|
131
|
+
*
|
|
132
|
+
* Purely additive: notes are saved locally first and the export is unchanged,
|
|
133
|
+
* so a collector that is down or unreachable costs nothing at all.
|
|
134
|
+
*
|
|
135
|
+
* The token here is WRITE-ONLY and ships to the browser, which is fine by
|
|
136
|
+
* design: it can add notes to one project and cannot read anything back.
|
|
137
|
+
* Never put the collector's admin token in a page.
|
|
138
|
+
*/
|
|
139
|
+
collector?: {
|
|
140
|
+
url: string;
|
|
141
|
+
token: string;
|
|
142
|
+
project: string;
|
|
143
|
+
campaign?: string;
|
|
144
|
+
tester?: string;
|
|
145
|
+
};
|
|
128
146
|
/**
|
|
129
147
|
* Whether the panel is visible.
|
|
130
148
|
* - true / false: always show / always hide
|
|
@@ -169,6 +187,13 @@ type ResolvedConfig = {
|
|
|
169
187
|
preamble: QaPreamble | null;
|
|
170
188
|
rtl: boolean;
|
|
171
189
|
beta: boolean;
|
|
190
|
+
collector: {
|
|
191
|
+
url: string;
|
|
192
|
+
token: string;
|
|
193
|
+
project: string;
|
|
194
|
+
campaign?: string;
|
|
195
|
+
tester?: string;
|
|
196
|
+
} | null;
|
|
172
197
|
/**
|
|
173
198
|
* Visibility sentinel.
|
|
174
199
|
* - true: always show
|