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.
@@ -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 || `${seen} \u2014 _no expectation was given; ask before assuming one_`;
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
- lines.push("");
3409
- lines.push("### Expected");
3410
- lines.push("");
3411
- lines.push(
3412
- note.wanted && oneLine(note.wanted) ? note.wanted.trim() : "_(the tester did not say what they expected instead -- ask rather than assume)_"
3413
- );
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. 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**.
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.0" ;
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
- const usedPct = quotaKnown ? Math.min(100, Math.round(storageHealth.ratio * 100)) : 0;
8260
- const meterColor = storageHealth.level === "critical" ? "var(--qa-danger)" : storageHealth.level === "warn" ? "var(--qa-warn)" : "var(--qa-accent)";
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.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
11212
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
11213
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_observed") }),
11214
- /* @__PURE__ */ jsxRuntime.jsx(
11215
- VoiceButton,
11216
- {
11217
- onText: (text) => setDescription((d) => d ? `${d} ${text}` : text),
11218
- onInterim: setSpoken
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
- /* @__PURE__ */ jsxRuntime.jsx(
11223
- "textarea",
11224
- {
11225
- ref: taRef,
11226
- value: description + (spoken ? ` ${spoken}` : ""),
11227
- onChange: (e) => setDescription(e.target.value),
11228
- onKeyDown: (e) => {
11229
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
11230
- if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
11231
- e.preventDefault();
11232
- setSeverity(SEVERITIES2[Number(e.key) - 1]);
11233
- }
11234
- },
11235
- rows: 2,
11236
- placeholder: t("q_observed_hint"),
11237
- className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
11238
- }
11239
- )
11240
- ] }),
11241
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
11242
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
11243
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-font-semibold qa-text-hi", children: t("q_wanted") }),
11244
- /* @__PURE__ */ jsxRuntime.jsx(VoiceButton, { onText: (x) => setWanted((d) => d ? `${d} ${x}` : x) })
11245
- ] }),
11246
- /* @__PURE__ */ jsxRuntime.jsx(
11247
- "textarea",
11248
- {
11249
- value: wanted,
11250
- onChange: (e) => setWanted(e.target.value),
11251
- onKeyDown: (e) => {
11252
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
11253
- },
11254
- rows: 2,
11255
- placeholder: t("q_wanted_hint"),
11256
- className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
11257
- }
11258
- )
11259
- ] }),
11260
- /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
11261
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-justify-between qa-gap-2", children: [
11262
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_why") }),
11263
- /* @__PURE__ */ jsxRuntime.jsx(VoiceButton, { onText: (x) => setWhy((d) => d ? `${d} ${x}` : x) })
11264
- ] }),
11265
- /* @__PURE__ */ jsxRuntime.jsx(
11266
- "textarea",
11267
- {
11268
- value: why,
11269
- onChange: (e) => setWhy(e.target.value),
11270
- onKeyDown: (e) => {
11271
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
11272
- },
11273
- rows: 1,
11274
- placeholder: t("q_why_hint"),
11275
- className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
11276
- }
11277
- )
11278
- ] }),
11279
- developerMode && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "qa-flex qa-flex-col qa-gap-1", children: [
11280
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid", children: t("q_fix") }),
11281
- /* @__PURE__ */ jsxRuntime.jsx(
11282
- "textarea",
11283
- {
11284
- value: fixHint,
11285
- onChange: (e) => setFixHint(e.target.value),
11286
- rows: 1,
11287
- placeholder: t("q_fix_hint"),
11288
- className: "qa-w-full qa-resize-y qa-rounded-lg qa-border qa-border-subtle qa-bg-0 qa-text-hi qa-px-2 qa-py-1.5 qa-text-sm qa-focus-ring"
11289
- }
11290
- )
11291
- ] }),
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-5EWNGJ2N.cjs.map
11546
- //# sourceMappingURL=chunk-5EWNGJ2N.cjs.map
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 chunk5EWNGJ2N_cjs = require('./chunk-5EWNGJ2N.cjs');
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 chunk5EWNGJ2N_cjs.Qapture; }
9
+ get: function () { return chunkJ77SGE5E_cjs.Qapture; }
10
10
  });
11
11
  Object.defineProperty(exports, "Qapture", {
12
12
  enumerable: true,
13
- get: function () { return chunk5EWNGJ2N_cjs.Qapture; }
13
+ get: function () { return chunkJ77SGE5E_cjs.Qapture; }
14
14
  });
15
15
  Object.defineProperty(exports, "deleteQaDatabase", {
16
16
  enumerable: true,
17
- get: function () { return chunk5EWNGJ2N_cjs.deleteQaDatabase; }
17
+ get: function () { return chunkJ77SGE5E_cjs.deleteQaDatabase; }
18
18
  });
19
19
  Object.defineProperty(exports, "initQaStudio", {
20
20
  enumerable: true,
21
- get: function () { return chunk5EWNGJ2N_cjs.initQaStudio; }
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