qapture2 0.8.3 → 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.
@@ -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 LIMIT = 6;
317
- const shown = missing.slice(0, LIMIT).join("; ");
318
- const rest = missing.length > LIMIT ? ` (+${missing.length - LIMIT} more)` : "";
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.",
@@ -2820,6 +2874,38 @@ function neutralizeDocumentColors(doc, aggressive = false) {
2820
2874
  return touched;
2821
2875
  }
2822
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
+
2823
2909
  // src/lib/capture.ts
2824
2910
  var HTML2CANVAS_TIMEOUT_MS = 1e4;
2825
2911
  var CHUNK_TIMEOUT_MS = 8e3;
@@ -3172,12 +3258,13 @@ async function captureRegion(rect, scroll, prefer = "auto") {
3172
3258
  if (blob) return { status: "ok", blob, engine: "exact" };
3173
3259
  }
3174
3260
  } catch (err) {
3175
- console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
3261
+ recordFault("screenshot/crop", err);
3176
3262
  }
3177
3263
  if (prefer === "exact") return { status: "failed" };
3178
3264
  }
3179
3265
  const weight = documentWeight();
3180
3266
  if (weight > TOO_HEAVY_NODES) {
3267
+ recordFault("screenshot/too-heavy", `page has ${weight} elements; the redraw engine was refused`);
3181
3268
  return { status: "too-heavy", nodes: weight };
3182
3269
  }
3183
3270
  try {
@@ -3186,14 +3273,14 @@ async function captureRegion(rect, scroll, prefer = "auto") {
3186
3273
  const blob = await encodeShot(canvas);
3187
3274
  return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
3188
3275
  } catch (err) {
3189
- console.warn("[QA] region capture failed, retrying without gradients/shadows:", err);
3276
+ recordFault("screenshot/render", err);
3190
3277
  try {
3191
3278
  const canvas = await captureViaDom(rect, sx, sy, true);
3192
3279
  if (!canvas) return { status: "failed" };
3193
3280
  const blob = await encodeShot(canvas);
3194
3281
  return blob ? { status: "ok", blob, engine: "dom" } : { status: "failed" };
3195
3282
  } catch (retryErr) {
3196
- console.warn("[QA] region capture failed after retry:", retryErr);
3283
+ recordFault("screenshot/render-retry", retryErr);
3197
3284
  return { status: "failed" };
3198
3285
  }
3199
3286
  }
@@ -3237,10 +3324,32 @@ function formatEvent(ev, t0) {
3237
3324
  }
3238
3325
  return `[${rel}] uncaught: ${oneLine(ev.message)}`;
3239
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
+ }
3240
3347
  function noteCheckLine(note, index) {
3241
3348
  const where = oneLine(note.route) || "/";
3242
- const what = oneLine(note.description) || "(no description)";
3243
- const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
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;
3244
3353
  return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
3245
3354
  }
3246
3355
  function noteToMarkdown(note, opts) {
@@ -3270,8 +3379,15 @@ function noteToMarkdown(note, opts) {
3270
3379
  );
3271
3380
  }
3272
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
+ }
3273
3386
  if (idx != null && note.screenshot) {
3274
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
+ }
3275
3391
  }
3276
3392
  if (idx != null && note.afterScreenshot) {
3277
3393
  lines.push(
@@ -3279,7 +3395,29 @@ function noteToMarkdown(note, opts) {
3279
3395
  );
3280
3396
  }
3281
3397
  lines.push("");
3282
- lines.push(oneLine(note.description) ? note.description.trim() : "_(no description)_");
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
+ }
3283
3421
  if (note.followUp && oneLine(note.followUp)) {
3284
3422
  lines.push("");
3285
3423
  lines.push(
@@ -3290,6 +3428,15 @@ function noteToMarkdown(note, opts) {
3290
3428
  lines.push("");
3291
3429
  lines.push(note.followUp.trim());
3292
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);
3293
3440
  const recordedSteps = note.context?.steps ?? [];
3294
3441
  if (recordedSteps.length) {
3295
3442
  const t0 = Date.parse(note.timestamp) || recordedSteps[recordedSteps.length - 1].t;
@@ -3300,14 +3447,6 @@ function noteToMarkdown(note, opts) {
3300
3447
  lines.push(`${i + 1}. ${formatStep(step, t0)}`);
3301
3448
  });
3302
3449
  }
3303
- if (idx != null) {
3304
- lines.push("");
3305
- lines.push(`**Check ${idx} \u2014 how this will be graded**`);
3306
- lines.push("");
3307
- 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?*");
3308
- lines.push("");
3309
- 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\`.`);
3310
- }
3311
3450
  const ctx = note.context;
3312
3451
  if (ctx) {
3313
3452
  const env = ctx.env;
@@ -3354,7 +3493,67 @@ function noteToMarkdown(note, opts) {
3354
3493
  lines.push("");
3355
3494
  lines.push("</details>");
3356
3495
  }
3357
- return lines.join("\n");
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");
3358
3557
  }
3359
3558
 
3360
3559
  // src/lib/exportZip.ts
@@ -3490,7 +3689,15 @@ This archive is not a list of suggestions. Every point in \`notes.md\` is an acc
3490
3689
 
3491
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.
3492
3691
 
3493
- 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.`
3494
3701
  );
3495
3702
  const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
3496
3703
  const runArr = toStrings(p.runCommands);
@@ -3630,7 +3837,14 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
3630
3837
  ""
3631
3838
  ].join("\n");
3632
3839
  const noteBlocks = notes.map(
3633
- (n, i) => noteToMarkdown(n, { brand: brandLabel, index: i + 1 })
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
+ })
3634
3848
  );
3635
3849
  const notesBody = noteBlocks.length > 0 ? `${noteBlocks.join("\n\n---\n\n")}
3636
3850
 
@@ -3671,6 +3885,43 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
3671
3885
  "Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
3672
3886
  ""
3673
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
+ }
3674
3925
  notes.forEach((n, i) => {
3675
3926
  if (n.screenshot && shots) {
3676
3927
  shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
@@ -4140,7 +4391,7 @@ function reportMarkdownText(allNotes) {
4140
4391
  "",
4141
4392
  "---",
4142
4393
  ""
4143
- ].filter((line) => line !== "").join("\n");
4394
+ ].filter((line2) => line2 !== "").join("\n");
4144
4395
  const body = ordered.map((n) => noteMarkdownForDisk(n, noteIndex[n.id])).join("\n---\n\n");
4145
4396
  return `${header}
4146
4397
  ${body}`;
@@ -4315,6 +4566,7 @@ function exactShotsWanted(store) {
4315
4566
  }
4316
4567
  var SIMPLE_MODE_KEY = "simpleMode";
4317
4568
  var COMPACT_KEY = "compactCapture";
4569
+ var DEV_MODE_KEY = "developerMode";
4318
4570
  var LAST_CAMPAIGN_KEY = "lastCampaign";
4319
4571
  var AUTO_BACKUP_KEY = "autoBackup";
4320
4572
  var AUTO_BACKUP_AT_KEY = "autoBackupAt";
@@ -4380,7 +4632,15 @@ function QaProvider({
4380
4632
  const [lang, setLangState] = useState(() => {
4381
4633
  const saved = storage.getItem(LANG_KEY);
4382
4634
  if (saved === "ar" || saved === "en") return saved;
4383
- return config.rtl ? "ar" : "en";
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";
4384
4644
  });
4385
4645
  const [guideChecked, setGuideChecked] = useState(
4386
4646
  () => new Set(storage.getJSON(GUIDE_KEY, []))
@@ -4418,6 +4678,9 @@ function QaProvider({
4418
4678
  const [simpleMode, setSimpleModeState] = useState(
4419
4679
  () => storage.getItem(SIMPLE_MODE_KEY) === "1"
4420
4680
  );
4681
+ const [developerMode, setDeveloperModeState] = useState(
4682
+ () => storage.getItem(DEV_MODE_KEY) === "1"
4683
+ );
4421
4684
  const [compactCapture, setCompactCaptureState] = useState(
4422
4685
  () => storage.getItem(COMPACT_KEY) === "1"
4423
4686
  );
@@ -4747,6 +5010,11 @@ function QaProvider({
4747
5010
  target: input.target ?? void 0,
4748
5011
  severity: input.severity,
4749
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,
4750
5018
  journeyRef,
4751
5019
  context
4752
5020
  };
@@ -5149,6 +5417,10 @@ function QaProvider({
5149
5417
  setCompactCaptureState(on);
5150
5418
  storage.setItem(COMPACT_KEY, on ? "1" : "0");
5151
5419
  }, [storage]);
5420
+ const setDeveloperMode = useCallback((on) => {
5421
+ setDeveloperModeState(on);
5422
+ storage.setItem(DEV_MODE_KEY, on ? "1" : "0");
5423
+ }, [storage]);
5152
5424
  const setFilter = useCallback((patch) => {
5153
5425
  setFilterState((prev) => ({ ...prev, ...patch }));
5154
5426
  }, []);
@@ -5488,6 +5760,7 @@ function QaProvider({
5488
5760
  // Config passthrough
5489
5761
  namespace: config.namespace,
5490
5762
  brand: config.brand,
5763
+ beta: config.beta === true,
5491
5764
  loginField: config.loginField,
5492
5765
  credentials: config.credentials,
5493
5766
  // Never the raw config value: an unconfigured project falls back to the
@@ -5596,6 +5869,8 @@ function QaProvider({
5596
5869
  simpleMode,
5597
5870
  setSimpleMode,
5598
5871
  compactCapture,
5872
+ developerMode,
5873
+ setDeveloperMode,
5599
5874
  setCompactCapture,
5600
5875
  exportZip: exportZipFn
5601
5876
  };
@@ -5663,6 +5938,16 @@ var ICONS = {
5663
5938
  Square: [
5664
5939
  ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }]
5665
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
+ ],
5666
5951
  ImagePlus: [
5667
5952
  ["path", { d: "M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7" }],
5668
5953
  ["line", { x1: "16", x2: "22", y1: "5", y2: "5" }],
@@ -5896,7 +6181,7 @@ function clampFabPos(p, w = FAB_SIZE_PX, h = FAB_SIZE_PX) {
5896
6181
  };
5897
6182
  }
5898
6183
  function QaFab() {
5899
- const { isOpen, setIsOpen, notes, captureActive, namespace, t } = useQa();
6184
+ const { isOpen, setIsOpen, notes, captureActive, namespace, beta, t } = useQa();
5900
6185
  const [pos, setPos] = useState(() => loadFabPos(namespace));
5901
6186
  const dragRef = useRef(null);
5902
6187
  const didDragRef = useRef(false);
@@ -6009,6 +6294,15 @@ function QaFab() {
6009
6294
  }
6010
6295
  ),
6011
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
+ ),
6012
6306
  !isOpen && notes.length > 0 && /* @__PURE__ */ jsx(
6013
6307
  "span",
6014
6308
  {
@@ -7702,6 +7996,154 @@ function NoteFilterBar() {
7702
7996
  ) })
7703
7997
  ] });
7704
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" ;
7705
8147
  function Section({
7706
8148
  icon,
7707
8149
  title,
@@ -7764,8 +8206,24 @@ function SettingsSheet({ onClose }) {
7764
8206
  setSimpleMode,
7765
8207
  compactCapture,
7766
8208
  setCompactCapture,
8209
+ developerMode,
8210
+ setDeveloperMode,
7767
8211
  notes
7768
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
+ }, []);
7769
8227
  const [project, setProject] = useState(lastCampaign.project);
7770
8228
  const [campaign2, setCampaign] = useState(lastCampaign.campaign || suggestCampaignName());
7771
8229
  const [tester, setTester] = useState(lastCampaign.tester);
@@ -8040,6 +8498,20 @@ function SettingsSheet({ onClose }) {
8040
8498
  ] }),
8041
8499
  /* @__PURE__ */ jsx("div", { className: "qa-h-px qa-bg-3 qa-mt-3 qa-mb-4" }),
8042
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
+ ] }),
8043
8515
  /* @__PURE__ */ jsxs("label", { className: "qa-flex qa-items-center qa-gap-2 qa-text-xs qa-text-hi", style: { cursor: "pointer" }, children: [
8044
8516
  /* @__PURE__ */ jsx(
8045
8517
  "input",
@@ -8062,6 +8534,91 @@ function SettingsSheet({ onClose }) {
8062
8534
  ),
8063
8535
  t("compact_mode")
8064
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}`)) })
8065
8622
  ] })
8066
8623
  ] })
8067
8624
  ]
@@ -8396,17 +8953,17 @@ function QaPanel() {
8396
8953
  if (typeof window === "undefined") return 0;
8397
8954
  const vv = window.visualViewport;
8398
8955
  if (!vv) return 0;
8399
- const overlap = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
8400
- if (overlap <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
8956
+ const overlap2 = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
8957
+ if (overlap2 <= KEYBOARD_OVERLAP_THRESHOLD) return 0;
8401
8958
  const panel = panelRef.current;
8402
8959
  if (!panel) return 0;
8403
8960
  const root2 = panel.getRootNode();
8404
8961
  const active = root2.activeElement;
8405
8962
  if (!active || !panel.contains(active)) return 0;
8406
8963
  const tag = active.tagName;
8407
- if (tag === "TEXTAREA") return Math.round(overlap) + KEYBOARD_LIFT_GAP;
8964
+ if (tag === "TEXTAREA") return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
8408
8965
  if (tag === "INPUT" && !NON_TEXT_INPUT_TYPES.has(active.type)) {
8409
- return Math.round(overlap) + KEYBOARD_LIFT_GAP;
8966
+ return Math.round(overlap2) + KEYBOARD_LIFT_GAP;
8410
8967
  }
8411
8968
  return 0;
8412
8969
  } catch {
@@ -9346,6 +9903,209 @@ function unlockPageScroll() {
9346
9903
  window.removeEventListener("touchmove", preventScroll, LISTENER_OPTIONS);
9347
9904
  }
9348
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
+ }
9349
10109
  var DRAG_THRESHOLD2 = 6;
9350
10110
  var TOUCH_DRAG_THRESHOLD = 12;
9351
10111
  var MIN_REGION_SIZE = 8;
@@ -9400,6 +10160,8 @@ function CaptureMode() {
9400
10160
  setCompactCapture,
9401
10161
  exactShots,
9402
10162
  photographNow,
10163
+ developerMode,
10164
+ notes,
9403
10165
  capturePrefill
9404
10166
  } = useQa();
9405
10167
  const coarse = useCoarsePointer();
@@ -9415,12 +10177,21 @@ function CaptureMode() {
9415
10177
  const [shot, setShot] = useState(null);
9416
10178
  const stillRef = useRef(null);
9417
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("");
9418
10185
  const [shotEngine, setShotEngine] = useState(null);
9419
10186
  const [shotUrl, setShotUrl] = useState(null);
9420
10187
  const [capturing, setCapturing] = useState(false);
9421
10188
  const [captureError, setCaptureError] = useState(false);
9422
10189
  const [tooHeavy, setTooHeavy] = useState(false);
9423
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
+ );
9424
10195
  const [severity, setSeverity] = useState("bug");
9425
10196
  const [targetForensics, setTargetForensics] = useState(void 0);
9426
10197
  const taRef = useRef(null);
@@ -9562,6 +10333,7 @@ function CaptureMode() {
9562
10333
  if (regionRect) {
9563
10334
  const sel2 = { kind: "region", rect: regionRect };
9564
10335
  setTargetForensics(void 0);
10336
+ setOrigin(void 0);
9565
10337
  if (coarse) {
9566
10338
  setCandidate(sel2);
9567
10339
  setPhase("confirming");
@@ -9584,6 +10356,7 @@ function CaptureMode() {
9584
10356
  tagName: el.tagName.toLowerCase()
9585
10357
  };
9586
10358
  setTargetForensics(collectTargetForensics(el));
10359
+ setOrigin(resolveOrigin(el));
9587
10360
  if (coarse) {
9588
10361
  setCandidate(sel);
9589
10362
  setHover({ rect: sel.rect, selector: sel.selector || "" });
@@ -9728,8 +10501,12 @@ function CaptureMode() {
9728
10501
  setShot(null);
9729
10502
  setShotEngine(null);
9730
10503
  setDescription("");
10504
+ setWanted("");
10505
+ setWhy("");
10506
+ setFixHint("");
9731
10507
  setSeverity("bug");
9732
10508
  setTargetForensics(void 0);
10509
+ setOrigin(void 0);
9733
10510
  setCaptureError(false);
9734
10511
  }, []);
9735
10512
  const save = async (keepGoing = false) => {
@@ -9749,9 +10526,14 @@ function CaptureMode() {
9749
10526
  };
9750
10527
  await addNote({
9751
10528
  description,
10529
+ wanted,
10530
+ why,
10531
+ fixHint,
9752
10532
  screenshot: shot ?? void 0,
10533
+ shotEngine: shotEngine ?? void 0,
9753
10534
  target,
9754
10535
  severity,
10536
+ origin,
9755
10537
  forensics: selection.kind === "element" ? targetForensics : void 0
9756
10538
  });
9757
10539
  if (keepGoing) {
@@ -10381,8 +11163,8 @@ function CaptureMode() {
10381
11163
  ]
10382
11164
  }
10383
11165
  ),
10384
- /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
10385
- /* @__PURE__ */ jsxs(
11166
+ developerMode && /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
11167
+ developerMode && /* @__PURE__ */ jsxs(
10386
11168
  "div",
10387
11169
  {
10388
11170
  role: "group",
@@ -10412,24 +11194,98 @@ function CaptureMode() {
10412
11194
  ]
10413
11195
  }
10414
11196
  ),
10415
- /* @__PURE__ */ jsx(
10416
- "textarea",
10417
- {
10418
- ref: taRef,
10419
- value: description,
10420
- onChange: (e) => setDescription(e.target.value),
10421
- onKeyDown: (e) => {
10422
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
10423
- if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
10424
- e.preventDefault();
10425
- setSeverity(SEVERITIES2[Number(e.key) - 1]);
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
10426
11212
  }
10427
- },
10428
- rows: 3,
10429
- placeholder: t("annotate_placeholder"),
10430
- 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"
10431
- }
10432
- ),
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
+ ] }),
10433
11289
  /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-2", children: [
10434
11290
  /* @__PURE__ */ jsxs(
10435
11291
  "button",
@@ -10471,8 +11327,7 @@ function CaptureMode() {
10471
11327
  ] }),
10472
11328
  /* @__PURE__ */ jsxs("p", { className: "qa-text-center qa-text-10 qa-text-slate-400", children: [
10473
11329
  t("save_hint"),
10474
- " \xB7 ",
10475
- t("severity_keys")
11330
+ developerMode ? ` \xB7 ${t("severity_keys")}` : ""
10476
11331
  ] })
10477
11332
  ] })
10478
11333
  ]
@@ -10678,5 +11533,5 @@ function Qapture({ config }) {
10678
11533
  }
10679
11534
 
10680
11535
  export { Qapture, deleteQaDatabase, initQaStudio };
10681
- //# sourceMappingURL=chunk-32LKZ2PG.js.map
10682
- //# sourceMappingURL=chunk-32LKZ2PG.js.map
11536
+ //# sourceMappingURL=chunk-B22X5Y6U.js.map
11537
+ //# sourceMappingURL=chunk-B22X5Y6U.js.map