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.
@@ -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 */
@@ -1741,6 +1744,8 @@ var STR = {
1741
1744
  loc_show: "Show captured location",
1742
1745
  loc_hide: "Hide location",
1743
1746
  loc_locate: "Locate on page",
1747
+ loc_other_page: "This one was filed on {path}, not the page you are on.",
1748
+ loc_go_there: "Go to {path}",
1744
1749
  journey_title: "Testing journey",
1745
1750
  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.",
1746
1751
  export_name_title: "Name your export",
@@ -1781,7 +1786,7 @@ var STR = {
1781
1786
  no_evidence: "ticked, no capture",
1782
1787
  expected_label: "Expected",
1783
1788
  exact_label: "Pixel-exact shots",
1784
- 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.",
1789
+ 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.",
1785
1790
  redraw_notice: "This is a redraw of the page, not a real photo.",
1786
1791
  redraw_action: "Use real screenshots",
1787
1792
  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.",
@@ -1790,7 +1795,7 @@ var STR = {
1790
1795
  exact_off: "Back to redrawn screenshots",
1791
1796
  exact_declined: "Staying on redrawn screenshots",
1792
1797
  exact_turn_on: "Turn on",
1793
- exact_unsupported: "Needs Chrome, Edge or Brave on desktop",
1798
+ exact_unsupported: "Needs a desktop browser \u2014 phones cannot photograph the screen",
1794
1799
  sync_title: "Save to a folder",
1795
1800
  sync_hint: "Every note is written to your disk the moment you save it \u2014 nothing is lost if this browser dies.",
1796
1801
  sync_choose: "Choose folder",
@@ -1896,6 +1901,7 @@ var STR = {
1896
1901
  capture_screen: "Whole screen",
1897
1902
  walk_title: "Walkthrough",
1898
1903
  walk_take_me: "Take me to {path}",
1904
+ walk_taking_you: "Taking you there\u2026",
1899
1905
  walk_reload_hint: "Reload the page instead (use this if the app didn't move)",
1900
1906
  walk_note_title: "Point {n}",
1901
1907
  walk_still_broken: "Still broken",
@@ -1946,6 +1952,8 @@ var STR = {
1946
1952
  loc_show: "\u0625\u0638\u0647\u0627\u0631 \u0627\u0644\u0645\u0648\u0642\u0639 \u0627\u0644\u0645\u064F\u0644\u062A\u0642\u064E\u0637",
1947
1953
  loc_hide: "\u0625\u062E\u0641\u0627\u0621 \u0627\u0644\u0645\u0648\u0642\u0639",
1948
1954
  loc_locate: "\u0625\u0638\u0647\u0627\u0631 \u0639\u0644\u0649 \u0627\u0644\u0635\u0641\u062D\u0629",
1955
+ 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.",
1956
+ loc_go_there: "\u0627\u0630\u0647\u0628 \u0625\u0644\u0649 {path}",
1949
1957
  journey_title: "\u0631\u062D\u0644\u0629 \u0627\u0644\u0627\u062E\u062A\u0628\u0627\u0631",
1950
1958
  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.",
1951
1959
  export_name_title: "\u0633\u0645\u0650\u0651 \u0645\u0644\u0641 \u0627\u0644\u062A\u0635\u062F\u064A\u0631",
@@ -1986,7 +1994,7 @@ var STR = {
1986
1994
  no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
1987
1995
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1988
1996
  exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
1989
- 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.",
1997
+ 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.",
1990
1998
  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.",
1991
1999
  redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1992
2000
  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.",
@@ -1995,7 +2003,7 @@ var STR = {
1995
2003
  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",
1996
2004
  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",
1997
2005
  exact_turn_on: "\u062A\u0641\u0639\u064A\u0644",
1998
- 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",
2006
+ 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",
1999
2007
  sync_title: "\u0627\u0644\u062D\u0641\u0638 \u0641\u064A \u0645\u062C\u0644\u062F",
2000
2008
  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.",
2001
2009
  sync_choose: "\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u062C\u0644\u062F",
@@ -2101,6 +2109,7 @@ var STR = {
2101
2109
  capture_screen: "\u0627\u0644\u0634\u0627\u0634\u0629 \u0643\u0627\u0645\u0644\u0629",
2102
2110
  walk_title: "\u0627\u0644\u062C\u0648\u0644\u0629",
2103
2111
  walk_take_me: "\u062E\u0630\u0646\u064A \u0625\u0644\u0649 {path}",
2112
+ walk_taking_you: "\u062C\u0627\u0631\u064D \u0646\u0642\u0644\u0643\u2026",
2104
2113
  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)",
2105
2114
  walk_note_title: "\u0627\u0644\u0646\u0642\u0637\u0629 {n}",
2106
2115
  walk_still_broken: "\u0645\u0627 \u0632\u0627\u0644 \u0645\u0639\u0637\u0644\u064B\u0627",
@@ -2299,7 +2308,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2299
2308
  if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
2300
2309
  const originX = tl.x - centres.tl.x * scaleX;
2301
2310
  const originY = tl.y - centres.tl.y * scaleY;
2302
- const mapping2 = { scaleX, scaleY, originX, originY };
2311
+ const mapping = { scaleX, scaleY, originX, originY };
2303
2312
  const farX = originX + vw * scaleX;
2304
2313
  const farY = originY + vh * scaleY;
2305
2314
  const slack = Math.max(4, frameW * 0.01);
@@ -2318,7 +2327,7 @@ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2318
2327
  verified++;
2319
2328
  }
2320
2329
  if (verified === 0) return null;
2321
- return mapping2;
2330
+ return mapping;
2322
2331
  }
2323
2332
  function mapRectToFrame(rect, m, frameW, frameH) {
2324
2333
  const x0 = m.originX + rect.left * m.scaleX;
@@ -2330,73 +2339,49 @@ function mapRectToFrame(rect, m, frameW, frameH) {
2330
2339
  if (sw < 1 || sh < 1) return null;
2331
2340
  return { sx, sy, sw, sh };
2332
2341
  }
2333
- function environmentSignature() {
2334
- if (typeof window === "undefined") return "";
2335
- return [
2336
- window.innerWidth,
2337
- window.innerHeight,
2338
- window.outerWidth,
2339
- window.outerHeight,
2340
- window.screenX,
2341
- window.screenY,
2342
- Math.round((window.devicePixelRatio || 1) * 100)
2343
- ].join("x");
2344
- }
2345
2342
 
2346
2343
  // src/lib/screenCapture.ts
2347
2344
  var ASPECT_TOLERANCE = 0.08;
2348
2345
  var CALIBRATION_SETTLE_MS = 220;
2349
2346
  var FRESH_FRAME_TIMEOUT_MS = 500;
2350
2347
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2351
- var stream = null;
2352
- var track = null;
2353
- var video = null;
2354
- var grabber = null;
2355
- var declined = false;
2356
2348
  function isExactCaptureSupported() {
2357
2349
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2358
2350
  const md = navigator.mediaDevices;
2359
2351
  return !!md && typeof md.getDisplayMedia === "function";
2360
2352
  }
2361
- var mode = "tab";
2362
- var mapping = null;
2363
- var mappingSignature = "";
2353
+ var armed = false;
2354
+ var declined = false;
2355
+ var frozen = null;
2356
+ var lastMode = null;
2357
+ function armExactCapture() {
2358
+ if (!isExactCaptureSupported()) return false;
2359
+ armed = true;
2360
+ declined = false;
2361
+ return true;
2362
+ }
2363
+ function disarmExactCapture() {
2364
+ armed = false;
2365
+ releaseFrozenFrame();
2366
+ }
2367
+ function resetExactCaptureDecline() {
2368
+ declined = false;
2369
+ }
2364
2370
  function getExactCaptureStatus() {
2365
2371
  if (!isExactCaptureSupported()) return "unsupported";
2366
- if (track && track.readyState === "live") return "live";
2372
+ if (armed) return "live";
2367
2373
  if (declined) return "declined";
2368
2374
  return "idle";
2369
2375
  }
2370
- function teardown() {
2371
- if (video) {
2372
- try {
2373
- video.pause();
2374
- } catch {
2375
- }
2376
- video.srcObject = null;
2377
- video.remove();
2378
- }
2379
- if (stream) {
2380
- for (const t of stream.getTracks()) {
2381
- try {
2382
- t.stop();
2383
- } catch {
2384
- }
2385
- }
2386
- }
2387
- video = null;
2388
- stream = null;
2389
- track = null;
2390
- grabber = null;
2391
- mode = "tab";
2392
- mapping = null;
2393
- mappingSignature = "";
2394
- }
2395
- function stopExactCapture() {
2396
- teardown();
2376
+ function getFrozenFrame() {
2377
+ return frozen;
2397
2378
  }
2398
- function resetExactCaptureDecline() {
2399
- declined = false;
2379
+ function releaseFrozenFrame() {
2380
+ if (frozen) {
2381
+ frozen.canvas.width = 0;
2382
+ frozen.canvas.height = 0;
2383
+ }
2384
+ frozen = null;
2400
2385
  }
2401
2386
  function waitFor(test, timeoutMs) {
2402
2387
  if (test()) return Promise.resolve(true);
@@ -2436,17 +2421,28 @@ function nextPresentedFrame(el) {
2436
2421
  });
2437
2422
  });
2438
2423
  }
2439
- function validateFrameMapping(frameW, frameH) {
2424
+ function looksLikeViewport(frameW, frameH) {
2440
2425
  if (!frameW || !frameH) return false;
2441
2426
  const vw = window.innerWidth;
2442
2427
  const vh = window.innerHeight;
2443
2428
  if (!vw || !vh) return false;
2444
- const frameAspect = frameW / frameH;
2445
- const viewAspect = vw / vh;
2446
- return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
2429
+ return Math.abs(frameW / frameH - vw / vh) / (vw / vh) <= ASPECT_TOLERANCE;
2430
+ }
2431
+ async function withOverlayHidden(fn) {
2432
+ const hosts = Array.from(
2433
+ document.querySelectorAll("body > [data-qa-overlay]")
2434
+ );
2435
+ const previous = hosts.map((el) => el.style.visibility);
2436
+ for (const el of hosts) el.style.visibility = "hidden";
2437
+ try {
2438
+ return await fn();
2439
+ } finally {
2440
+ hosts.forEach((el, i) => {
2441
+ el.style.visibility = previous[i];
2442
+ });
2443
+ }
2447
2444
  }
2448
- async function grabFrameCanvas() {
2449
- if (!video) return null;
2445
+ async function grabFrameCanvas(video, grabber) {
2450
2446
  await nextPresentedFrame(video);
2451
2447
  let source = video;
2452
2448
  let w = video.videoWidth;
@@ -2477,11 +2473,10 @@ async function grabFrameCanvas() {
2477
2473
  bitmap?.close?.();
2478
2474
  return c;
2479
2475
  }
2480
- async function calibrate() {
2481
- if (!video) return false;
2476
+ async function calibrate(video, grabber) {
2482
2477
  const vw = window.innerWidth;
2483
2478
  const vh = window.innerHeight;
2484
- if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
2479
+ if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return null;
2485
2480
  const card = document.createElement("div");
2486
2481
  card.setAttribute("data-qa-calibration", "true");
2487
2482
  card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
@@ -2495,10 +2490,10 @@ async function calibrate() {
2495
2490
  document.body.appendChild(card);
2496
2491
  try {
2497
2492
  await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2498
- const frame = await grabFrameCanvas();
2499
- if (!frame) return false;
2493
+ const frame = await grabFrameCanvas(video, grabber);
2494
+ if (!frame) return null;
2500
2495
  const ctx = frame.getContext("2d", { willReadFrequently: true });
2501
- if (!ctx) return false;
2496
+ if (!ctx) return null;
2502
2497
  const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
2503
2498
  const found = deriveMapping(
2504
2499
  findMarkerCandidates(data, frame.width, frame.height),
@@ -2507,20 +2502,40 @@ async function calibrate() {
2507
2502
  frame.width,
2508
2503
  frame.height
2509
2504
  );
2510
- if (!found) return false;
2511
- mapping = found;
2512
- mappingSignature = environmentSignature();
2513
- return true;
2505
+ frame.width = 0;
2506
+ frame.height = 0;
2507
+ return found;
2514
2508
  } catch {
2515
- return false;
2509
+ return null;
2516
2510
  } finally {
2517
2511
  card.remove();
2518
2512
  }
2519
2513
  }
2520
- async function startExactCapture() {
2521
- if (!isExactCaptureSupported()) return false;
2522
- if (track && track.readyState === "live" && video) return true;
2523
- if (stream) teardown();
2514
+ async function freezeViewport() {
2515
+ if (!isExactCaptureSupported()) return null;
2516
+ releaseFrozenFrame();
2517
+ let stream = null;
2518
+ let el = null;
2519
+ const release = () => {
2520
+ if (el) {
2521
+ try {
2522
+ el.pause();
2523
+ } catch {
2524
+ }
2525
+ el.srcObject = null;
2526
+ el.remove();
2527
+ el = null;
2528
+ }
2529
+ if (stream) {
2530
+ for (const t of stream.getTracks()) {
2531
+ try {
2532
+ t.stop();
2533
+ } catch {
2534
+ }
2535
+ }
2536
+ stream = null;
2537
+ }
2538
+ };
2524
2539
  try {
2525
2540
  const dpr = Math.min(window.devicePixelRatio || 1, 2);
2526
2541
  const constraints = {
@@ -2538,121 +2553,103 @@ async function startExactCapture() {
2538
2553
  surfaceSwitching: "exclude",
2539
2554
  systemAudio: "exclude"
2540
2555
  };
2541
- const s = await navigator.mediaDevices.getDisplayMedia(constraints);
2542
- const t = s.getVideoTracks()[0];
2543
- if (!t) {
2544
- for (const x of s.getTracks()) x.stop();
2556
+ stream = await navigator.mediaDevices.getDisplayMedia(constraints);
2557
+ const track = stream.getVideoTracks()[0];
2558
+ if (!track) {
2545
2559
  declined = true;
2546
- return false;
2560
+ return null;
2547
2561
  }
2548
- const settings = t.getSettings();
2562
+ const settings = track.getSettings();
2549
2563
  const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
2550
- const el = document.createElement("video");
2564
+ el = document.createElement("video");
2551
2565
  el.setAttribute("data-qa-overlay", "true");
2552
2566
  el.muted = true;
2553
2567
  el.playsInline = true;
2554
2568
  el.style.cssText = "position:fixed;top:0;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;";
2555
- el.srcObject = s;
2569
+ el.srcObject = stream;
2556
2570
  document.body.appendChild(el);
2557
2571
  try {
2558
2572
  await el.play();
2559
2573
  } catch {
2560
2574
  }
2561
- const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
2562
- const giveUp = () => {
2563
- el.remove();
2564
- for (const x of s.getTracks()) x.stop();
2575
+ const video = el;
2576
+ const ready = await waitFor(
2577
+ () => video.videoWidth > 0 && video.videoHeight > 0,
2578
+ VIDEO_READY_TIMEOUT_MS
2579
+ );
2580
+ if (!ready) {
2565
2581
  declined = true;
2566
- return false;
2567
- };
2568
- if (!ready) return giveUp();
2569
- const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
2570
- stream = s;
2571
- track = t;
2572
- video = el;
2582
+ return null;
2583
+ }
2573
2584
  const IC = window.ImageCapture;
2574
- grabber = IC ? new IC(t) : null;
2575
- if (isDirect) {
2576
- mode = "tab";
2577
- mapping = null;
2578
- mappingSignature = "";
2579
- } else {
2580
- mode = "surface";
2581
- if (!await calibrate()) {
2582
- teardown();
2583
- declined = true;
2584
- return false;
2585
- }
2585
+ const grabber = IC ? new IC(track) : null;
2586
+ const vw = window.innerWidth;
2587
+ const vh = window.innerHeight;
2588
+ const mode = sharedTab && looksLikeViewport(video.videoWidth, video.videoHeight) ? "tab" : "surface";
2589
+ const mapping = mode === "surface" ? await calibrate(video, grabber) : null;
2590
+ if (mode === "surface" && !mapping) {
2591
+ declined = true;
2592
+ return null;
2586
2593
  }
2587
- t.addEventListener("ended", () => {
2588
- teardown();
2589
- });
2590
- return true;
2594
+ const raw = await withOverlayHidden(() => grabFrameCanvas(video, grabber));
2595
+ if (!raw) return null;
2596
+ let page;
2597
+ if (mode === "surface" && mapping) {
2598
+ const box = mapRectToFrame(
2599
+ { left: 0, top: 0, width: vw, height: vh },
2600
+ mapping,
2601
+ raw.width,
2602
+ raw.height
2603
+ );
2604
+ if (!box) return null;
2605
+ page = document.createElement("canvas");
2606
+ page.width = box.sw;
2607
+ page.height = box.sh;
2608
+ const ctx = page.getContext("2d");
2609
+ if (!ctx) return null;
2610
+ ctx.drawImage(raw, box.sx, box.sy, box.sw, box.sh, 0, 0, box.sw, box.sh);
2611
+ raw.width = 0;
2612
+ raw.height = 0;
2613
+ } else {
2614
+ page = raw;
2615
+ }
2616
+ frozen = {
2617
+ canvas: page,
2618
+ mode,
2619
+ viewportWidth: vw,
2620
+ viewportHeight: vh,
2621
+ takenAt: Date.now()
2622
+ };
2623
+ lastMode = mode;
2624
+ return frozen;
2591
2625
  } catch {
2592
2626
  declined = true;
2593
- teardown();
2594
- return false;
2627
+ return null;
2628
+ } finally {
2629
+ release();
2595
2630
  }
2596
2631
  }
2597
- async function grabExactRegion(rect) {
2598
- if (!track || track.readyState !== "live" || !video) return null;
2599
- if (mode === "surface") {
2600
- if (!mapping || environmentSignature() !== mappingSignature) {
2601
- if (!await calibrate()) return null;
2602
- }
2603
- }
2604
- await nextPresentedFrame(video);
2605
- let source;
2606
- let frameW;
2607
- let frameH;
2608
- let bitmap = null;
2609
- if (grabber) {
2610
- try {
2611
- bitmap = await grabber.grabFrame();
2612
- source = bitmap;
2613
- frameW = bitmap.width;
2614
- frameH = bitmap.height;
2615
- } catch {
2616
- source = video;
2617
- frameW = video.videoWidth;
2618
- frameH = video.videoHeight;
2619
- }
2620
- } else {
2621
- source = video;
2622
- frameW = video.videoWidth;
2623
- frameH = video.videoHeight;
2624
- }
2625
- try {
2626
- let sx;
2627
- let sy;
2628
- let sw;
2629
- let sh;
2630
- if (mode === "surface") {
2631
- if (!mapping) return null;
2632
- const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
2633
- if (!mapped) return null;
2634
- ({ sx, sy, sw, sh } = mapped);
2635
- } else {
2636
- if (!validateFrameMapping(frameW, frameH)) return null;
2637
- const scaleX = frameW / window.innerWidth;
2638
- const scaleY = frameH / window.innerHeight;
2639
- sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
2640
- sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
2641
- sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
2642
- sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
2643
- }
2644
- const canvas = document.createElement("canvas");
2645
- canvas.width = sw;
2646
- canvas.height = sh;
2647
- const ctx = canvas.getContext("2d");
2648
- if (!ctx) return null;
2649
- ctx.drawImage(source, sx, sy, sw, sh, 0, 0, sw, sh);
2650
- return canvas;
2651
- } catch {
2632
+ function cropFrozenRegion(rect) {
2633
+ const f = frozen;
2634
+ if (!f || !f.canvas.width || !f.canvas.height) return null;
2635
+ if (!f.viewportWidth || !f.viewportHeight) return null;
2636
+ if (window.innerWidth !== f.viewportWidth || window.innerHeight !== f.viewportHeight) {
2652
2637
  return null;
2653
- } finally {
2654
- if (bitmap && typeof bitmap.close === "function") bitmap.close();
2655
2638
  }
2639
+ const scaleX = f.canvas.width / f.viewportWidth;
2640
+ const scaleY = f.canvas.height / f.viewportHeight;
2641
+ const sx = Math.max(0, Math.min(f.canvas.width, Math.round(rect.left * scaleX)));
2642
+ const sy = Math.max(0, Math.min(f.canvas.height, Math.round(rect.top * scaleY)));
2643
+ const sw = Math.min(f.canvas.width - sx, Math.round(rect.width * scaleX));
2644
+ const sh = Math.min(f.canvas.height - sy, Math.round(rect.height * scaleY));
2645
+ if (sw < 1 || sh < 1) return null;
2646
+ const out = document.createElement("canvas");
2647
+ out.width = sw;
2648
+ out.height = sh;
2649
+ const ctx = out.getContext("2d");
2650
+ if (!ctx) return null;
2651
+ ctx.drawImage(f.canvas, sx, sy, sw, sh, 0, 0, sw, sh);
2652
+ return out;
2656
2653
  }
2657
2654
 
2658
2655
  // src/lib/cssColors.ts
@@ -2891,20 +2888,6 @@ function shotExtension(blob) {
2891
2888
  if (type === "image/jpeg") return "jpg";
2892
2889
  return "png";
2893
2890
  }
2894
- async function withOverlayHidden(fn) {
2895
- const hosts = Array.from(
2896
- document.querySelectorAll("body > [data-qa-overlay]")
2897
- );
2898
- const previous = hosts.map((el) => el.style.visibility);
2899
- for (const el of hosts) el.style.visibility = "hidden";
2900
- try {
2901
- return await fn();
2902
- } finally {
2903
- hosts.forEach((el, i) => {
2904
- el.style.visibility = previous[i];
2905
- });
2906
- }
2907
- }
2908
2891
  function markStuckElements() {
2909
2892
  let all;
2910
2893
  try {
@@ -3119,17 +3102,17 @@ async function captureRegion(rect, scroll, prefer = "auto") {
3119
3102
  if (!rect || rect.width < 2 || rect.height < 2) return { status: "empty" };
3120
3103
  const sx = scroll?.x ?? window.scrollX;
3121
3104
  const sy = scroll?.y ?? window.scrollY;
3122
- if (prefer !== "dom" && getExactCaptureStatus() === "live") {
3105
+ if (prefer !== "dom" && getFrozenFrame()) {
3123
3106
  const visible = intersectViewport(rect);
3124
3107
  if (!visible) return { status: "empty" };
3125
3108
  try {
3126
- const canvas = await withOverlayHidden(() => grabExactRegion(visible));
3109
+ const canvas = cropFrozenRegion(visible);
3127
3110
  if (canvas) {
3128
3111
  const blob = await encodeShot(canvas);
3129
3112
  if (blob) return { status: "ok", blob, engine: "exact" };
3130
3113
  }
3131
3114
  } catch (err) {
3132
- console.warn("[QA] exact capture failed, falling back to DOM render:", err);
3115
+ console.warn("[QA] cropping the still failed, falling back to DOM render:", err);
3133
3116
  }
3134
3117
  if (prefer === "exact") return { status: "failed" };
3135
3118
  }
@@ -4218,6 +4201,14 @@ var DENSE_NOTES_KEY = "denseNotes";
4218
4201
  var PANEL_OPEN_KEY = "panelOpen";
4219
4202
  var ACTIVE_TAB_KEY = "activeTab";
4220
4203
  var WALK_KEY = "walk";
4204
+ var SOFT_NAV_PROOF_MS = 700;
4205
+ var SIGNATURE_SEP = String.fromCharCode(0);
4206
+ function pageSignature() {
4207
+ if (typeof document === "undefined" || !document.body) return "";
4208
+ const body = document.body;
4209
+ const text = body.textContent || "";
4210
+ return [document.title, body.childElementCount, text.length, text.slice(0, 200)].join(SIGNATURE_SEP);
4211
+ }
4221
4212
  var GUIDE_SKIPPED_KEY = "guideSkipped";
4222
4213
  var NOTICE_QUEUE_CAP = 3;
4223
4214
  var NOTICE_DURATION_INFO = 4e3;
@@ -4274,8 +4265,12 @@ function QaProvider({
4274
4265
  const [loginsUsed, setLoginsUsed] = React.useState(
4275
4266
  () => new Set(storage.getJSON(LOGIN_KEY, []))
4276
4267
  );
4277
- const [exactStatus, setExactStatus] = React.useState(() => getExactCaptureStatus());
4268
+ const [exactStatus, setExactStatus] = React.useState(() => {
4269
+ if (storage.getItem(EXACT_SHOTS_KEY) === "1") armExactCapture();
4270
+ return getExactCaptureStatus();
4271
+ });
4278
4272
  const exactSupported = isExactCaptureSupported();
4273
+ const [frozenAt, setFrozenAt] = React.useState(null);
4279
4274
  const [syncState, setSyncState] = React.useState(() => getFsSyncState());
4280
4275
  const [syncTick, setSyncTick] = React.useState(0);
4281
4276
  const [lastCampaign, setLastCampaign] = React.useState(
@@ -4776,20 +4771,26 @@ function QaProvider({
4776
4771
  flushPendingDeletes();
4777
4772
  for (const timer of noticeTimers.current.values()) clearTimeout(timer);
4778
4773
  noticeTimers.current.clear();
4779
- stopExactCapture();
4774
+ releaseFrozenFrame();
4780
4775
  };
4781
4776
  }, [flushPendingDeletes]);
4782
4777
  const startCapture = React.useCallback((prefill) => {
4783
4778
  setIsOpen(false);
4784
4779
  setCapturePrefill(prefill ?? "");
4785
4780
  setCaptureActive(true);
4786
- if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported() && getExactCaptureStatus() !== "live") {
4781
+ setFrozenAt(null);
4782
+ if (storage.getItem(EXACT_SHOTS_KEY) === "1" && isExactCaptureSupported()) {
4787
4783
  resetExactCaptureDecline();
4788
- void startExactCapture().then(() => setExactStatus(getExactCaptureStatus()));
4784
+ void freezeViewport().then((frame) => {
4785
+ setFrozenAt(frame?.takenAt ?? null);
4786
+ setExactStatus(getExactCaptureStatus());
4787
+ });
4789
4788
  }
4790
4789
  }, [storage]);
4791
4790
  const endCapture = React.useCallback((reopen = true) => {
4792
4791
  setCaptureActive(false);
4792
+ releaseFrozenFrame();
4793
+ setFrozenAt(null);
4793
4794
  if (reopen) setIsOpen(true);
4794
4795
  }, []);
4795
4796
  const toggleGuide = React.useCallback((key) => {
@@ -4830,16 +4831,32 @@ function QaProvider({
4830
4831
  exportZipRef.current = exportZipFn;
4831
4832
  }, [exportZipFn]);
4832
4833
  const enableExactShots = React.useCallback(async () => {
4833
- resetExactCaptureDecline();
4834
- const ok = await startExactCapture();
4834
+ const ok = armExactCapture();
4835
4835
  setExactStatus(getExactCaptureStatus());
4836
4836
  storage.setItem(EXACT_SHOTS_KEY, ok ? "1" : "0");
4837
4837
  notify(ok ? t("exact_on") : t("exact_declined"), { tone: ok ? "success" : "info", id: "exact-shots" });
4838
4838
  return ok;
4839
4839
  }, [storage, notify, t]);
4840
+ const photographNow = React.useCallback(async () => {
4841
+ if (!armExactCapture()) {
4842
+ setExactStatus(getExactCaptureStatus());
4843
+ notify(t("exact_declined"), { tone: "info", id: "exact-shots" });
4844
+ return false;
4845
+ }
4846
+ storage.setItem(EXACT_SHOTS_KEY, "1");
4847
+ const frame = await freezeViewport();
4848
+ setFrozenAt(frame?.takenAt ?? null);
4849
+ setExactStatus(getExactCaptureStatus());
4850
+ notify(frame ? t("exact_on") : t("exact_declined"), {
4851
+ tone: frame ? "success" : "info",
4852
+ id: "exact-shots"
4853
+ });
4854
+ return !!frame;
4855
+ }, [storage, notify, t]);
4840
4856
  const disableExactShots = React.useCallback(() => {
4841
- stopExactCapture();
4857
+ disarmExactCapture();
4842
4858
  storage.setItem(EXACT_SHOTS_KEY, "0");
4859
+ setFrozenAt(null);
4843
4860
  setExactStatus(getExactCaptureStatus());
4844
4861
  }, [storage]);
4845
4862
  const rememberCampaign = React.useCallback((next) => {
@@ -5254,16 +5271,23 @@ function QaProvider({
5254
5271
  const walkNavigate = React.useCallback((path, hard = false) => {
5255
5272
  if (typeof window === "undefined" || !path) return;
5256
5273
  const target = path.startsWith("/") ? path : `/${path}`;
5257
- if (hard) {
5274
+ if (hard || target === window.location.pathname) {
5258
5275
  window.location.assign(target);
5259
5276
  return;
5260
5277
  }
5278
+ const before = pageSignature();
5261
5279
  try {
5262
- window.history.pushState({}, "", target);
5263
- window.dispatchEvent(new PopStateEvent("popstate"));
5280
+ window.history.pushState(window.history.state, "", target);
5281
+ window.dispatchEvent(new PopStateEvent("popstate", { state: window.history.state }));
5264
5282
  } catch {
5265
5283
  window.location.assign(target);
5284
+ return;
5266
5285
  }
5286
+ window.setTimeout(() => {
5287
+ if (window.location.pathname !== target) return;
5288
+ if (pageSignature() !== before) return;
5289
+ window.location.assign(target);
5290
+ }, SOFT_NAV_PROOF_MS);
5267
5291
  }, []);
5268
5292
  React.useEffect(() => {
5269
5293
  if (typeof window === "undefined") return;
@@ -5351,8 +5375,9 @@ function QaProvider({
5351
5375
  gradeStep,
5352
5376
  evidenceByStep,
5353
5377
  // v0.4 — screenshots
5354
- exactShots: { supported: exactSupported, status: exactStatus },
5378
+ exactShots: { supported: exactSupported, status: exactStatus, frozenAt },
5355
5379
  enableExactShots,
5380
+ photographNow,
5356
5381
  disableExactShots,
5357
5382
  // v0.4 — folder sync. syncTick is read here purely so this object is
5358
5383
  // rebuilt when fsSync's module state changes (path/campaign live outside
@@ -6146,11 +6171,17 @@ function flashLocate(target) {
6146
6171
  paint(rect);
6147
6172
  }
6148
6173
  }
6149
- function LocationReveal({ target }) {
6150
- const { t } = useQa();
6174
+ function LocationReveal({
6175
+ target,
6176
+ route
6177
+ }) {
6178
+ const { t, walkNavigate } = useQa();
6151
6179
  const [open, setOpen] = React.useState(false);
6152
6180
  if (!target) return null;
6153
6181
  const r = target.rect;
6182
+ const notePath = (route || "").split("?")[0].split("#")[0];
6183
+ const here = typeof window === "undefined" ? "" : window.location.pathname;
6184
+ const elsewhere = !!notePath && notePath !== here;
6154
6185
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-rounded-lg qa-border qa-border-subtle qa-bg-2", children: [
6155
6186
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1.5 qa-px-2 qa-py-1.5 qa-text-11", children: [
6156
6187
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 14, className: "qa-text-success" }),
@@ -6214,7 +6245,21 @@ function LocationReveal({ target }) {
6214
6245
  "\xD7",
6215
6246
  Math.round(r.height)
6216
6247
  ] }),
6217
- /* @__PURE__ */ jsxRuntime.jsxs(
6248
+ elsewhere ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6249
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "qa-mt-1 qa-mb-1 qa-m-0 qa-text-lo", children: t("loc_other_page", { path: notePath }) }),
6250
+ /* @__PURE__ */ jsxRuntime.jsxs(
6251
+ "button",
6252
+ {
6253
+ onClick: () => walkNavigate(notePath),
6254
+ 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",
6255
+ style: { border: "none", cursor: "pointer" },
6256
+ children: [
6257
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPinned", size: 12 }),
6258
+ t("loc_go_there", { path: notePath })
6259
+ ]
6260
+ }
6261
+ )
6262
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs(
6218
6263
  "button",
6219
6264
  {
6220
6265
  onClick: () => flashLocate(target),
@@ -6932,7 +6977,7 @@ function NoteItem({
6932
6977
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPin", size: 12, className: "qa-shrink-0" }),
6933
6978
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-truncate qa-dir-ltr", title: note.url, children: note.route })
6934
6979
  ] }),
6935
- note.target && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: note.target }),
6980
+ note.target && /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: note.target, route: note.route }),
6936
6981
  contextEventCount > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { children: t("context_attached", { n: contextEventCount }) }),
6937
6982
  stepCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-flex qa-items-center qa-gap-1", children: [
6938
6983
  /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "ClipboardList", size: 12, className: "qa-shrink-0" }),
@@ -8676,6 +8721,20 @@ var RISK_COLOR = {
8676
8721
  amber: "var(--qa-warn)",
8677
8722
  green: "var(--qa-success)"
8678
8723
  };
8724
+ function useCurrentPath() {
8725
+ const [path, setPath] = React.useState(() => typeof window === "undefined" ? "" : window.location.pathname);
8726
+ React.useEffect(() => {
8727
+ if (typeof window === "undefined") return;
8728
+ const sync = () => setPath(window.location.pathname);
8729
+ window.addEventListener("popstate", sync);
8730
+ window.addEventListener("hashchange", sync);
8731
+ return () => {
8732
+ window.removeEventListener("popstate", sync);
8733
+ window.removeEventListener("hashchange", sync);
8734
+ };
8735
+ }, []);
8736
+ return path;
8737
+ }
8679
8738
  function WalkHud() {
8680
8739
  const {
8681
8740
  dir,
@@ -8695,10 +8754,13 @@ function WalkHud() {
8695
8754
  startCapture,
8696
8755
  updateNote,
8697
8756
  retestNote,
8698
- notes
8757
+ notes,
8758
+ notesLoading
8699
8759
  } = useQa();
8700
8760
  const index = Math.min(walk.index, Math.max(0, walkStops.length - 1));
8701
8761
  const stop = walkStops[index];
8762
+ const currentPath = useCurrentPath();
8763
+ const [going, setGoing] = React.useState(false);
8702
8764
  const backIcon = dir === "rtl" ? "ChevronRight" : "ChevronLeft";
8703
8765
  const nextIcon = dir === "rtl" ? "ChevronLeft" : "ChevronRight";
8704
8766
  React.useEffect(() => {
@@ -8712,8 +8774,9 @@ function WalkHud() {
8712
8774
  if (latest?.target) flashLocate(latest.target);
8713
8775
  }, [index, walk.source]);
8714
8776
  React.useEffect(() => {
8777
+ if (notesLoading) return;
8715
8778
  if (walk.active && walkStops.length === 0) exitWalk();
8716
- }, [walk.active, walkStops.length, exitWalk]);
8779
+ }, [walk.active, walkStops.length, exitWalk, notesLoading]);
8717
8780
  if (!walk.active || !stop) return null;
8718
8781
  const atLast = index >= walkStops.length - 1;
8719
8782
  let title;
@@ -8874,7 +8937,7 @@ function WalkHud() {
8874
8937
  )
8875
8938
  ] });
8876
8939
  }
8877
- const onThisPage = stop.path === ANY_PAGE || typeof window !== "undefined" && stop.path === window.location.pathname;
8940
+ const onThisPage = stop.path === ANY_PAGE || stop.path === currentPath;
8878
8941
  return /* @__PURE__ */ jsxRuntime.jsxs(
8879
8942
  "div",
8880
8943
  {
@@ -8910,12 +8973,24 @@ function WalkHud() {
8910
8973
  "button",
8911
8974
  {
8912
8975
  type: "button",
8913
- onClick: () => walkNavigate(stop.path),
8976
+ disabled: going,
8977
+ onClick: () => {
8978
+ setGoing(true);
8979
+ walkNavigate(stop.path);
8980
+ window.setTimeout(() => setGoing(false), 900);
8981
+ },
8914
8982
  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",
8915
- style: { border: "none", cursor: "pointer" },
8983
+ style: { border: "none", cursor: going ? "default" : "pointer" },
8916
8984
  children: [
8917
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "MapPinned", size: 13 }),
8918
- t("walk_take_me", { path: stop.path })
8985
+ /* @__PURE__ */ jsxRuntime.jsx(
8986
+ Icon,
8987
+ {
8988
+ name: going ? "Loader2" : "MapPinned",
8989
+ size: 13,
8990
+ className: going ? "qa-animate-spin" : void 0
8991
+ }
8992
+ ),
8993
+ going ? t("walk_taking_you") : t("walk_take_me", { path: stop.path })
8919
8994
  ]
8920
8995
  }
8921
8996
  ),
@@ -9135,7 +9210,7 @@ function CaptureMode() {
9135
9210
  compactCapture,
9136
9211
  setCompactCapture,
9137
9212
  exactShots,
9138
- enableExactShots,
9213
+ photographNow,
9139
9214
  capturePrefill
9140
9215
  } = useQa();
9141
9216
  const coarse = useCoarsePointer();
@@ -9149,6 +9224,7 @@ function CaptureMode() {
9149
9224
  const [candidate, setCandidate] = React.useState(null);
9150
9225
  const [regionMode, setRegionMode] = React.useState(false);
9151
9226
  const [shot, setShot] = React.useState(null);
9227
+ const stillRef = React.useRef(null);
9152
9228
  const [shotEngine, setShotEngine] = React.useState(null);
9153
9229
  const [shotUrl, setShotUrl] = React.useState(null);
9154
9230
  const [capturing, setCapturing] = React.useState(false);
@@ -9441,6 +9517,18 @@ function CaptureMode() {
9441
9517
  React.useEffect(() => () => {
9442
9518
  unlockPageScroll();
9443
9519
  }, []);
9520
+ React.useEffect(() => {
9521
+ const host = stillRef.current;
9522
+ const frame = getFrozenFrame();
9523
+ if (!host || !frame) return;
9524
+ const canvas = frame.canvas;
9525
+ canvas.style.cssText = "display:block;width:100%;height:100%;";
9526
+ host.appendChild(canvas);
9527
+ return () => {
9528
+ if (canvas.parentNode === host) host.removeChild(canvas);
9529
+ canvas.removeAttribute("style");
9530
+ };
9531
+ }, [exactShots.frozenAt]);
9444
9532
  const save = async () => {
9445
9533
  if (!selection || !description.trim()) return;
9446
9534
  const target = {
@@ -9499,6 +9587,16 @@ function CaptureMode() {
9499
9587
  const isRegion = !!drag?.rect || candidate?.kind === "region" || selection?.kind === "region";
9500
9588
  const confirmingRegion = phase === "confirming" && candidate?.kind === "region" && coarse;
9501
9589
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-qa-overlay": "true", "data-qa-capture-root": "true", ref: overlayRootRef, children: [
9590
+ exactShots.frozenAt !== null && /* @__PURE__ */ jsxRuntime.jsx(
9591
+ "div",
9592
+ {
9593
+ ref: stillRef,
9594
+ "aria-hidden": "true",
9595
+ "data-qa-capture-still": "true",
9596
+ className: "qa-fixed qa-inset-0 qa-z-10089",
9597
+ style: { pointerEvents: "none", overflow: "hidden" }
9598
+ }
9599
+ ),
9502
9600
  /* @__PURE__ */ jsxRuntime.jsx(
9503
9601
  "div",
9504
9602
  {
@@ -9557,7 +9655,7 @@ function CaptureMode() {
9557
9655
  ) : /* @__PURE__ */ jsxRuntime.jsxs(
9558
9656
  "button",
9559
9657
  {
9560
- onClick: () => void enableExactShots(),
9658
+ onClick: () => void photographNow(),
9561
9659
  title: t("exact_hint"),
9562
9660
  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",
9563
9661
  style: { background: "transparent", cursor: "pointer" },
@@ -10001,8 +10099,8 @@ function CaptureMode() {
10001
10099
  {
10002
10100
  type: "button",
10003
10101
  onClick: () => void (async () => {
10004
- const granted = await enableExactShots();
10005
- if (granted && selection) void runCapture(selection.rect);
10102
+ const shot2 = await photographNow();
10103
+ if (shot2 && selection) void runCapture(selection.rect);
10006
10104
  })(),
10007
10105
  title: t("exact_hint"),
10008
10106
  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",
@@ -10328,5 +10426,5 @@ function Qapture({ config }) {
10328
10426
  exports.Qapture = Qapture;
10329
10427
  exports.deleteQaDatabase = deleteQaDatabase;
10330
10428
  exports.initQaStudio = initQaStudio;
10331
- //# sourceMappingURL=chunk-4LMLW73U.cjs.map
10332
- //# sourceMappingURL=chunk-4LMLW73U.cjs.map
10429
+ //# sourceMappingURL=chunk-E7OFH6EM.cjs.map
10430
+ //# sourceMappingURL=chunk-E7OFH6EM.cjs.map