qapture2 0.7.9 → 0.8.2

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.
@@ -58,10 +58,10 @@ function openDB(dbName) {
58
58
  dbCache.set(dbName, promise);
59
59
  return promise;
60
60
  }
61
- function run(dbName, store, mode2, fn) {
61
+ function run(dbName, store, mode, fn) {
62
62
  return openDB(dbName).then(
63
63
  (db) => new Promise((resolve, reject) => {
64
- const tx = db.transaction(store, mode2);
64
+ const tx = db.transaction(store, mode);
65
65
  const s = tx.objectStore(store);
66
66
  let result;
67
67
  const req = fn(s);
@@ -516,6 +516,7 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
516
516
  /* Z-index scale */
517
517
  --qa-z-fab: 9990;
518
518
  --qa-z-panel: 9995;
519
+ --qa-z-capture-still: 10089;
519
520
  --qa-z-capture-dim: 10090;
520
521
  --qa-z-capture-highlight: 10092;
521
522
  --qa-z-capture-region-move: 10093;
@@ -565,6 +566,8 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
565
566
  .qa-z-1 { z-index: 1; }
566
567
  .qa-z-50 { z-index: 50; }
567
568
  .qa-z-100 { z-index: 100; }
569
+ /* the frozen still, directly beneath the dim so the scrim tints it too */
570
+ .qa-z-10089 { z-index: var(--qa-z-capture-still); }
568
571
  .qa-z-10090 { z-index: var(--qa-z-capture-dim); }
569
572
  .qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
570
573
  /* region-handle layering */
@@ -1729,11 +1732,15 @@ var STR = {
1729
1732
  sel_region: "Region selected",
1730
1733
  sel_element: "Element selected",
1731
1734
  capturing: "capturing screenshot\u2026",
1735
+ 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.",
1732
1736
  no_shot: "no screenshot (location saved)",
1733
1737
  annotate_placeholder: "What do you want to do here? (add / remove / change\u2026)",
1734
1738
  save_point: "Save point",
1739
+ save_next: "Save + next",
1740
+ save_next_hint: "Save this one and mark up another part of the same screenshot \u2014 no second permission prompt.",
1741
+ shot_reused: "{n} saved from this screenshot",
1735
1742
  reselect: "Reselect",
1736
- save_hint: "\u2318/Ctrl + Enter to save",
1743
+ save_hint: "\u2318/Ctrl + Enter to save \xB7 add Shift to keep the same screenshot",
1737
1744
  login_with: "Log in with {field} + password.",
1738
1745
  used_count: "{n}/{m} used",
1739
1746
  used: "used",
@@ -1769,6 +1776,9 @@ var STR = {
1769
1776
  sev_design: "Design",
1770
1777
  sev_enhance: "Enhance",
1771
1778
  status_open: "Open",
1779
+ status_sent: "Sent",
1780
+ check_fixes: "Check the {n} fixes",
1781
+ check_fixes_hint: "Walk back through everything the last export sent, one spot at a time, and say whether each is now what you asked for.",
1772
1782
  status_verified: "Verified",
1773
1783
  context_attached: "{n} runtime events attached",
1774
1784
  start_walkthrough: "Start walkthrough",
@@ -1783,7 +1793,7 @@ var STR = {
1783
1793
  no_evidence: "ticked, no capture",
1784
1794
  expected_label: "Expected",
1785
1795
  exact_label: "Pixel-exact shots",
1786
- exact_hint: "Screenshots become a real photo instead of a redraw. Asks once to share this tab (or, in Safari and Firefox, this window); nothing leaves your device.",
1796
+ exact_hint: "Screenshots become a real photo instead of a redraw. Each capture asks to share this tab (or, in Safari and Firefox, this window), takes one frame, and hands the screen straight back \u2014 nothing keeps recording, and nothing leaves your device.",
1787
1797
  redraw_notice: "This is a redraw of the page, not a real photo.",
1788
1798
  redraw_action: "Use real screenshots",
1789
1799
  redraw_no_engine: "This browser cannot photograph the screen at all, so charts and canvases may come out wrong. Open the app on a desktop browser for true screenshots.",
@@ -1792,7 +1802,7 @@ var STR = {
1792
1802
  exact_off: "Back to redrawn screenshots",
1793
1803
  exact_declined: "Staying on redrawn screenshots",
1794
1804
  exact_turn_on: "Turn on",
1795
- exact_unsupported: "Needs Chrome, Edge or Brave on desktop",
1805
+ exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
1796
1806
  sync_title: "Save to a folder",
1797
1807
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1798
1808
  sync_choose: "Choose folder",
@@ -1937,11 +1947,15 @@ var STR = {
1937
1947
  sel_region: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0645\u0646\u0637\u0642\u0629",
1938
1948
  sel_element: "\u062A\u0645 \u062A\u062D\u062F\u064A\u062F \u0639\u0646\u0635\u0631",
1939
1949
  capturing: "\u064A\u062A\u0645 \u0627\u0644\u062A\u0642\u0627\u0637 \u0627\u0644\u0635\u0648\u0631\u0629\u2026",
1950
+ 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.",
1940
1951
  no_shot: "\u0628\u062F\u0648\u0646 \u0635\u0648\u0631\u0629 (\u062A\u0645 \u062D\u0641\u0638 \u0627\u0644\u0645\u0648\u0642\u0639)",
1941
1952
  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)",
1942
1953
  save_point: "\u062D\u0641\u0638 \u0627\u0644\u0646\u0642\u0637\u0629",
1954
+ save_next: "\u062D\u0641\u0638 + \u0627\u0644\u062A\u0627\u0644\u064A",
1955
+ 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.",
1956
+ shot_reused: "\u062D\u064F\u0641\u0638\u062A {n} \u0645\u0646 \u0647\u0630\u0647 \u0627\u0644\u0644\u0642\u0637\u0629",
1943
1957
  reselect: "\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u062A\u062D\u062F\u064A\u062F",
1944
- save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638",
1958
+ save_hint: "\u2318/Ctrl + Enter \u0644\u0644\u062D\u0641\u0638 \xB7 \u0623\u0636\u0641 Shift \u0644\u0644\u0628\u0642\u0627\u0621 \u0639\u0644\u0649 \u0646\u0641\u0633 \u0627\u0644\u0644\u0642\u0637\u0629",
1945
1959
  login_with: "\u0633\u062C\u0651\u0644 \u0627\u0644\u062F\u062E\u0648\u0644 \u0628\u0640 {field} + \u0643\u0644\u0645\u0629 \u0627\u0644\u0645\u0631\u0648\u0631.",
1946
1960
  used_count: "{n}/{m} \u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
1947
1961
  used: "\u0645\u064F\u0633\u062A\u062E\u062F\u064E\u0645",
@@ -1977,6 +1991,9 @@ var STR = {
1977
1991
  sev_design: "Design",
1978
1992
  sev_enhance: "Enhance",
1979
1993
  status_open: "\u0645\u0641\u062A\u0648\u062D",
1994
+ status_sent: "\u0645\u064F\u0631\u0633\u064E\u0644",
1995
+ check_fixes: "\u062A\u062D\u0642\u0651\u0642 \u0645\u0646 {n} \u062A\u0639\u062F\u064A\u0644\u0627\u062A",
1996
+ check_fixes_hint: "\u0645\u0631\u0651 \u0639\u0644\u0649 \u0643\u0644 \u0645\u0627 \u0623\u0631\u0633\u0644\u0647 \u0627\u0644\u062A\u0635\u062F\u064A\u0631 \u0627\u0644\u0623\u062E\u064A\u0631\u060C \u0646\u0642\u0637\u0629 \u0646\u0642\u0637\u0629\u060C \u0648\u0642\u0644 \u0647\u0644 \u0635\u0627\u0631 \u0643\u0645\u0627 \u0637\u0644\u0628\u062A \u0623\u0645 \u0644\u0627.",
1980
1997
  status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
1981
1998
  context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
1982
1999
  start_walkthrough: "\u0627\u0628\u062F\u0623 \u0627\u0644\u062C\u0648\u0644\u0629",
@@ -1991,7 +2008,7 @@ var STR = {
1991
2008
  no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
1992
2009
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1993
2010
  exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
1994
- exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \u0628\u062F\u0644 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0633\u0645. \u064A\u064F\u0637\u0644\u0628 \u0627\u0644\u0625\u0630\u0646 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629 \u0644\u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u062A\u0628\u0648\u064A\u0628 (\u0623\u0648 \u0627\u0644\u0646\u0627\u0641\u0630\u0629 \u0641\u064A Safari \u0648Firefox)\u060C \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
2011
+ exact_hint: "\u062A\u0635\u0628\u062D \u0627\u0644\u0644\u0642\u0637\u0629 \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629 \u0628\u062F\u0644 \u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0631\u0633\u0645. \u062A\u0637\u0644\u0628 \u0643\u0644 \u0644\u0642\u0637\u0629 \u0625\u0630\u0646 \u0645\u0634\u0627\u0631\u0643\u0629 \u0627\u0644\u062A\u0628\u0648\u064A\u0628 (\u0623\u0648 \u0627\u0644\u0646\u0627\u0641\u0630\u0629 \u0641\u064A Safari \u0648Firefox)\u060C \u062A\u0623\u062E\u0630 \u0625\u0637\u0627\u0631\u064B\u0627 \u0648\u0627\u062D\u062F\u064B\u0627\u060C \u062B\u0645 \u062A\u0639\u064A\u062F \u0627\u0644\u0634\u0627\u0634\u0629 \u0641\u0648\u0631\u064B\u0627 \u2014 \u0644\u0627 \u062A\u0633\u062C\u064A\u0644 \u0645\u0633\u062A\u0645\u0631\u060C \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
1995
2012
  redraw_notice: "\u0647\u0630\u0647 \u0625\u0639\u0627\u062F\u0629 \u0631\u0633\u0645 \u0644\u0644\u0635\u0641\u062D\u0629\u060C \u0648\u0644\u064A\u0633\u062A \u0635\u0648\u0631\u0629 \u062D\u0642\u064A\u0642\u064A\u0629.",
1996
2013
  redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1997
2014
  redraw_no_engine: "\u0644\u0627 \u064A\u0633\u062A\u0637\u064A\u0639 \u0647\u0630\u0627 \u0627\u0644\u0645\u062A\u0635\u0641\u062D \u062A\u0635\u0648\u064A\u0631 \u0627\u0644\u0634\u0627\u0634\u0629 \u0625\u0637\u0644\u0627\u0642\u064B\u0627\u060C \u0644\u0630\u0627 \u0642\u062F \u062A\u0638\u0647\u0631 \u0627\u0644\u0645\u062E\u0637\u0637\u0627\u062A \u0648\u0627\u0644\u0644\u0648\u062D\u0627\u062A \u0628\u0634\u0643\u0644 \u062E\u0627\u0637\u0626. \u0627\u0641\u062A\u062D \u0627\u0644\u062A\u0637\u0628\u064A\u0642 \u0639\u0644\u0649 \u0645\u062A\u0635\u0641\u062D \u0633\u0637\u062D \u0645\u0643\u062A\u0628 \u0644\u0644\u062D\u0635\u0648\u0644 \u0639\u0644\u0649 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629.",
@@ -2000,7 +2017,7 @@ var STR = {
2000
2017
  exact_off: "\u0627\u0644\u0639\u0648\u062F\u0629 \u0625\u0644\u0649 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627",
2001
2018
  exact_declined: "\u0633\u0646\u0628\u0642\u0649 \u0639\u0644\u0649 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u064F\u0639\u0627\u062F \u0631\u0633\u0645\u0647\u0627",
2002
2019
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
2003
- exact_unsupported: "\u064A\u062A\u0637\u0644\u0628 Chrome \u0623\u0648 Edge \u0623\u0648 Brave \u0639\u0644\u0649 \u0633\u0637\u062D \u0627\u0644\u0645\u0643\u062A\u0628",
2020
+ exact_unsupported: "\u064A\u062A\u0637\u0644\u0628 \u0645\u062A\u0635\u0641\u062D \u0633\u0637\u062D \u0645\u0643\u062A\u0628 \u2014 \u0627\u0644\u0647\u0648\u0627\u062A\u0641 \u0644\u0627 \u062A\u0633\u062A\u0637\u064A\u0639 \u062A\u0635\u0648\u064A\u0631 \u0627\u0644\u0634\u0627\u0634\u0629",
2004
2021
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
2005
2022
  sync_hint: "\u062A\u064F\u0643\u062A\u0628 \u0643\u0644 \u0645\u0644\u0627\u062D\u0638\u0629 \u0639\u0644\u0649 \u0627\u0644\u0642\u0631\u0635 \u0644\u062D\u0638\u0629 \u062D\u0641\u0638\u0647\u0627 \u2014 \u0644\u0627 \u0634\u064A\u0621 \u064A\u0636\u064A\u0639 \u0625\u0630\u0627 \u062A\u0639\u0637\u0651\u0644 \u0627\u0644\u0645\u062A\u0635\u0641\u062D.",
2006
2023
  sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
@@ -2305,7 +2322,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2305
2322
  if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
2306
2323
  const originX = tl.x - centres.tl.x * scaleX;
2307
2324
  const originY = tl.y - centres.tl.y * scaleY;
2308
- const mapping2 = { scaleX, scaleY, originX, originY };
2325
+ const mapping = { scaleX, scaleY, originX, originY };
2309
2326
  const farX = originX + vw * scaleX;
2310
2327
  const farY = originY + vh * scaleY;
2311
2328
  const slack = Math.max(4, frameW * 0.01);
@@ -2324,7 +2341,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2324
2341
  verified++;
2325
2342
  }
2326
2343
  if (verified === 0) return null;
2327
- return mapping2;
2344
+ return mapping;
2328
2345
  }
2329
2346
  function mapRectToFrame(rect, m, frameW, frameH) {
2330
2347
  const x0 = m.originX + rect.left * m.scaleX;
@@ -2336,73 +2353,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2336
2353
  if (sw < 1 || sh < 1) return null;
2337
2354
  return { sx, sy, sw, sh };
2338
2355
  }
2339
- function environmentSignature() {
2340
- if (typeof window === "undefined") return "";
2341
- return [
2342
- window.innerWidth,
2343
- window.innerHeight,
2344
- window.outerWidth,
2345
- window.outerHeight,
2346
- window.screenX,
2347
- window.screenY,
2348
- Math.round((window.devicePixelRatio || 1) * 100)
2349
- ].join("x");
2350
- }
2351
2356
 
2352
2357
  // src/lib/screenCapture.ts
2353
2358
  var ASPECT_TOLERANCE = 0.08;
2354
2359
  var CALIBRATION_SETTLE_MS = 220;
2355
2360
  var FRESH_FRAME_TIMEOUT_MS = 500;
2356
2361
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2357
- var stream = null;
2358
- var track = null;
2359
- var video = null;
2360
- var grabber = null;
2361
- var declined = false;
2362
2362
  function isExactCaptureSupported() {
2363
2363
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2364
2364
  const md = navigator.mediaDevices;
2365
2365
  return !!md && typeof md.getDisplayMedia === "function";
2366
2366
  }
2367
- var mode = "tab";
2368
- var mapping = null;
2369
- var mappingSignature = "";
2367
+ var armed = false;
2368
+ var declined = false;
2369
+ var frozen = null;
2370
+ var lastMode = null;
2371
+ function armExactCapture() {
2372
+ if (!isExactCaptureSupported()) return false;
2373
+ armed = true;
2374
+ declined = false;
2375
+ return true;
2376
+ }
2377
+ function disarmExactCapture() {
2378
+ armed = false;
2379
+ releaseFrozenFrame();
2380
+ }
2381
+ function resetExactCaptureDecline() {
2382
+ declined = false;
2383
+ }
2370
2384
  function getExactCaptureStatus() {
2371
2385
  if (!isExactCaptureSupported()) return "unsupported";
2372
- if (track && track.readyState === "live") return "live";
2386
+ if (armed) return "live";
2373
2387
  if (declined) return "declined";
2374
2388
  return "idle";
2375
2389
  }
2376
- function teardown() {
2377
- if (video) {
2378
- try {
2379
- video.pause();
2380
- } catch {
2381
- }
2382
- video.srcObject = null;
2383
- video.remove();
2384
- }
2385
- if (stream) {
2386
- for (const t of stream.getTracks()) {
2387
- try {
2388
- t.stop();
2389
- } catch {
2390
- }
2391
- }
2392
- }
2393
- video = null;
2394
- stream = null;
2395
- track = null;
2396
- grabber = null;
2397
- mode = "tab";
2398
- mapping = null;
2399
- mappingSignature = "";
2400
- }
2401
- function stopExactCapture() {
2402
- teardown();
2390
+ function getFrozenFrame() {
2391
+ return frozen;
2403
2392
  }
2404
- function resetExactCaptureDecline() {
2405
- declined = false;
2393
+ function releaseFrozenFrame() {
2394
+ if (frozen) {
2395
+ frozen.canvas.width = 0;
2396
+ frozen.canvas.height = 0;
2397
+ }
2398
+ frozen = null;
2406
2399
  }
2407
2400
  function waitFor(test, timeoutMs) {
2408
2401
  if (test()) return Promise.resolve(true);
@@ -2442,17 +2435,28 @@ function nextPresentedFrame(el) {
2442
2435
  });
2443
2436
  });
2444
2437
  }
2445
- function validateFrameMapping(frameW, frameH) {
2438
+ function looksLikeViewport(frameW, frameH) {
2446
2439
  if (!frameW || !frameH) return false;
2447
2440
  const vw = window.innerWidth;
2448
2441
  const vh = window.innerHeight;
2449
2442
  if (!vw || !vh) return false;
2450
- const frameAspect = frameW / frameH;
2451
- const viewAspect = vw / vh;
2452
- return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
2443
+ return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
2453
2444
  }
2454
- async function grabFrameCanvas() {
2455
- if (!video) return null;
2445
+ async function withOverlayHidden(fn) {
2446
+ const hosts = Array.from(
2447
+ document.querySelectorAll("body > [data-qa-overlay]")
2448
+ );
2449
+ const previous = hosts.map((el) => el.style.visibility);
2450
+ for (const el of hosts) el.style.visibility = "hidden";
2451
+ try {
2452
+ return await fn();
2453
+ } finally {
2454
+ hosts.forEach((el, i) => {
2455
+ el.style.visibility = previous[i];
2456
+ });
2457
+ }
2458
+ }
2459
+ async function grabFrameCanvas(video, grabber) {
2456
2460
  await nextPresentedFrame(video);
2457
2461
  let source = video;
2458
2462
  let w = video.videoWidth;
@@ -2483,11 +2487,10 @@ async function grabFrameCanvas() {
2483
2487
  bitmap?.close?.();
2484
2488
  return c;
2485
2489
  }
2486
- async function calibrate() {
2487
- if (!video) return false;
2490
+ async function calibrate(video, grabber) {
2488
2491
  const vw = window.innerWidth;
2489
2492
  const vh = window.innerHeight;
2490
- if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
2493
+ if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
2491
2494
  const card = document.createElement("div");
2492
2495
  card.setAttribute("data-qa-calibration", "true");
2493
2496
  card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
@@ -2501,10 +2504,10 @@ async function calibrate() {
2501
2504
  document.body.appendChild(card);
2502
2505
  try {
2503
2506
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2504
- const frame = await grabFrameCanvas();
2505
- if (!frame) return false;
2507
+ const frame = await grabFrameCanvas(video, grabber);
2508
+ if (!frame) return null;
2506
2509
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2507
- if (!ctx) return false;
2510
+ if (!ctx) return null;
2508
2511
  const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
2509
2512
  const found = deriveMapping(
2510
2513
  findMarkerCandidates(data, frame.width, frame.height),
@@ -2513,20 +2516,40 @@ async function calibrate() {
2513
2516
  frame.width,
2514
2517
  frame.height
2515
2518
  );
2516
- if (!found) return false;
2517
- mapping = found;
2518
- mappingSignature = environmentSignature();
2519
- return true;
2519
+ frame.width = 0;
2520
+ frame.height = 0;
2521
+ return found;
2520
2522
  } catch {
2521
- return false;
2523
+ return null;
2522
2524
  } finally {
2523
2525
  card.remove();
2524
2526
  }
2525
2527
  }
2526
- async function startExactCapture() {
2527
- if (!isExactCaptureSupported()) return false;
2528
- if (track && track.readyState === "live" && video) return true;
2529
- if (stream) teardown();
2528
+ async function freezeViewport() {
2529
+ if (!isExactCaptureSupported()) return null;
2530
+ releaseFrozenFrame();
2531
+ let stream = null;
2532
+ let el = null;
2533
+ const release = () => {
2534
+ if (el) {
2535
+ try {
2536
+ el.pause();
2537
+ } catch {
2538
+ }
2539
+ el.srcObject = null;
2540
+ el.remove();
2541
+ el = null;
2542
+ }
2543
+ if (stream) {
2544
+ for (const t of stream.getTracks()) {
2545
+ try {
2546
+ t.stop();
2547
+ } catch {
2548
+ }
2549
+ }
2550
+ stream = null;
2551
+ }
2552
+ };
2530
2553
  try {
2531
2554
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
2532
2555
  const constraints = {
@@ -2544,121 +2567,103 @@ async function startExactCapture() {
2544
2567
  surfaceSwitching: "exclude",
2545
2568
  systemAudio: "exclude"
2546
2569
  };
2547
- const s = await navigator.mediaDevices.getDisplayMedia(constraints);
2548
- const t = s.getVideoTracks()[0];
2549
- if (!t) {
2550
- for (const x of s.getTracks()) x.stop();
2570
+ stream = await navigator.mediaDevices.getDisplayMedia(constraints);
2571
+ const track = stream.getVideoTracks()[0];
2572
+ if (!track) {
2551
2573
  declined = true;
2552
- return false;
2574
+ return null;
2553
2575
  }
2554
- const settings = t.getSettings();
2576
+ const settings = track.getSettings();
2555
2577
  const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
2556
- const el = document.createElement("video");
2578
+ el = document.createElement("video");
2557
2579
  el.setAttribute("data-qa-overlay", "true");
2558
2580
  el.muted = true;
2559
2581
  el.playsInline = true;
2560
2582
  el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
2561
- el.srcObject = s;
2583
+ el.srcObject = stream;
2562
2584
  document.body.appendChild(el);
2563
2585
  try {
2564
2586
  await el.play();
2565
2587
  } catch {
2566
2588
  }
2567
- const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
2568
- const giveUp = () => {
2569
- el.remove();
2570
- for (const x of s.getTracks()) x.stop();
2589
+ const video = el;
2590
+ const ready = await waitFor(
2591
+ () => video.videoWidth > 0 && video.videoHeight > 0,
2592
+ VIDEO_READY_TIMEOUT_MS
2593
+ );
2594
+ if (!ready) {
2571
2595
  declined = true;
2572
- return false;
2573
- };
2574
- if (!ready) return giveUp();
2575
- const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
2576
- stream = s;
2577
- track = t;
2578
- video = el;
2596
+ return null;
2597
+ }
2579
2598
  const IC = window.ImageCapture;
2580
- grabber = IC ? new IC(t) : null;
2581
- if (isDirect) {
2582
- mode = "tab";
2583
- mapping = null;
2584
- mappingSignature = "";
2585
- } else {
2586
- mode = "surface";
2587
- if (!await calibrate()) {
2588
- teardown();
2589
- declined = true;
2590
- return false;
2591
- }
2599
+ const grabber = IC ? new IC(track) : null;
2600
+ const vw = window.innerWidth;
2601
+ const vh = window.innerHeight;
2602
+ const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2603
+ const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
2604
+ if (mode === "surface" && !mapping) {
2605
+ declined = true;
2606
+ return null;
2592
2607
  }
2593
- t.addEventListener("ended", () => {
2594
- teardown();
2595
- });
2596
- return true;
2608
+ const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2609
+ if (!raw) return null;
2610
+ let page;
2611
+ if (mode === "surface" && mapping) {
2612
+ const box = mapRectToFrame(
2613
+ { left: 0, top: 0, width: vw, height: vh },
2614
+ mapping,
2615
+ raw.width,
2616
+ raw.height
2617
+ );
2618
+ if (!box) return null;
2619
+ page = document.createElement("canvas");
2620
+ page.width = box.sw;
2621
+ page.height = box.sh;
2622
+ const ctx = page.getContext("2d");
2623
+ if (!ctx) return null;
2624
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2625
+ raw.width = 0;
2626
+ raw.height = 0;
2627
+ } else {
2628
+ page = raw;
2629
+ }
2630
+ frozen = {
2631
+ canvas: page,
2632
+ mode,
2633
+ viewportWidth: vw,
2634
+ viewportHeight: vh,
2635
+ takenAt: Date.now()
2636
+ };
2637
+ lastMode = mode;
2638
+ return frozen;
2597
2639
  } catch {
2598
2640
  declined = true;
2599
- teardown();
2600
- return false;
2641
+ return null;
2642
+ } finally {
2643
+ release();
2601
2644
  }
2602
2645
  }
2603
- async function grabExactRegion(rect) {
2604
- if (!track || track.readyState !== "live" || !video) return null;
2605
- if (mode === "surface") {
2606
- if (!mapping || environmentSignature() !== mappingSignature) {
2607
- if (!await calibrate()) return null;
2608
- }
2609
- }
2610
- await nextPresentedFrame(video);
2611
- let source;
2612
- let frameW;
2613
- let frameH;
2614
- let bitmap = null;
2615
- if (grabber) {
2616
- try {
2617
- bitmap = await grabber.grabFrame();
2618
- source = bitmap;
2619
- frameW = bitmap.width;
2620
- frameH = bitmap.height;
2621
- } catch {
2622
- source = video;
2623
- frameW = video.videoWidth;
2624
- frameH = video.videoHeight;
2625
- }
2626
- } else {
2627
- source = video;
2628
- frameW = video.videoWidth;
2629
- frameH = video.videoHeight;
2630
- }
2631
- try {
2632
- let sx;
2633
- let sy;
2634
- let sw;
2635
- let sh;
2636
- if (mode === "surface") {
2637
- if (!mapping) return null;
2638
- const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
2639
- if (!mapped) return null;
2640
- ({ sx, sy, sw, sh } = mapped);
2641
- } else {
2642
- if (!validateFrameMapping(frameW, frameH)) return null;
2643
- const scaleX = frameW / window.innerWidth;
2644
- const scaleY = frameH / window.innerHeight;
2645
- sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
2646
- sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
2647
- sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
2648
- sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
2649
- }
2650
- const canvas = document.createElement("canvas");
2651
- canvas.width = sw;
2652
- canvas.height = sh;
2653
- const ctx = canvas.getContext("2d");
2654
- if (!ctx) return null;
2655
- ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
2656
- return canvas;
2657
- } catch {
2646
+ function cropFrozenRegion(rect) {
2647
+ const f = frozen;
2648
+ if (!f || !f.canvas.width || !f.canvas.height) return null;
2649
+ if (!f.viewportWidth || !f.viewportHeight) return null;
2650
+ if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
2658
2651
  return null;
2659
- } finally {
2660
- if (bitmap && typeof bitmap.close === "function") bitmap.close();
2661
2652
  }
2653
+ const scaleX = f.canvas.width / f.viewportWidth;
2654
+ const scaleY = f.canvas.height / f.viewportHeight;
2655
+ const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
2656
+ const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
2657
+ const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
2658
+ const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
2659
+ if (sw < 1 || sh < 1) return null;
2660
+ const out = document.createElement("canvas");
2661
+ out.width = sw;
2662
+ out.height = sh;
2663
+ const ctx = out.getContext("2d");
2664
+ if (!ctx) return null;
2665
+ ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
2666
+ return out;
2662
2667
  }
2663
2668
 
2664
2669
  // src/lib/cssColors.ts
@@ -2802,12 +2807,21 @@ function neutralizeDocumentColors(doc, aggressive = false) {
2802
2807
 
2803
2808
  // src/lib/capture.ts
2804
2809
  var HTML2CANVAS_TIMEOUT_MS = 1e4;
2810
+ var CHUNK_TIMEOUT_MS = 8e3;
2805
2811
  var FALLBACK_PAGE_BACKGROUND = "#ffffff";
2806
2812
  var MAX_SHOT_EDGE = 1800;
2807
2813
  var WEBP_QUALITY = 0.92;
2808
2814
  var STUCK_ATTR = "data-qa-stuck";
2809
2815
  var MAX_CAPTURE_EDGE = 4e3;
2810
2816
  var MAX_RENDER_DEVICE_PIXELS = 16e6;
2817
+ var TOO_HEAVY_NODES = 6e3;
2818
+ function documentWeight() {
2819
+ try {
2820
+ return document.getElementsByTagName("*").length;
2821
+ } catch {
2822
+ return 0;
2823
+ }
2824
+ }
2811
2825
  function withTimeout(promise, ms) {
2812
2826
  let timer;
2813
2827
  return Promise.race([
@@ -2869,8 +2883,13 @@ function fitToBudget(canvas) {
2869
2883
  function canvasToBlob(canvas, type, quality) {
2870
2884
  return new Promise((resolve) => {
2871
2885
  if (canvas.toBlob) {
2872
- canvas.toBlob((b) => resolve(b), type, quality);
2873
- return;
2886
+ try {
2887
+ canvas.toBlob((b) => resolve(b), type, quality);
2888
+ return;
2889
+ } catch {
2890
+ resolve(null);
2891
+ return;
2892
+ }
2874
2893
  }
2875
2894
  try {
2876
2895
  const dataUrl = canvas.toDataURL(type, quality);
@@ -2897,20 +2916,6 @@ function shotExtension(blob) {
2897
2916
  if (type === "image/jpeg") return "jpg";
2898
2917
  return "png";
2899
2918
  }
2900
- async function withOverlayHidden(fn) {
2901
- const hosts = Array.from(
2902
- document.querySelectorAll("body > [data-qa-overlay]")
2903
- );
2904
- const previous = hosts.map((el) => el.style.visibility);
2905
- for (const el of hosts) el.style.visibility = "hidden";
2906
- try {
2907
- return await fn();
2908
- } finally {
2909
- hosts.forEach((el, i) => {
2910
- el.style.visibility = previous[i];
2911
- });
2912
- }
2913
- }
2914
2919
  function markStuckElements() {
2915
2920
  let all;
2916
2921
  try {
@@ -3071,7 +3076,9 @@ function planRender(rect, sx, sy, vw, vh) {
3071
3076
  };
3072
3077
  }
3073
3078
  async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
3074
- const { default: html2canvas } = await import('html2canvas');
3079
+ const mod = await withTimeout(import('html2canvas'), CHUNK_TIMEOUT_MS);
3080
+ if (!mod) return null;
3081
+ const { default: html2canvas } = mod;
3075
3082
  const { vw, vh } = viewportSize();
3076
3083
  const plan = planRender(rect, sx, sy, vw, vh);
3077
3084
  let scale = Math.min(window.devicePixelRatio || 1, 2);
@@ -3090,7 +3097,22 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
3090
3097
  height: plan.render.height,
3091
3098
  scale,
3092
3099
  useCORS: true,
3093
- allowTaint: true,
3100
+ // MUST stay false. `allowTaint: true` tells html2canvas to draw
3101
+ // cross-origin images it could not fetch with CORS -- which succeeds,
3102
+ // and TAINTS the canvas. A tainted canvas throws SecurityError from
3103
+ // toBlob/toDataURL, so the render finishes and then the encode dies,
3104
+ // and the tester gets "couldn't capture" with nothing to look at.
3105
+ //
3106
+ // That is one image away on any real page: a map tile, a news
3107
+ // thumbnail, an avatar, an ad, a CDN logo. It made image-heavy pages
3108
+ // fail 100% of the time while a plain page worked, which is exactly
3109
+ // the "it works for you and not for me" this kept producing.
3110
+ //
3111
+ // False is strictly better: html2canvas SKIPS the images it cannot
3112
+ // read, so those come out blank and everything else comes out right.
3113
+ // A screenshot with two grey rectangles in it is worth having. A
3114
+ // SecurityError is not.
3115
+ allowTaint: false,
3094
3116
  // The page's own background, never null — see resolvePageBackground().
3095
3117
  // Passed straight to html2canvas's parser, so it gets the same
3096
3118
  // CSS Color 4 treatment the document walk applies.
@@ -3125,20 +3147,24 @@ async function captureRegion(rect, scroll, prefer = "auto") {
3125
3147
  if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
3126
3148
  const sx = scroll?.x ?? window.scrollX;
3127
3149
  const sy = scroll?.y ?? window.scrollY;
3128
- if (prefer !== "dom" && getExactCaptureStatus() === "live") {
3150
+ if (prefer !== "dom" && getFrozenFrame()) {
3129
3151
  const visible = intersectViewport(rect);
3130
3152
  if (!visible) return { status: "empty" };
3131
3153
  try {
3132
- const canvas = await withOverlayHidden(() => grabExactRegion(visible));
3154
+ const canvas = cropFrozenRegion(visible);
3133
3155
  if (canvas) {
3134
3156
  const blob = await encodeShot(canvas);
3135
3157
  if (blob) return { status: "ok", blob, engine: "exact" };
3136
3158
  }
3137
3159
  } catch (err) {
3138
- console.warn("[QA] exact capture failed, falling back to DOM render:", err);
3160
+ console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
3139
3161
  }
3140
3162
  if (prefer === "exact") return { status: "failed" };
3141
3163
  }
3164
+ const weight = documentWeight();
3165
+ if (weight > TOO_HEAVY_NODES) {
3166
+ return { status: "too-heavy", nodes: weight };
3167
+ }
3142
3168
  try {
3143
3169
  const canvas = await captureViaDom(rect, sx, sy);
3144
3170
  if (!canvas) return { status: "failed" };
@@ -3196,6 +3222,12 @@ function formatEvent(ev, t0) {
3196
3222
  }
3197
3223
  return `[${rel}] uncaught: ${oneLine(ev.message)}`;
3198
3224
  }
3225
+ function noteCheckLine(note, index) {
3226
+ const where = oneLine(note.route) || "/";
3227
+ const what = oneLine(note.description) || "(no description)";
3228
+ const trimmed = what.length > 160 ? `${what.slice(0, 157)}...` : what;
3229
+ return `- [ ] **check-${index}** (\`${where}\`) \u2014 ${trimmed}`;
3230
+ }
3199
3231
  function noteToMarkdown(note, opts) {
3200
3232
  const brand = opts?.brand ?? "Qapture";
3201
3233
  const idx = opts?.index;
@@ -3253,6 +3285,14 @@ function noteToMarkdown(note, opts) {
3253
3285
  lines.push(`${i + 1}. ${formatStep(step, t0)}`);
3254
3286
  });
3255
3287
  }
3288
+ if (idx != null) {
3289
+ lines.push("");
3290
+ lines.push(`**Check ${idx} \u2014 how this will be graded**`);
3291
+ lines.push("");
3292
+ lines.push(`The tester will be taken back to \`${oneLine(note.route) || "/"}\`` + (target?.selector ? `, shown \`${oneLine(target.selector)}\`` : ", shown this region") + ", and asked: *is this now what I asked for?*");
3293
+ lines.push("");
3294
+ lines.push(`Treat it as done only when the paragraph above is true on that page, on a fresh load, without the tester doing anything extra. Record the outcome against \`check-${idx}\` in \`verify.md\`.`);
3295
+ }
3256
3296
  const ctx = note.context;
3257
3297
  if (ctx) {
3258
3298
  const env = ctx.env;
@@ -3425,6 +3465,17 @@ Second, **the previous attempt is evidence.** Something about the first reading
3425
3465
  The tag sets your starting posture; **the words win.** A point tagged Design that says "this crashes" is a bug \u2014 treat it as one, and mention that you changed lane. People pick the wrong chip all the time; that is not a reason to do the wrong work.
3426
3466
 
3427
3467
  And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
3468
+ );
3469
+ sections.push(
3470
+ `## You Are Being Graded On This
3471
+
3472
+ This archive is not a list of suggestions. Every point in \`notes.md\` is an acceptance test, and \`verify.md\` is the checklist those tests live in \u2014 one unticked box per point.
3473
+
3474
+ **What happens next.** The tester will be walked back through these same checks on the real page, in this order, one stop at a time, and asked about each: *is this now what I asked for?* Anything they say is still wrong comes back to you as round two with their new words attached, so nothing quietly disappears.
3475
+
3476
+ **What to do.** Do the work, then fill in \`verify.md\`: tick a box only when the check is true on a fresh load of the page named beside it, with the tester doing nothing extra. Where you could not do something, or you think it is the wrong thing to do, leave the box unticked and write one line saying why. An unticked box with a reason is a good answer. A ticked box that does not hold up is the only bad one \u2014 it costs the tester the trip to find out.
3477
+
3478
+ Hand \`verify.md\` back with the work.`
3428
3479
  );
3429
3480
  const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
3430
3481
  const runArr = toStrings(p.runCommands);
@@ -3572,6 +3623,39 @@ async function buildZipBlob(notes, stamp, config, guideChecked, guideSkipped) {
3572
3623
  ` : "";
3573
3624
  const notesMd = preambleMd + "\n\n---NOTES---\n\n" + notesHeader + notesBody;
3574
3625
  zip.file("notes.md", notesMd);
3626
+ zip.file("verify.md", [
3627
+ `# ${brandLabel} \u2014 checks to satisfy`,
3628
+ "",
3629
+ `Exported: ${stamp}`,
3630
+ `Checks: ${notes.length}`,
3631
+ "",
3632
+ "Each line below is one thing the tester asked for. The full ask, the",
3633
+ "screenshot, the page and the element are in `notes.md` under the matching",
3634
+ '"Point" heading.',
3635
+ "",
3636
+ "**What is expected of you**",
3637
+ "",
3638
+ "1. Do the work in `notes.md`.",
3639
+ "2. Tick a box here only when the check is true on a fresh load of the page",
3640
+ " named beside it, with the tester doing nothing extra.",
3641
+ "3. Leave a box unticked and write one line under it saying why, if you",
3642
+ " could not do it or disagree with it. An unticked box with a reason is a",
3643
+ " good answer; a ticked box that is not true is the only bad one.",
3644
+ "4. Hand this file back with the work.",
3645
+ "",
3646
+ "The tester will then be walked back through these same checks on the real",
3647
+ "page, in this order, and asked about each one. Anything they say is still",
3648
+ "wrong comes back to you as round two, with their new words attached.",
3649
+ "",
3650
+ "---",
3651
+ "",
3652
+ ...notes.map((n, i) => noteCheckLine(n, i + 1)),
3653
+ "",
3654
+ "---",
3655
+ "",
3656
+ "Re-open the walkthrough on the tester's machine with `?qa=walk:verify`.",
3657
+ ""
3658
+ ].join("\n"));
3575
3659
  notes.forEach((n, i) => {
3576
3660
  if (n.screenshot && shots) {
3577
3661
  shots.file(`point-${i + 1}.${shotExtension(n.screenshot)}`, n.screenshot);
@@ -4211,6 +4295,9 @@ var GUIDE_FAILED_KEY = "guideFailed";
4211
4295
  var LOGIN_KEY = "logins";
4212
4296
  var PENDING_DELETE_KEY = "pendingDeleteIds";
4213
4297
  var EXACT_SHOTS_KEY = "exactShots";
4298
+ function exactShotsWanted(store) {
4299
+ return store.getItem(EXACT_SHOTS_KEY) !== "0";
4300
+ }
4214
4301
  var SIMPLE_MODE_KEY = "simpleMode";
4215
4302
  var COMPACT_KEY = "compactCapture";
4216
4303
  var LAST_CAMPAIGN_KEY = "lastCampaign";
@@ -4225,11 +4312,12 @@ var PANEL_OPEN_KEY = "panelOpen";
4225
4312
  var ACTIVE_TAB_KEY = "activeTab";
4226
4313
  var WALK_KEY = "walk";
4227
4314
  var SOFT_NAV_PROOF_MS = 700;
4315
+ var SIGNATURE_SEP = String.fromCharCode(0);
4228
4316
  function pageSignature() {
4229
4317
  if (typeof document === "undefined" || !document.body) return "";
4230
4318
  const body = document.body;
4231
4319
  const text = body.textContent || "";
4232
- return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join("\0");
4320
+ return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
4233
4321
  }
4234
4322
  var GUIDE_SKIPPED_KEY = "guideSkipped";
4235
4323
  var NOTICE_QUEUE_CAP = 3;
@@ -4287,8 +4375,12 @@ function QaProvider({
4287
4375
  const [loginsUsed, setLoginsUsed] = React.useState(
4288
4376
  () => new Set(storage.getJSON(LOGIN_KEY, []))
4289
4377
  );
4290
- const [exactStatus, setExactStatus] = React.useState(() => getExactCaptureStatus());
4378
+ const [exactStatus, setExactStatus] = React.useState(() => {
4379
+ if (exactShotsWanted(storage)) armExactCapture();
4380
+ return getExactCaptureStatus();
4381
+ });
4291
4382
  const exactSupported = isExactCaptureSupported();
4383
+ const [frozenAt, setFrozenAt] = React.useState(null);
4292
4384
  const [syncState, setSyncState] = React.useState(() => getFsSyncState());
4293
4385
  const [syncTick, setSyncTick] = React.useState(0);
4294
4386
  const [lastCampaign, setLastCampaign] = React.useState(
@@ -4789,20 +4881,26 @@ function QaProvider({
4789
4881
  flushPendingDeletes();
4790
4882
  for (const timer of noticeTimers.current.values()) clearTimeout(timer);
4791
4883
  noticeTimers.current.clear();
4792
- stopExactCapture();
4884
+ releaseFrozenFrame();
4793
4885
  };
4794
4886
  }, [flushPendingDeletes]);
4795
4887
  const startCapture = React.useCallback((prefill) => {
4796
4888
  setIsOpen(false);
4797
4889
  setCapturePrefill(prefill ?? "");
4798
4890
  setCaptureActive(true);
4799
- if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported() && getExactCaptureStatus() !== "live") {
4891
+ setFrozenAt(null);
4892
+ if (exactShotsWanted(storage) && isExactCaptureSupported()) {
4800
4893
  resetExactCaptureDecline();
4801
- void startExactCapture().then(() => setExactStatus(getExactCaptureStatus()));
4894
+ void freezeViewport().then((frame) => {
4895
+ setFrozenAt(frame?.takenAt ?? null);
4896
+ setExactStatus(getExactCaptureStatus());
4897
+ });
4802
4898
  }
4803
4899
  }, [storage]);
4804
4900
  const endCapture = React.useCallback((reopen = true) => {
4805
4901
  setCaptureActive(false);
4902
+ releaseFrozenFrame();
4903
+ setFrozenAt(null);
4806
4904
  if (reopen) setIsOpen(true);
4807
4905
  }, []);
4808
4906
  const toggleGuide = React.useCallback((key) => {
@@ -4823,19 +4921,36 @@ function QaProvider({
4823
4921
  return next;
4824
4922
  });
4825
4923
  }, [storage]);
4924
+ const stampHandedOff = React.useCallback(async (ids, at) => {
4925
+ if (!ids.length) return;
4926
+ const idSet = new Set(ids);
4927
+ const touched = [];
4928
+ applyNotes(
4929
+ (prev) => prev.map((n) => {
4930
+ if (!idSet.has(n.id)) return n;
4931
+ if ((n.status ?? "open") === "verified") return n;
4932
+ const next = { ...n, handedOffAt: at };
4933
+ touched.push(next);
4934
+ return next;
4935
+ })
4936
+ );
4937
+ for (const note of touched) await idb.put(note);
4938
+ }, [applyNotes, idb]);
4826
4939
  const exportZipFn = React.useCallback(
4827
4940
  async (filename) => {
4828
4941
  if (!notes.length || isExporting) return;
4829
4942
  setIsExporting(true);
4830
4943
  try {
4831
- await buildAndDownloadZip(notes, nowIso(), filename, config, guideChecked, guideSkipped);
4944
+ const stamp = nowIso();
4945
+ await buildAndDownloadZip(notes, stamp, filename, config, guideChecked, guideSkipped);
4946
+ await stampHandedOff(notes.map((n) => n.id), stamp);
4832
4947
  } catch (err) {
4833
4948
  console.error("[QA] export failed", err);
4834
4949
  } finally {
4835
4950
  setIsExporting(false);
4836
4951
  }
4837
4952
  },
4838
- [notes, isExporting, config, guideChecked, guideSkipped]
4953
+ [notes, isExporting, config, guideChecked, guideSkipped, stampHandedOff]
4839
4954
  );
4840
4955
  const exportZipRef = React.useRef(async () => {
4841
4956
  });
@@ -4843,16 +4958,32 @@ function QaProvider({
4843
4958
  exportZipRef.current = exportZipFn;
4844
4959
  }, [exportZipFn]);
4845
4960
  const enableExactShots = React.useCallback(async () => {
4846
- resetExactCaptureDecline();
4847
- const ok = await startExactCapture();
4961
+ const ok = armExactCapture();
4848
4962
  setExactStatus(getExactCaptureStatus());
4849
4963
  storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
4850
4964
  notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
4851
4965
  return ok;
4852
4966
  }, [storage, notify, t]);
4967
+ const photographNow = React.useCallback(async () => {
4968
+ if (!armExactCapture()) {
4969
+ setExactStatus(getExactCaptureStatus());
4970
+ notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
4971
+ return false;
4972
+ }
4973
+ storage.setItem(EXACT_SHOTS_KEY, "1");
4974
+ const frame = await freezeViewport();
4975
+ setFrozenAt(frame?.takenAt ?? null);
4976
+ setExactStatus(getExactCaptureStatus());
4977
+ notify(frame ? t("exact_on") : t("exact_declined"), {
4978
+ tone: frame ? "success" : "info",
4979
+ id: "exact-shots"
4980
+ });
4981
+ return !!frame;
4982
+ }, [storage, notify, t]);
4853
4983
  const disableExactShots = React.useCallback(() => {
4854
- stopExactCapture();
4984
+ disarmExactCapture();
4855
4985
  storage.setItem(EXACT_SHOTS_KEY, "0");
4986
+ setFrozenAt(null);
4856
4987
  setExactStatus(getExactCaptureStatus());
4857
4988
  }, [storage]);
4858
4989
  const rememberCampaign = React.useCallback((next) => {
@@ -5005,6 +5136,7 @@ function QaProvider({
5005
5136
  design: 0,
5006
5137
  enhance: 0,
5007
5138
  open: 0,
5139
+ sent: 0,
5008
5140
  fixed: 0,
5009
5141
  verified: 0,
5010
5142
  thisPage: 0
@@ -5018,6 +5150,7 @@ function QaProvider({
5018
5150
  if (status === "verified") counts.verified++;
5019
5151
  else if (status === "fixed") counts.fixed++;
5020
5152
  else counts.open++;
5153
+ if (n.handedOffAt && status !== "verified") counts.sent++;
5021
5154
  if (n.route.split("?")[0] === currentRoute) counts.thisPage++;
5022
5155
  }
5023
5156
  return counts;
@@ -5026,7 +5159,9 @@ function QaProvider({
5026
5159
  const q = filter.query.trim().toLowerCase();
5027
5160
  return notes.filter((n) => {
5028
5161
  if (filter.severity !== "all" && (n.severity ?? "bug") !== filter.severity) return false;
5029
- if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
5162
+ if (filter.status === "sent") {
5163
+ if (!n.handedOffAt || (n.status ?? "open") === "verified") return false;
5164
+ } else if (filter.status !== "all" && (n.status ?? "open") !== filter.status) return false;
5030
5165
  if (filter.thisPageOnly && n.route.split("?")[0] !== currentRoute) return false;
5031
5166
  if (!q) return true;
5032
5167
  const haystack = `${n.description} ${n.route} ${n.target?.selector ?? ""} ${n.target?.text ?? ""}`;
@@ -5071,6 +5206,7 @@ function QaProvider({
5071
5206
  const blob = await buildZipBlob(notes, stamp, config, guideChecked, guideSkipped);
5072
5207
  if (!blob) return { status: "unsupported" };
5073
5208
  const outcome = await shareZipFile(blob, name, config.brand.label);
5209
+ await stampHandedOff(notes.map((n) => n.id), stamp);
5074
5210
  if (outcome.status === "needs-gesture") {
5075
5211
  setPendingShare({ blob, filename: name });
5076
5212
  } else if (outcome.status === "unsupported") {
@@ -5082,7 +5218,7 @@ function QaProvider({
5082
5218
  } finally {
5083
5219
  setIsExporting(false);
5084
5220
  }
5085
- }, [notes, config, guideChecked, guideSkipped]);
5221
+ }, [notes, config, guideChecked, guideSkipped, stampHandedOff]);
5086
5222
  const sharePending = React.useCallback(async () => {
5087
5223
  const pending = pendingShare;
5088
5224
  if (!pending) return { status: "unsupported" };
@@ -5298,7 +5434,10 @@ function QaProvider({
5298
5434
  window.history.replaceState({}, "", window.location.pathname + (qs ? `?${qs}` : ""));
5299
5435
  } catch {
5300
5436
  }
5301
- if (which === "retest") {
5437
+ if (which === "verify") {
5438
+ setFilterState((prev) => ({ ...prev, status: "sent", severity: "all", thisPageOnly: false }));
5439
+ startWalk("notes", 0);
5440
+ } else if (which === "retest") {
5302
5441
  setFilterState((prev) => ({ ...prev, status: "fixed", severity: "all", thisPageOnly: false }));
5303
5442
  startWalk("notes", 0);
5304
5443
  } else if (which === "notes") {
@@ -5371,8 +5510,9 @@ function QaProvider({
5371
5510
  gradeStep,
5372
5511
  evidenceByStep,
5373
5512
  // v0.4 — screenshots
5374
- exactShots: { supported: exactSupported, status: exactStatus },
5513
+ exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
5375
5514
  enableExactShots,
5515
+ photographNow,
5376
5516
  disableExactShots,
5377
5517
  // v0.4 — folder sync. syncTick is read here purely so this object is
5378
5518
  // rebuilt when fsSync's module state changes (path/campaign live outside
@@ -7355,7 +7495,7 @@ function FilterChip({ chip }) {
7355
7495
  );
7356
7496
  }
7357
7497
  function NoteFilterBar() {
7358
- const { filter, setFilter, noteCounts, notes, deleteNotes, t } = useQa();
7498
+ const { filter, setFilter, noteCounts, notes, deleteNotes, startWalk, t } = useQa();
7359
7499
  const [confirmPrune, setConfirmPrune] = React.useState(false);
7360
7500
  if (notes.length < 3) return null;
7361
7501
  const others = notes.filter((n) => (n.status ?? "open") !== "fixed");
@@ -7397,6 +7537,18 @@ function NoteFilterBar() {
7397
7537
  tone: "accent",
7398
7538
  onClick: sev("design")
7399
7539
  },
7540
+ // Only ever shown once something has actually been sent. A chip reading
7541
+ // "Sent 0" on a first run is noise; the same chip reading "Sent 6" the
7542
+ // morning after an export is the one thing the tester came back for.
7543
+ ...noteCounts.sent > 0 ? [{
7544
+ key: "sent",
7545
+ label: t("status_sent"),
7546
+ count: noteCounts.sent,
7547
+ active: filter.status === "sent",
7548
+ icon: "Camera",
7549
+ tone: "accent",
7550
+ onClick: () => setFilter({ status: "sent", severity: "all", thisPageOnly: false })
7551
+ }] : [],
7400
7552
  {
7401
7553
  key: "open",
7402
7554
  label: t("status_open"),
@@ -7465,6 +7617,24 @@ function NoteFilterBar() {
7465
7617
  }
7466
7618
  ),
7467
7619
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-flex qa-flex-wrap qa-gap-1", children: chips.map((chip) => /* @__PURE__ */ jsxRuntime.jsx(FilterChip, { chip }, chip.key)) }),
7620
+ noteCounts.sent > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
7621
+ "button",
7622
+ {
7623
+ type: "button",
7624
+ "data-qa-verify-walk": "true",
7625
+ onClick: () => {
7626
+ setFilter({ status: "sent", severity: "all", thisPageOnly: false });
7627
+ startWalk("notes", 0);
7628
+ },
7629
+ title: t("check_fixes_hint"),
7630
+ className: "qa-tap qa-inline-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1.5 qa-text-xs qa-text-hi qa-focus-ring",
7631
+ style: { background: "transparent", cursor: "pointer" },
7632
+ children: [
7633
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Check", size: 13 }),
7634
+ t("check_fixes", { n: noteCounts.sent })
7635
+ ]
7636
+ }
7637
+ ),
7468
7638
  showPrune && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "qa-text-11", "data-qa-prune": "true", children: confirmPrune ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-text-mid", children: [
7469
7639
  t("keep_only_retest_q", { n: others.length }),
7470
7640
  " ",
@@ -9205,7 +9375,7 @@ function CaptureMode() {
9205
9375
  compactCapture,
9206
9376
  setCompactCapture,
9207
9377
  exactShots,
9208
- enableExactShots,
9378
+ photographNow,
9209
9379
  capturePrefill
9210
9380
  } = useQa();
9211
9381
  const coarse = useCoarsePointer();
@@ -9219,10 +9389,13 @@ function CaptureMode() {
9219
9389
  const [candidate, setCandidate] = React.useState(null);
9220
9390
  const [regionMode, setRegionMode] = React.useState(false);
9221
9391
  const [shot, setShot] = React.useState(null);
9392
+ const stillRef = React.useRef(null);
9393
+ const [notesFromThisShot, setNotesFromThisShot] = React.useState(0);
9222
9394
  const [shotEngine, setShotEngine] = React.useState(null);
9223
9395
  const [shotUrl, setShotUrl] = React.useState(null);
9224
9396
  const [capturing, setCapturing] = React.useState(false);
9225
9397
  const [captureError, setCaptureError] = React.useState(false);
9398
+ const [tooHeavy, setTooHeavy] = React.useState(false);
9226
9399
  const [description, setDescription] = React.useState(capturePrefill);
9227
9400
  const [severity, setSeverity] = React.useState("bug");
9228
9401
  const [targetForensics, setTargetForensics] = React.useState(void 0);
@@ -9252,6 +9425,7 @@ function CaptureMode() {
9252
9425
  const runCapture = React.useCallback(async (rect) => {
9253
9426
  setCapturing(true);
9254
9427
  setCaptureError(false);
9428
+ setTooHeavy(false);
9255
9429
  lockPageScroll();
9256
9430
  try {
9257
9431
  const outcome = await captureRegion(rect, scrollSnap.current);
@@ -9262,6 +9436,7 @@ function CaptureMode() {
9262
9436
  return;
9263
9437
  }
9264
9438
  setCaptureError(outcome.status === "failed");
9439
+ setTooHeavy(outcome.status === "too-heavy");
9265
9440
  setShotEngine(outcome.status === "ok" ? outcome.engine : null);
9266
9441
  setShot(blob);
9267
9442
  setShotUrl((old) => {
@@ -9511,7 +9686,29 @@ function CaptureMode() {
9511
9686
  React.useEffect(() => () => {
9512
9687
  unlockPageScroll();
9513
9688
  }, []);
9514
- const save = async () => {
9689
+ React.useEffect(() => {
9690
+ const host = stillRef.current;
9691
+ const frame = getFrozenFrame();
9692
+ if (!host || !frame) return;
9693
+ const canvas = frame.canvas;
9694
+ canvas.style.cssText = "display:block;width:100%;height:100%;";
9695
+ host.appendChild(canvas);
9696
+ return () => {
9697
+ if (canvas.parentNode === host) host.removeChild(canvas);
9698
+ canvas.removeAttribute("style");
9699
+ };
9700
+ }, [exactShots.frozenAt]);
9701
+ const resetForNextNote = React.useCallback(() => {
9702
+ setPhase("selecting");
9703
+ setSelection(null);
9704
+ setShot(null);
9705
+ setShotEngine(null);
9706
+ setDescription("");
9707
+ setSeverity("bug");
9708
+ setTargetForensics(void 0);
9709
+ setCaptureError(false);
9710
+ }, []);
9711
+ const save = async (keepGoing = false) => {
9515
9712
  if (!selection || !description.trim()) return;
9516
9713
  const target = {
9517
9714
  kind: selection.kind,
@@ -9533,6 +9730,11 @@ function CaptureMode() {
9533
9730
  severity,
9534
9731
  forensics: selection.kind === "element" ? targetForensics : void 0
9535
9732
  });
9733
+ if (keepGoing) {
9734
+ setNotesFromThisShot((n) => n + 1);
9735
+ resetForNextNote();
9736
+ return;
9737
+ }
9536
9738
  endCapture();
9537
9739
  };
9538
9740
  const popStyleFor = React.useCallback((r, cardWidth = 340) => {
@@ -9569,6 +9771,16 @@ function CaptureMode() {
9569
9771
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
9570
9772
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
9571
9773
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
9774
+ exactShots.frozenAt !== null && /* @__PURE__ */ jsxRuntime.jsx(
9775
+ "div",
9776
+ {
9777
+ ref: stillRef,
9778
+ "aria-hidden": "true",
9779
+ "data-qa-capture-still": "true",
9780
+ className: "qa-fixed qa-inset-0 qa-z-10089",
9781
+ style: { pointerEvents: "none", overflow: "hidden" }
9782
+ }
9783
+ ),
9572
9784
  /* @__PURE__ */ jsxRuntime.jsx(
9573
9785
  "div",
9574
9786
  {
@@ -9590,6 +9802,21 @@ function CaptureMode() {
9590
9802
  {
9591
9803
  className: "qa-fixed qa-left-half qa-top-4 qa-z-10095 qa-translate-x-neg-half qa-flex qa-items-center qa-gap-3 qa-rounded-full qa-border qa-border-subtle qa-bg-2 qa-px-4 qa-py-2 qa-text-sm qa-text-hi qa-elev-2",
9592
9804
  children: [
9805
+ notesFromThisShot > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9806
+ /* @__PURE__ */ jsxRuntime.jsxs(
9807
+ "span",
9808
+ {
9809
+ "data-qa-shot-reused": "true",
9810
+ className: "qa-flex qa-items-center qa-gap-1.5",
9811
+ style: { color: "var(--qa-success)" },
9812
+ children: [
9813
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 16 }),
9814
+ t("shot_reused", { n: String(notesFromThisShot) })
9815
+ ]
9816
+ }
9817
+ ),
9818
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-opacity-50", children: "\xB7" })
9819
+ ] }),
9593
9820
  /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-1.5", children: [
9594
9821
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MousePointerClick", size: 16 }),
9595
9822
  t("cap_click")
@@ -9627,7 +9854,7 @@ function CaptureMode() {
9627
9854
  ) : /* @__PURE__ */ jsxRuntime.jsxs(
9628
9855
  "button",
9629
9856
  {
9630
- onClick: () => void enableExactShots(),
9857
+ onClick: () => void photographNow(),
9631
9858
  title: t("exact_hint"),
9632
9859
  className: "qa-tap qa-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-white-40 qa-px-2 qa-py-0.5 qa-text-11 qa-text-hi qa-hover-bg-white-15",
9633
9860
  style: { background: "transparent", cursor: "pointer" },
@@ -10039,6 +10266,29 @@ function CaptureMode() {
10039
10266
  )
10040
10267
  ]
10041
10268
  }
10269
+ ) : tooHeavy ? (
10270
+ // Not a failure to report — a refusal to explain, with the one
10271
+ // control that actually solves it.
10272
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3 qa-px-3 qa-text-center", children: [
10273
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-mid", children: t("too_heavy") }),
10274
+ exactShots.supported ? /* @__PURE__ */ jsxRuntime.jsxs(
10275
+ "button",
10276
+ {
10277
+ type: "button",
10278
+ onClick: () => void (async () => {
10279
+ const shot2 = await photographNow();
10280
+ if (shot2 && selection) void runCapture(selection.rect);
10281
+ })(),
10282
+ title: t("exact_hint"),
10283
+ 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",
10284
+ style: { background: "transparent", cursor: "pointer" },
10285
+ children: [
10286
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 13 }),
10287
+ t("redraw_action")
10288
+ ]
10289
+ }
10290
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-10 qa-text-mid", children: t("redraw_no_engine") })
10291
+ ] })
10042
10292
  ) : captureError ? (
10043
10293
  // The render broke rather than being skipped — offer a retry
10044
10294
  // against the same selection instead of a dead-end message.
@@ -10071,8 +10321,8 @@ function CaptureMode() {
10071
10321
  {
10072
10322
  type: "button",
10073
10323
  onClick: () => void (async () => {
10074
- const granted = await enableExactShots();
10075
- if (granted && selection) void runCapture(selection.rect);
10324
+ const shot2 = await photographNow();
10325
+ if (shot2 && selection) void runCapture(selection.rect);
10076
10326
  })(),
10077
10327
  title: t("exact_hint"),
10078
10328
  className: "qa-tap qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-border qa-border-subtle qa-px-1.5 qa-py-0.5 qa-text-10 qa-text-hi qa-focus-ring",
@@ -10145,7 +10395,7 @@ function CaptureMode() {
10145
10395
  value: description,
10146
10396
  onChange: (e) => setDescription(e.target.value),
10147
10397
  onKeyDown: (e) => {
10148
- if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save();
10398
+ if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) void save(e.shiftKey);
10149
10399
  if ((e.altKey || e.metaKey || e.ctrlKey) && SEVERITIES2[Number(e.key) - 1]) {
10150
10400
  e.preventDefault();
10151
10401
  setSeverity(SEVERITIES2[Number(e.key) - 1]);
@@ -10170,17 +10420,25 @@ function CaptureMode() {
10170
10420
  ]
10171
10421
  }
10172
10422
  ),
10423
+ /* @__PURE__ */ jsxRuntime.jsxs(
10424
+ "button",
10425
+ {
10426
+ "data-qa-save-next": "true",
10427
+ onClick: () => void save(true),
10428
+ disabled: !description.trim(),
10429
+ title: t("save_next_hint"),
10430
+ className: "qa-tap qa-flex qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-hi",
10431
+ style: { background: "transparent", cursor: "pointer" },
10432
+ children: [
10433
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Plus", size: 16 }),
10434
+ t("save_next")
10435
+ ]
10436
+ }
10437
+ ),
10173
10438
  /* @__PURE__ */ jsxRuntime.jsx(
10174
10439
  "button",
10175
10440
  {
10176
- onClick: () => {
10177
- setPhase("selecting");
10178
- setSelection(null);
10179
- setShot(null);
10180
- setDescription("");
10181
- setSeverity("bug");
10182
- setTargetForensics(void 0);
10183
- },
10441
+ onClick: resetForNextNote,
10184
10442
  className: "qa-tap qa-rounded-lg qa-border qa-border-subtle qa-px-3 qa-py-2 qa-text-sm qa-text-mid",
10185
10443
  style: { background: "transparent", cursor: "pointer" },
10186
10444
  children: t("reselect")
@@ -10398,5 +10656,5 @@ function Qapture({ config }) {
10398
10656
  exports.Qapture = Qapture;
10399
10657
  exports.deleteQaDatabase = deleteQaDatabase;
10400
10658
  exports.initQaStudio = initQaStudio;
10401
- //# sourceMappingURL=chunk-6UNSUNJS.cjs.map
10402
- //# sourceMappingURL=chunk-6UNSUNJS.cjs.map
10659
+ //# sourceMappingURL=chunk-IYQ5JZJX.cjs.map
10660
+ //# sourceMappingURL=chunk-IYQ5JZJX.cjs.map