qapture2 0.7.8 → 0.8.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.
@@ -51,10 +51,10 @@ function openDB(dbName) {
51
51
  dbCache.set(dbName, promise);
52
52
  return promise;
53
53
  }
54
- function run(dbName, store, mode2, fn) {
54
+ function run(dbName, store, mode, fn) {
55
55
  return openDB(dbName).then(
56
56
  (db) => new Promise((resolve, reject) => {
57
- const tx = db.transaction(store, mode2);
57
+ const tx = db.transaction(store, mode);
58
58
  const s = tx.objectStore(store);
59
59
  let result;
60
60
  const req = fn(s);
@@ -509,6 +509,7 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
509
509
  /* Z-index scale */
510
510
  --qa-z-fab: 9990;
511
511
  --qa-z-panel: 9995;
512
+ --qa-z-capture-still: 10089;
512
513
  --qa-z-capture-dim: 10090;
513
514
  --qa-z-capture-highlight: 10092;
514
515
  --qa-z-capture-region-move: 10093;
@@ -558,6 +559,8 @@ ul, ol { list-style: none; margin: 0; padding: 0; }
558
559
  .qa-z-1 { z-index: 1; }
559
560
  .qa-z-50 { z-index: 50; }
560
561
  .qa-z-100 { z-index: 100; }
562
+ /* the frozen still, directly beneath the dim so the scrim tints it too */
563
+ .qa-z-10089 { z-index: var(--qa-z-capture-still); }
561
564
  .qa-z-10090 { z-index: var(--qa-z-capture-dim); }
562
565
  .qa-z-10092 { z-index: var(--qa-z-capture-highlight); }
563
566
  /* region-handle layering */
@@ -1734,6 +1737,8 @@ var STR = {
1734
1737
  loc_show: "Show captured location",
1735
1738
  loc_hide: "Hide location",
1736
1739
  loc_locate: "Locate on page",
1740
+ loc_other_page: "This one was filed on {path}, not the page you are on.",
1741
+ loc_go_there: "Go to {path}",
1737
1742
  journey_title: "Testing journey",
1738
1743
  journey_generic: "No test plan was set up for this project, so this is a general one that fits any app. Work through it, or ask whoever set up Qapture to add the real journey to qa.config.",
1739
1744
  export_name_title: "Name your export",
@@ -1774,7 +1779,7 @@ var STR = {
1774
1779
  no_evidence: "ticked, no capture",
1775
1780
  expected_label: "Expected",
1776
1781
  exact_label: "Pixel-exact shots",
1777
- 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.",
1782
+ 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.",
1778
1783
  redraw_notice: "This is a redraw of the page, not a real photo.",
1779
1784
  redraw_action: "Use real screenshots",
1780
1785
  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.",
@@ -1783,7 +1788,7 @@ var STR = {
1783
1788
  exact_off: "Back to redrawn screenshots",
1784
1789
  exact_declined: "Staying on redrawn screenshots",
1785
1790
  exact_turn_on: "Turn on",
1786
- exact_unsupported: "Needs Chrome, Edge or Brave on desktop",
1791
+ exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
1787
1792
  sync_title: "Save to a folder",
1788
1793
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1789
1794
  sync_choose: "Choose folder",
@@ -1889,6 +1894,7 @@ var STR = {
1889
1894
  capture_screen: "Whole screen",
1890
1895
  walk_title: "Walkthrough",
1891
1896
  walk_take_me: "Take me to {path}",
1897
+ walk_taking_you: "Taking you there\u2026",
1892
1898
  walk_reload_hint: "Reload the page instead (use this if the app didn't move)",
1893
1899
  walk_note_title: "Point {n}",
1894
1900
  walk_still_broken: "Still broken",
@@ -1939,6 +1945,8 @@ var STR = {
1939
1945
  loc_show: "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u064F\u0644\u062A\u0642\u064E\u0637",
1940
1946
  loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
1941
1947
  loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
1948
+ loc_other_page: "\u0633\u064F\u062C\u0651\u0644\u062A \u0647\u0630\u0647 \u0641\u064A {path}\u060C \u0648\u0644\u064A\u0633\u062A \u0627\u0644\u0635\u0641\u062D\u0629 \u0627\u0644\u062A\u064A \u0623\u0646\u062A \u0641\u064A\u0647\u0627.",
1949
+ loc_go_there: "\u0627\u0630\u0647\u0628 \u0625\u0644\u0649 {path}",
1942
1950
  journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
1943
1951
  journey_generic: "\u0644\u0645 \u064A\u062A\u0645 \u0625\u0639\u062F\u0627\u062F \u062E\u0637\u0629 \u0627\u062E\u062A\u0628\u0627\u0631 \u0644\u0647\u0630\u0627 \u0627\u0644\u0645\u0634\u0631\u0648\u0639\u060C \u0644\u0630\u0644\u0643 \u0647\u0630\u0647 \u062E\u0637\u0629 \u0639\u0627\u0645\u0629 \u062A\u0635\u0644\u062D \u0644\u0623\u064A \u062A\u0637\u0628\u064A\u0642. \u0627\u0639\u0645\u0644 \u0628\u0647\u0627\u060C \u0623\u0648 \u0627\u0637\u0644\u0628 \u0645\u0645\u0646 \u062B\u0628\u0651\u062A Qapture \u0623\u0646 \u064A\u0636\u064A\u0641 \u0631\u062D\u0644\u0629 \u0627\u0644\u0645\u0634\u0631\u0648\u0639 \u0641\u064A qa.config.",
1944
1952
  export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
@@ -1979,7 +1987,7 @@ var STR = {
1979
1987
  no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
1980
1988
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1981
1989
  exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
1982
- 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.",
1990
+ 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.",
1983
1991
  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.",
1984
1992
  redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1985
1993
  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.",
@@ -1988,7 +1996,7 @@ var STR = {
1988
1996
  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",
1989
1997
  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",
1990
1998
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
1991
- 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",
1999
+ 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",
1992
2000
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
1993
2001
  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.",
1994
2002
  sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
@@ -2094,6 +2102,7 @@ var STR = {
2094
2102
  capture_screen: "\u0627\u0644\u0634\u0627\u0634\u0629 \u0643\u0627\u0645\u0644\u0629",
2095
2103
  walk_title: "\u0627\u0644\u062C\u0648\u0644\u0629",
2096
2104
  walk_take_me: "\u062E\u0630\u0646\u064A \u0625\u0644\u0649 {path}",
2105
+ walk_taking_you: "\u062C\u0627\u0631\u064D \u0646\u0642\u0644\u0643\u2026",
2097
2106
  walk_reload_hint: "\u0623\u0639\u062F \u062A\u062D\u0645\u064A\u0644 \u0627\u0644\u0635\u0641\u062D\u0629 \u0628\u062F\u0644\u064B\u0627 \u0645\u0646 \u0630\u0644\u0643 (\u0627\u0633\u062A\u062E\u062F\u0645\u0647 \u0625\u0630\u0627 \u0644\u0645 \u064A\u0646\u062A\u0642\u0644 \u0627\u0644\u062A\u0637\u0628\u064A\u0642)",
2098
2107
  walk_note_title: "\u0627\u0644\u0646\u0642\u0637\u0629 {n}",
2099
2108
  walk_still_broken: "\u0645\u0627 \u0632\u0627\u0644 \u0645\u0639\u0637\u0644\u064B\u0627",
@@ -2292,7 +2301,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2292
2301
  if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
2293
2302
  const originX = tl.x - centres.tl.x * scaleX;
2294
2303
  const originY = tl.y - centres.tl.y * scaleY;
2295
- const mapping2 = { scaleX, scaleY, originX, originY };
2304
+ const mapping = { scaleX, scaleY, originX, originY };
2296
2305
  const farX = originX + vw * scaleX;
2297
2306
  const farY = originY + vh * scaleY;
2298
2307
  const slack = Math.max(4, frameW * 0.01);
@@ -2311,7 +2320,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2311
2320
  verified++;
2312
2321
  }
2313
2322
  if (verified === 0) return null;
2314
- return mapping2;
2323
+ return mapping;
2315
2324
  }
2316
2325
  function mapRectToFrame(rect, m, frameW, frameH) {
2317
2326
  const x0 = m.originX + rect.left * m.scaleX;
@@ -2323,73 +2332,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2323
2332
  if (sw < 1 || sh < 1) return null;
2324
2333
  return { sx, sy, sw, sh };
2325
2334
  }
2326
- function environmentSignature() {
2327
- if (typeof window === "undefined") return "";
2328
- return [
2329
- window.innerWidth,
2330
- window.innerHeight,
2331
- window.outerWidth,
2332
- window.outerHeight,
2333
- window.screenX,
2334
- window.screenY,
2335
- Math.round((window.devicePixelRatio || 1) * 100)
2336
- ].join("x");
2337
- }
2338
2335
 
2339
2336
  // src/lib/screenCapture.ts
2340
2337
  var ASPECT_TOLERANCE = 0.08;
2341
2338
  var CALIBRATION_SETTLE_MS = 220;
2342
2339
  var FRESH_FRAME_TIMEOUT_MS = 500;
2343
2340
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2344
- var stream = null;
2345
- var track = null;
2346
- var video = null;
2347
- var grabber = null;
2348
- var declined = false;
2349
2341
  function isExactCaptureSupported() {
2350
2342
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2351
2343
  const md = navigator.mediaDevices;
2352
2344
  return !!md && typeof md.getDisplayMedia === "function";
2353
2345
  }
2354
- var mode = "tab";
2355
- var mapping = null;
2356
- var mappingSignature = "";
2346
+ var armed = false;
2347
+ var declined = false;
2348
+ var frozen = null;
2349
+ var lastMode = null;
2350
+ function armExactCapture() {
2351
+ if (!isExactCaptureSupported()) return false;
2352
+ armed = true;
2353
+ declined = false;
2354
+ return true;
2355
+ }
2356
+ function disarmExactCapture() {
2357
+ armed = false;
2358
+ releaseFrozenFrame();
2359
+ }
2360
+ function resetExactCaptureDecline() {
2361
+ declined = false;
2362
+ }
2357
2363
  function getExactCaptureStatus() {
2358
2364
  if (!isExactCaptureSupported()) return "unsupported";
2359
- if (track && track.readyState === "live") return "live";
2365
+ if (armed) return "live";
2360
2366
  if (declined) return "declined";
2361
2367
  return "idle";
2362
2368
  }
2363
- function teardown() {
2364
- if (video) {
2365
- try {
2366
- video.pause();
2367
- } catch {
2368
- }
2369
- video.srcObject = null;
2370
- video.remove();
2371
- }
2372
- if (stream) {
2373
- for (const t of stream.getTracks()) {
2374
- try {
2375
- t.stop();
2376
- } catch {
2377
- }
2378
- }
2379
- }
2380
- video = null;
2381
- stream = null;
2382
- track = null;
2383
- grabber = null;
2384
- mode = "tab";
2385
- mapping = null;
2386
- mappingSignature = "";
2387
- }
2388
- function stopExactCapture() {
2389
- teardown();
2369
+ function getFrozenFrame() {
2370
+ return frozen;
2390
2371
  }
2391
- function resetExactCaptureDecline() {
2392
- declined = false;
2372
+ function releaseFrozenFrame() {
2373
+ if (frozen) {
2374
+ frozen.canvas.width = 0;
2375
+ frozen.canvas.height = 0;
2376
+ }
2377
+ frozen = null;
2393
2378
  }
2394
2379
  function waitFor(test, timeoutMs) {
2395
2380
  if (test()) return Promise.resolve(true);
@@ -2429,17 +2414,28 @@ function nextPresentedFrame(el) {
2429
2414
  });
2430
2415
  });
2431
2416
  }
2432
- function validateFrameMapping(frameW, frameH) {
2417
+ function looksLikeViewport(frameW, frameH) {
2433
2418
  if (!frameW || !frameH) return false;
2434
2419
  const vw = window.innerWidth;
2435
2420
  const vh = window.innerHeight;
2436
2421
  if (!vw || !vh) return false;
2437
- const frameAspect = frameW / frameH;
2438
- const viewAspect = vw / vh;
2439
- return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
2422
+ return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
2423
+ }
2424
+ async function withOverlayHidden(fn) {
2425
+ const hosts = Array.from(
2426
+ document.querySelectorAll("body > [data-qa-overlay]")
2427
+ );
2428
+ const previous = hosts.map((el) => el.style.visibility);
2429
+ for (const el of hosts) el.style.visibility = "hidden";
2430
+ try {
2431
+ return await fn();
2432
+ } finally {
2433
+ hosts.forEach((el, i) => {
2434
+ el.style.visibility = previous[i];
2435
+ });
2436
+ }
2440
2437
  }
2441
- async function grabFrameCanvas() {
2442
- if (!video) return null;
2438
+ async function grabFrameCanvas(video, grabber) {
2443
2439
  await nextPresentedFrame(video);
2444
2440
  let source = video;
2445
2441
  let w = video.videoWidth;
@@ -2470,11 +2466,10 @@ async function grabFrameCanvas() {
2470
2466
  bitmap?.close?.();
2471
2467
  return c;
2472
2468
  }
2473
- async function calibrate() {
2474
- if (!video) return false;
2469
+ async function calibrate(video, grabber) {
2475
2470
  const vw = window.innerWidth;
2476
2471
  const vh = window.innerHeight;
2477
- if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
2472
+ if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
2478
2473
  const card = document.createElement("div");
2479
2474
  card.setAttribute("data-qa-calibration", "true");
2480
2475
  card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
@@ -2488,10 +2483,10 @@ async function calibrate() {
2488
2483
  document.body.appendChild(card);
2489
2484
  try {
2490
2485
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2491
- const frame = await grabFrameCanvas();
2492
- if (!frame) return false;
2486
+ const frame = await grabFrameCanvas(video, grabber);
2487
+ if (!frame) return null;
2493
2488
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2494
- if (!ctx) return false;
2489
+ if (!ctx) return null;
2495
2490
  const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
2496
2491
  const found = deriveMapping(
2497
2492
  findMarkerCandidates(data, frame.width, frame.height),
@@ -2500,20 +2495,40 @@ async function calibrate() {
2500
2495
  frame.width,
2501
2496
  frame.height
2502
2497
  );
2503
- if (!found) return false;
2504
- mapping = found;
2505
- mappingSignature = environmentSignature();
2506
- return true;
2498
+ frame.width = 0;
2499
+ frame.height = 0;
2500
+ return found;
2507
2501
  } catch {
2508
- return false;
2502
+ return null;
2509
2503
  } finally {
2510
2504
  card.remove();
2511
2505
  }
2512
2506
  }
2513
- async function startExactCapture() {
2514
- if (!isExactCaptureSupported()) return false;
2515
- if (track && track.readyState === "live" && video) return true;
2516
- if (stream) teardown();
2507
+ async function freezeViewport() {
2508
+ if (!isExactCaptureSupported()) return null;
2509
+ releaseFrozenFrame();
2510
+ let stream = null;
2511
+ let el = null;
2512
+ const release = () => {
2513
+ if (el) {
2514
+ try {
2515
+ el.pause();
2516
+ } catch {
2517
+ }
2518
+ el.srcObject = null;
2519
+ el.remove();
2520
+ el = null;
2521
+ }
2522
+ if (stream) {
2523
+ for (const t of stream.getTracks()) {
2524
+ try {
2525
+ t.stop();
2526
+ } catch {
2527
+ }
2528
+ }
2529
+ stream = null;
2530
+ }
2531
+ };
2517
2532
  try {
2518
2533
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
2519
2534
  const constraints = {
@@ -2531,121 +2546,103 @@ async function startExactCapture() {
2531
2546
  surfaceSwitching: "exclude",
2532
2547
  systemAudio: "exclude"
2533
2548
  };
2534
- const s = await navigator.mediaDevices.getDisplayMedia(constraints);
2535
- const t = s.getVideoTracks()[0];
2536
- if (!t) {
2537
- for (const x of s.getTracks()) x.stop();
2549
+ stream = await navigator.mediaDevices.getDisplayMedia(constraints);
2550
+ const track = stream.getVideoTracks()[0];
2551
+ if (!track) {
2538
2552
  declined = true;
2539
- return false;
2553
+ return null;
2540
2554
  }
2541
- const settings = t.getSettings();
2555
+ const settings = track.getSettings();
2542
2556
  const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
2543
- const el = document.createElement("video");
2557
+ el = document.createElement("video");
2544
2558
  el.setAttribute("data-qa-overlay", "true");
2545
2559
  el.muted = true;
2546
2560
  el.playsInline = true;
2547
2561
  el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
2548
- el.srcObject = s;
2562
+ el.srcObject = stream;
2549
2563
  document.body.appendChild(el);
2550
2564
  try {
2551
2565
  await el.play();
2552
2566
  } catch {
2553
2567
  }
2554
- const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
2555
- const giveUp = () => {
2556
- el.remove();
2557
- for (const x of s.getTracks()) x.stop();
2568
+ const video = el;
2569
+ const ready = await waitFor(
2570
+ () => video.videoWidth > 0 && video.videoHeight > 0,
2571
+ VIDEO_READY_TIMEOUT_MS
2572
+ );
2573
+ if (!ready) {
2558
2574
  declined = true;
2559
- return false;
2560
- };
2561
- if (!ready) return giveUp();
2562
- const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
2563
- stream = s;
2564
- track = t;
2565
- video = el;
2575
+ return null;
2576
+ }
2566
2577
  const IC = window.ImageCapture;
2567
- grabber = IC ? new IC(t) : null;
2568
- if (isDirect) {
2569
- mode = "tab";
2570
- mapping = null;
2571
- mappingSignature = "";
2572
- } else {
2573
- mode = "surface";
2574
- if (!await calibrate()) {
2575
- teardown();
2576
- declined = true;
2577
- return false;
2578
- }
2578
+ const grabber = IC ? new IC(track) : null;
2579
+ const vw = window.innerWidth;
2580
+ const vh = window.innerHeight;
2581
+ const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2582
+ const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
2583
+ if (mode === "surface" && !mapping) {
2584
+ declined = true;
2585
+ return null;
2579
2586
  }
2580
- t.addEventListener("ended", () => {
2581
- teardown();
2582
- });
2583
- return true;
2587
+ const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2588
+ if (!raw) return null;
2589
+ let page;
2590
+ if (mode === "surface" && mapping) {
2591
+ const box = mapRectToFrame(
2592
+ { left: 0, top: 0, width: vw, height: vh },
2593
+ mapping,
2594
+ raw.width,
2595
+ raw.height
2596
+ );
2597
+ if (!box) return null;
2598
+ page = document.createElement("canvas");
2599
+ page.width = box.sw;
2600
+ page.height = box.sh;
2601
+ const ctx = page.getContext("2d");
2602
+ if (!ctx) return null;
2603
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2604
+ raw.width = 0;
2605
+ raw.height = 0;
2606
+ } else {
2607
+ page = raw;
2608
+ }
2609
+ frozen = {
2610
+ canvas: page,
2611
+ mode,
2612
+ viewportWidth: vw,
2613
+ viewportHeight: vh,
2614
+ takenAt: Date.now()
2615
+ };
2616
+ lastMode = mode;
2617
+ return frozen;
2584
2618
  } catch {
2585
2619
  declined = true;
2586
- teardown();
2587
- return false;
2620
+ return null;
2621
+ } finally {
2622
+ release();
2588
2623
  }
2589
2624
  }
2590
- async function grabExactRegion(rect) {
2591
- if (!track || track.readyState !== "live" || !video) return null;
2592
- if (mode === "surface") {
2593
- if (!mapping || environmentSignature() !== mappingSignature) {
2594
- if (!await calibrate()) return null;
2595
- }
2596
- }
2597
- await nextPresentedFrame(video);
2598
- let source;
2599
- let frameW;
2600
- let frameH;
2601
- let bitmap = null;
2602
- if (grabber) {
2603
- try {
2604
- bitmap = await grabber.grabFrame();
2605
- source = bitmap;
2606
- frameW = bitmap.width;
2607
- frameH = bitmap.height;
2608
- } catch {
2609
- source = video;
2610
- frameW = video.videoWidth;
2611
- frameH = video.videoHeight;
2612
- }
2613
- } else {
2614
- source = video;
2615
- frameW = video.videoWidth;
2616
- frameH = video.videoHeight;
2617
- }
2618
- try {
2619
- let sx;
2620
- let sy;
2621
- let sw;
2622
- let sh;
2623
- if (mode === "surface") {
2624
- if (!mapping) return null;
2625
- const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
2626
- if (!mapped) return null;
2627
- ({ sx, sy, sw, sh } = mapped);
2628
- } else {
2629
- if (!validateFrameMapping(frameW, frameH)) return null;
2630
- const scaleX = frameW / window.innerWidth;
2631
- const scaleY = frameH / window.innerHeight;
2632
- sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
2633
- sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
2634
- sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
2635
- sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
2636
- }
2637
- const canvas = document.createElement("canvas");
2638
- canvas.width = sw;
2639
- canvas.height = sh;
2640
- const ctx = canvas.getContext("2d");
2641
- if (!ctx) return null;
2642
- ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
2643
- return canvas;
2644
- } catch {
2625
+ function cropFrozenRegion(rect) {
2626
+ const f = frozen;
2627
+ if (!f || !f.canvas.width || !f.canvas.height) return null;
2628
+ if (!f.viewportWidth || !f.viewportHeight) return null;
2629
+ if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
2645
2630
  return null;
2646
- } finally {
2647
- if (bitmap && typeof bitmap.close === "function") bitmap.close();
2648
2631
  }
2632
+ const scaleX = f.canvas.width / f.viewportWidth;
2633
+ const scaleY = f.canvas.height / f.viewportHeight;
2634
+ const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
2635
+ const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
2636
+ const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
2637
+ const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
2638
+ if (sw < 1 || sh < 1) return null;
2639
+ const out = document.createElement("canvas");
2640
+ out.width = sw;
2641
+ out.height = sh;
2642
+ const ctx = out.getContext("2d");
2643
+ if (!ctx) return null;
2644
+ ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
2645
+ return out;
2649
2646
  }
2650
2647
 
2651
2648
  // src/lib/cssColors.ts
@@ -2884,20 +2881,6 @@ function shotExtension(blob) {
2884
2881
  if (type === "image/jpeg") return "jpg";
2885
2882
  return "png";
2886
2883
  }
2887
- async function withOverlayHidden(fn) {
2888
- const hosts = Array.from(
2889
- document.querySelectorAll("body > [data-qa-overlay]")
2890
- );
2891
- const previous = hosts.map((el) => el.style.visibility);
2892
- for (const el of hosts) el.style.visibility = "hidden";
2893
- try {
2894
- return await fn();
2895
- } finally {
2896
- hosts.forEach((el, i) => {
2897
- el.style.visibility = previous[i];
2898
- });
2899
- }
2900
- }
2901
2884
  function markStuckElements() {
2902
2885
  let all;
2903
2886
  try {
@@ -3112,17 +3095,17 @@ async function captureRegion(rect, scroll, prefer = "auto") {
3112
3095
  if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
3113
3096
  const sx = scroll?.x ?? window.scrollX;
3114
3097
  const sy = scroll?.y ?? window.scrollY;
3115
- if (prefer !== "dom" && getExactCaptureStatus() === "live") {
3098
+ if (prefer !== "dom" && getFrozenFrame()) {
3116
3099
  const visible = intersectViewport(rect);
3117
3100
  if (!visible) return { status: "empty" };
3118
3101
  try {
3119
- const canvas = await withOverlayHidden(() => grabExactRegion(visible));
3102
+ const canvas = cropFrozenRegion(visible);
3120
3103
  if (canvas) {
3121
3104
  const blob = await encodeShot(canvas);
3122
3105
  if (blob) return { status: "ok", blob, engine: "exact" };
3123
3106
  }
3124
3107
  } catch (err) {
3125
- console.warn("[QA] exact capture failed, falling back to DOM render:", err);
3108
+ console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
3126
3109
  }
3127
3110
  if (prefer === "exact") return { status: "failed" };
3128
3111
  }
@@ -4211,6 +4194,14 @@ var DENSE_NOTES_KEY = "denseNotes";
4211
4194
  var PANEL_OPEN_KEY = "panelOpen";
4212
4195
  var ACTIVE_TAB_KEY = "activeTab";
4213
4196
  var WALK_KEY = "walk";
4197
+ var SOFT_NAV_PROOF_MS = 700;
4198
+ var SIGNATURE_SEP = String.fromCharCode(0);
4199
+ function pageSignature() {
4200
+ if (typeof document === "undefined" || !document.body) return "";
4201
+ const body = document.body;
4202
+ const text = body.textContent || "";
4203
+ return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
4204
+ }
4214
4205
  var GUIDE_SKIPPED_KEY = "guideSkipped";
4215
4206
  var NOTICE_QUEUE_CAP = 3;
4216
4207
  var NOTICE_DURATION_INFO = 4e3;
@@ -4267,8 +4258,12 @@ function QaProvider({
4267
4258
  const [loginsUsed, setLoginsUsed] = useState(
4268
4259
  () => new Set(storage.getJSON(LOGIN_KEY, []))
4269
4260
  );
4270
- const [exactStatus, setExactStatus] = useState(() => getExactCaptureStatus());
4261
+ const [exactStatus, setExactStatus] = useState(() => {
4262
+ if (storage.getItem(EXACT_SHOTS_KEY) === "1") armExactCapture();
4263
+ return getExactCaptureStatus();
4264
+ });
4271
4265
  const exactSupported = isExactCaptureSupported();
4266
+ const [frozenAt, setFrozenAt] = useState(null);
4272
4267
  const [syncState, setSyncState] = useState(() => getFsSyncState());
4273
4268
  const [syncTick, setSyncTick] = useState(0);
4274
4269
  const [lastCampaign, setLastCampaign] = useState(
@@ -4769,20 +4764,26 @@ function QaProvider({
4769
4764
  flushPendingDeletes();
4770
4765
  for (const timer of noticeTimers.current.values()) clearTimeout(timer);
4771
4766
  noticeTimers.current.clear();
4772
- stopExactCapture();
4767
+ releaseFrozenFrame();
4773
4768
  };
4774
4769
  }, [flushPendingDeletes]);
4775
4770
  const startCapture = useCallback((prefill) => {
4776
4771
  setIsOpen(false);
4777
4772
  setCapturePrefill(prefill ?? "");
4778
4773
  setCaptureActive(true);
4779
- if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported() && getExactCaptureStatus() !== "live") {
4774
+ setFrozenAt(null);
4775
+ if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported()) {
4780
4776
  resetExactCaptureDecline();
4781
- void startExactCapture().then(() => setExactStatus(getExactCaptureStatus()));
4777
+ void freezeViewport().then((frame) => {
4778
+ setFrozenAt(frame?.takenAt ?? null);
4779
+ setExactStatus(getExactCaptureStatus());
4780
+ });
4782
4781
  }
4783
4782
  }, [storage]);
4784
4783
  const endCapture = useCallback((reopen = true) => {
4785
4784
  setCaptureActive(false);
4785
+ releaseFrozenFrame();
4786
+ setFrozenAt(null);
4786
4787
  if (reopen) setIsOpen(true);
4787
4788
  }, []);
4788
4789
  const toggleGuide = useCallback((key) => {
@@ -4823,16 +4824,32 @@ function QaProvider({
4823
4824
  exportZipRef.current = exportZipFn;
4824
4825
  }, [exportZipFn]);
4825
4826
  const enableExactShots = useCallback(async () => {
4826
- resetExactCaptureDecline();
4827
- const ok = await startExactCapture();
4827
+ const ok = armExactCapture();
4828
4828
  setExactStatus(getExactCaptureStatus());
4829
4829
  storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
4830
4830
  notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
4831
4831
  return ok;
4832
4832
  }, [storage, notify, t]);
4833
+ const photographNow = useCallback(async () => {
4834
+ if (!armExactCapture()) {
4835
+ setExactStatus(getExactCaptureStatus());
4836
+ notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
4837
+ return false;
4838
+ }
4839
+ storage.setItem(EXACT_SHOTS_KEY, "1");
4840
+ const frame = await freezeViewport();
4841
+ setFrozenAt(frame?.takenAt ?? null);
4842
+ setExactStatus(getExactCaptureStatus());
4843
+ notify(frame ? t("exact_on") : t("exact_declined"), {
4844
+ tone: frame ? "success" : "info",
4845
+ id: "exact-shots"
4846
+ });
4847
+ return !!frame;
4848
+ }, [storage, notify, t]);
4833
4849
  const disableExactShots = useCallback(() => {
4834
- stopExactCapture();
4850
+ disarmExactCapture();
4835
4851
  storage.setItem(EXACT_SHOTS_KEY, "0");
4852
+ setFrozenAt(null);
4836
4853
  setExactStatus(getExactCaptureStatus());
4837
4854
  }, [storage]);
4838
4855
  const rememberCampaign = useCallback((next) => {
@@ -5247,16 +5264,23 @@ function QaProvider({
5247
5264
  const walkNavigate = useCallback((path, hard = false) => {
5248
5265
  if (typeof window === "undefined" || !path) return;
5249
5266
  const target = path.startsWith("/") ? path : `/${path}`;
5250
- if (hard) {
5267
+ if (hard || target === window.location.pathname) {
5251
5268
  window.location.assign(target);
5252
5269
  return;
5253
5270
  }
5271
+ const before = pageSignature();
5254
5272
  try {
5255
- window.history.pushState({}, "", target);
5256
- window.dispatchEvent(new PopStateEvent("popstate"));
5273
+ window.history.pushState(window.history.state, "", target);
5274
+ window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state }));
5257
5275
  } catch {
5258
5276
  window.location.assign(target);
5277
+ return;
5259
5278
  }
5279
+ window.setTimeout(() => {
5280
+ if (window.location.pathname !== target) return;
5281
+ if (pageSignature() !== before) return;
5282
+ window.location.assign(target);
5283
+ }, SOFT_NAV_PROOF_MS);
5260
5284
  }, []);
5261
5285
  useEffect(() => {
5262
5286
  if (typeof window === "undefined") return;
@@ -5344,8 +5368,9 @@ function QaProvider({
5344
5368
  gradeStep,
5345
5369
  evidenceByStep,
5346
5370
  // v0.4 — screenshots
5347
- exactShots: { supported: exactSupported, status: exactStatus },
5371
+ exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
5348
5372
  enableExactShots,
5373
+ photographNow,
5349
5374
  disableExactShots,
5350
5375
  // v0.4 — folder sync. syncTick is read here purely so this object is
5351
5376
  // rebuilt when fsSync's module state changes (path/campaign live outside
@@ -6139,11 +6164,17 @@ function flashLocate(target) {
6139
6164
  paint(rect);
6140
6165
  }
6141
6166
  }
6142
- function LocationReveal({ target }) {
6143
- const { t } = useQa();
6167
+ function LocationReveal({
6168
+ target,
6169
+ route
6170
+ }) {
6171
+ const { t, walkNavigate } = useQa();
6144
6172
  const [open, setOpen] = useState(false);
6145
6173
  if (!target) return null;
6146
6174
  const r = target.rect;
6175
+ const notePath = (route || "").split("?")[0].split("#")[0];
6176
+ const here = typeof window === "undefined" ? "" : window.location.pathname;
6177
+ const elsewhere = !!notePath && notePath !== here;
6147
6178
  return /* @__PURE__ */ jsxs("div", { className: "qa-rounded-lg qa-border qa-border-subtle qa-bg-2", children: [
6148
6179
  /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1.5 qa-px-2 qa-py-1.5 qa-text-11", children: [
6149
6180
  /* @__PURE__ */ jsx(Icon, { name: "CheckCircle2", size: 14, className: "qa-text-success" }),
@@ -6207,7 +6238,21 @@ function LocationReveal({ target }) {
6207
6238
  "\xD7",
6208
6239
  Math.round(r.height)
6209
6240
  ] }),
6210
- /* @__PURE__ */ jsxs(
6241
+ elsewhere ? /* @__PURE__ */ jsxs(Fragment, { children: [
6242
+ /* @__PURE__ */ jsx("p", { className: "qa-mt-1 qa-mb-1 qa-m-0 qa-text-lo", children: t("loc_other_page", { path: notePath }) }),
6243
+ /* @__PURE__ */ jsxs(
6244
+ "button",
6245
+ {
6246
+ onClick: () => walkNavigate(notePath),
6247
+ className: "qa-inline-flex qa-items-center qa-gap-1 qa-rounded-md qa-px-2 qa-py-1 qa-font-medium qa-tap qa-bg-accent",
6248
+ style: { border: "none", cursor: "pointer" },
6249
+ children: [
6250
+ /* @__PURE__ */ jsx(Icon, { name: "MapPinned", size: 12 }),
6251
+ t("loc_go_there", { path: notePath })
6252
+ ]
6253
+ }
6254
+ )
6255
+ ] }) : /* @__PURE__ */ jsxs(
6211
6256
  "button",
6212
6257
  {
6213
6258
  onClick: () => flashLocate(target),
@@ -6925,7 +6970,7 @@ function NoteItem({
6925
6970
  /* @__PURE__ */ jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
6926
6971
  /* @__PURE__ */ jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
6927
6972
  ] }),
6928
- note.target && /* @__PURE__ */ jsx(LocationReveal, { target: note.target }),
6973
+ note.target && /* @__PURE__ */ jsx(LocationReveal, { target: note.target, route: note.route }),
6929
6974
  contextEventCount > 0 && /* @__PURE__ */ jsx("div", { children: t("context_attached", { n: contextEventCount }) }),
6930
6975
  stepCount > 0 && /* @__PURE__ */ jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
6931
6976
  /* @__PURE__ */ jsx(Icon, { name: "ClipboardList", size: 12, className: "qa-shrink-0" }),
@@ -8669,6 +8714,20 @@ var RISK_COLOR = {
8669
8714
  amber: "var(--qa-warn)",
8670
8715
  green: "var(--qa-success)"
8671
8716
  };
8717
+ function useCurrentPath() {
8718
+ const [path, setPath] = useState(() => typeof window === "undefined" ? "" : window.location.pathname);
8719
+ useEffect(() => {
8720
+ if (typeof window === "undefined") return;
8721
+ const sync = () => setPath(window.location.pathname);
8722
+ window.addEventListener("popstate", sync);
8723
+ window.addEventListener("hashchange", sync);
8724
+ return () => {
8725
+ window.removeEventListener("popstate", sync);
8726
+ window.removeEventListener("hashchange", sync);
8727
+ };
8728
+ }, []);
8729
+ return path;
8730
+ }
8672
8731
  function WalkHud() {
8673
8732
  const {
8674
8733
  dir,
@@ -8688,10 +8747,13 @@ function WalkHud() {
8688
8747
  startCapture,
8689
8748
  updateNote,
8690
8749
  retestNote,
8691
- notes
8750
+ notes,
8751
+ notesLoading
8692
8752
  } = useQa();
8693
8753
  const index = Math.min(walk.index, Math.max(0, walkStops.length - 1));
8694
8754
  const stop = walkStops[index];
8755
+ const currentPath = useCurrentPath();
8756
+ const [going, setGoing] = useState(false);
8695
8757
  const backIcon = dir === "rtl" ? "ChevronRight" : "ChevronLeft";
8696
8758
  const nextIcon = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
8697
8759
  useEffect(() => {
@@ -8705,8 +8767,9 @@ function WalkHud() {
8705
8767
  if (latest?.target) flashLocate(latest.target);
8706
8768
  }, [index, walk.source]);
8707
8769
  useEffect(() => {
8770
+ if (notesLoading) return;
8708
8771
  if (walk.active && walkStops.length === 0) exitWalk();
8709
- }, [walk.active, walkStops.length, exitWalk]);
8772
+ }, [walk.active, walkStops.length, exitWalk, notesLoading]);
8710
8773
  if (!walk.active || !stop) return null;
8711
8774
  const atLast = index >= walkStops.length - 1;
8712
8775
  let title;
@@ -8867,7 +8930,7 @@ function WalkHud() {
8867
8930
  )
8868
8931
  ] });
8869
8932
  }
8870
- const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
8933
+ const onThisPage = stop.path === ANY_PAGE || stop.path === currentPath;
8871
8934
  return /* @__PURE__ */ jsxs(
8872
8935
  "div",
8873
8936
  {
@@ -8903,12 +8966,24 @@ function WalkHud() {
8903
8966
  "button",
8904
8967
  {
8905
8968
  type: "button",
8906
- onClick: () => walkNavigate(stop.path),
8969
+ disabled: going,
8970
+ onClick: () => {
8971
+ setGoing(true);
8972
+ walkNavigate(stop.path);
8973
+ window.setTimeout(() => setGoing(false), 900);
8974
+ },
8907
8975
  className: "qa-tap qa-inline-flex qa-flex-1 qa-items-center qa-justify-center qa-gap-1.5 qa-rounded-lg qa-bg-accent qa-px-2 qa-py-1.5 qa-text-11 qa-font-semibold",
8908
- style: { border: "none", cursor: "pointer" },
8976
+ style: { border: "none", cursor: going ? "default" : "pointer" },
8909
8977
  children: [
8910
- /* @__PURE__ */ jsx(Icon, { name: "MapPinned", size: 13 }),
8911
- t("walk_take_me", { path: stop.path })
8978
+ /* @__PURE__ */ jsx(
8979
+ Icon,
8980
+ {
8981
+ name: going ? "Loader2" : "MapPinned",
8982
+ size: 13,
8983
+ className: going ? "qa-animate-spin" : void 0
8984
+ }
8985
+ ),
8986
+ going ? t("walk_taking_you") : t("walk_take_me", { path: stop.path })
8912
8987
  ]
8913
8988
  }
8914
8989
  ),
@@ -9128,7 +9203,7 @@ function CaptureMode() {
9128
9203
  compactCapture,
9129
9204
  setCompactCapture,
9130
9205
  exactShots,
9131
- enableExactShots,
9206
+ photographNow,
9132
9207
  capturePrefill
9133
9208
  } = useQa();
9134
9209
  const coarse = useCoarsePointer();
@@ -9142,6 +9217,7 @@ function CaptureMode() {
9142
9217
  const [candidate, setCandidate] = useState(null);
9143
9218
  const [regionMode, setRegionMode] = useState(false);
9144
9219
  const [shot, setShot] = useState(null);
9220
+ const stillRef = useRef(null);
9145
9221
  const [shotEngine, setShotEngine] = useState(null);
9146
9222
  const [shotUrl, setShotUrl] = useState(null);
9147
9223
  const [capturing, setCapturing] = useState(false);
@@ -9434,6 +9510,18 @@ function CaptureMode() {
9434
9510
  useEffect(() => () => {
9435
9511
  unlockPageScroll();
9436
9512
  }, []);
9513
+ useEffect(() => {
9514
+ const host = stillRef.current;
9515
+ const frame = getFrozenFrame();
9516
+ if (!host || !frame) return;
9517
+ const canvas = frame.canvas;
9518
+ canvas.style.cssText = "display:block;width:100%;height:100%;";
9519
+ host.appendChild(canvas);
9520
+ return () => {
9521
+ if (canvas.parentNode === host) host.removeChild(canvas);
9522
+ canvas.removeAttribute("style");
9523
+ };
9524
+ }, [exactShots.frozenAt]);
9437
9525
  const save = async () => {
9438
9526
  if (!selection || !description.trim()) return;
9439
9527
  const target = {
@@ -9492,6 +9580,16 @@ function CaptureMode() {
9492
9580
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
9493
9581
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
9494
9582
  return /* @__PURE__ */ jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
9583
+ exactShots.frozenAt !== null && /* @__PURE__ */ jsx(
9584
+ "div",
9585
+ {
9586
+ ref: stillRef,
9587
+ "aria-hidden": "true",
9588
+ "data-qa-capture-still": "true",
9589
+ className: "qa-fixed qa-inset-0 qa-z-10089",
9590
+ style: { pointerEvents: "none", overflow: "hidden" }
9591
+ }
9592
+ ),
9495
9593
  /* @__PURE__ */ jsx(
9496
9594
  "div",
9497
9595
  {
@@ -9550,7 +9648,7 @@ function CaptureMode() {
9550
9648
  ) : /* @__PURE__ */ jsxs(
9551
9649
  "button",
9552
9650
  {
9553
- onClick: () => void enableExactShots(),
9651
+ onClick: () => void photographNow(),
9554
9652
  title: t("exact_hint"),
9555
9653
  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",
9556
9654
  style: { background: "transparent", cursor: "pointer" },
@@ -9994,8 +10092,8 @@ function CaptureMode() {
9994
10092
  {
9995
10093
  type: "button",
9996
10094
  onClick: () => void (async () => {
9997
- const granted = await enableExactShots();
9998
- if (granted && selection) void runCapture(selection.rect);
10095
+ const shot2 = await photographNow();
10096
+ if (shot2 && selection) void runCapture(selection.rect);
9999
10097
  })(),
10000
10098
  title: t("exact_hint"),
10001
10099
  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",
@@ -10319,5 +10417,5 @@ function Qapture({ config }) {
10319
10417
  }
10320
10418
 
10321
10419
  export { Qapture, deleteQaDatabase, initQaStudio };
10322
- //# sourceMappingURL=chunk-2JX7YU2P.js.map
10323
- //# sourceMappingURL=chunk-2JX7YU2P.js.map
10420
+ //# sourceMappingURL=chunk-RJV6FTIB.js.map
10421
+ //# sourceMappingURL=chunk-RJV6FTIB.js.map