qapture2 0.7.5 → 0.7.7

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, mode, fn) {
61
+ function run(dbName, store, mode2, fn) {
62
62
  return openDB(dbName).then(
63
63
  (db) => new Promise((resolve, reject) => {
64
- const tx = db.transaction(store, mode);
64
+ const tx = db.transaction(store, mode2);
65
65
  const s = tx.objectStore(store);
66
66
  let result;
67
67
  const req = fn(s);
@@ -1762,10 +1762,10 @@ var STR = {
1762
1762
  copied: "Copied",
1763
1763
  copy_failed: "Copy failed",
1764
1764
  copy_prompt: "Copy as agent prompt",
1765
- severity_label: "Severity",
1765
+ severity_label: "What is this?",
1766
1766
  sev_bug: "Bug",
1767
- sev_question: "Question",
1768
- sev_polish: "Polish",
1767
+ sev_design: "Design",
1768
+ sev_enhance: "Enhance",
1769
1769
  status_open: "Open",
1770
1770
  status_verified: "Verified",
1771
1771
  context_attached: "{n} runtime events attached",
@@ -1781,10 +1781,10 @@ var STR = {
1781
1781
  no_evidence: "ticked, no capture",
1782
1782
  expected_label: "Expected",
1783
1783
  exact_label: "Pixel-exact shots",
1784
- exact_hint: "Screenshots become a real photo of the tab instead of a redraw. Asks once to share this tab; nothing leaves your device.",
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.",
1785
1785
  redraw_notice: "This is a redraw of the page, not a real photo.",
1786
1786
  redraw_action: "Use real screenshots",
1787
- redraw_no_engine: "This browser cannot photograph a tab, so charts and canvases may come out wrong. Open the app in Chrome, Edge or Brave for true screenshots.",
1787
+ 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.",
1788
1788
  fab_title: "Qapture \u2014 click to open, drag to move it out of the way",
1789
1789
  exact_on: "Pixel-exact screenshots on",
1790
1790
  exact_off: "Back to redrawn screenshots",
@@ -1960,10 +1960,10 @@ var STR = {
1960
1960
  copied: "\u062A\u0645 \u0627\u0644\u0646\u0633\u062E",
1961
1961
  copy_failed: "\u0641\u0634\u0644 \u0627\u0644\u0646\u0633\u062E",
1962
1962
  copy_prompt: "\u0646\u0633\u062E \u0643\u0645\u0648\u062C\u0651\u0647 \u0644\u0644\u0648\u0643\u064A\u0644",
1963
- severity_label: "\u0627\u0644\u0623\u0647\u0645\u064A\u0629",
1964
- sev_bug: "\u062E\u0644\u0644",
1965
- sev_question: "\u0633\u0624\u0627\u0644",
1966
- sev_polish: "\u062A\u062D\u0633\u064A\u0646",
1963
+ severity_label: "\u0645\u0627 \u0647\u0630\u0627\u061F",
1964
+ sev_bug: "Bug",
1965
+ sev_design: "Design",
1966
+ sev_enhance: "Enhance",
1967
1967
  status_open: "\u0645\u0641\u062A\u0648\u062D",
1968
1968
  status_verified: "\u062A\u0645 \u0627\u0644\u062A\u062D\u0642\u0642",
1969
1969
  context_attached: "{n} \u0645\u0646 \u0623\u062D\u062F\u0627\u062B \u0627\u0644\u062A\u0634\u063A\u064A\u0644 \u0645\u0631\u0641\u0642\u0629",
@@ -1979,10 +1979,10 @@ var STR = {
1979
1979
  no_evidence: "\u0645\u064F\u0639\u0644\u0651\u0645 \u0628\u062F\u0648\u0646 \u0627\u0644\u062A\u0642\u0627\u0637",
1980
1980
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1981
1981
  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 \u0644\u0644\u062A\u0628\u0648\u064A\u0628 \u0628\u062F\u0644 \u0625\u0639\u0627\u062F\u0629 \u0631\u0633\u0645\u0647. \u064A\u064F\u0637\u0644\u0628 \u0627\u0644\u0625\u0630\u0646 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629\u060C \u0648\u0644\u0627 \u0634\u064A\u0621 \u064A\u063A\u0627\u062F\u0631 \u062C\u0647\u0627\u0632\u0643.",
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.",
1983
1983
  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
1984
  redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1985
- 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\u062A\u0628\u0648\u064A\u0628\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 \u0641\u064A Chrome \u0623\u0648 Edge \u0623\u0648 Brave \u0644\u0644\u062D\u0635\u0648\u0644 \u0639\u0644\u0649 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629.",
1985
+ 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.",
1986
1986
  fab_title: "Qapture \u2014 \u0627\u0636\u063A\u0637 \u0644\u0644\u0641\u062A\u062D\u060C \u0648\u0627\u0633\u062D\u0628 \u0644\u062A\u062D\u0631\u064A\u0643\u0647 \u0645\u0646 \u0645\u0643\u0627\u0646\u0647",
1987
1987
  exact_on: "\u062A\u0645 \u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629",
1988
1988
  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",
@@ -2182,8 +2182,168 @@ function computeCoverage(journey, guideChecked, skipped) {
2182
2182
  };
2183
2183
  }
2184
2184
 
2185
+ // src/lib/frameCalibration.ts
2186
+ var MARKER_SIZE = 28;
2187
+ var MARKERS = [
2188
+ { key: "tl", color: [255, 0, 0] },
2189
+ { key: "tr", color: [0, 255, 0] },
2190
+ { key: "bl", color: [0, 0, 255] },
2191
+ { key: "br", color: [255, 0, 255] }
2192
+ ];
2193
+ function markerCentres(vw, vh) {
2194
+ const h = MARKER_SIZE / 2;
2195
+ return {
2196
+ tl: { x: h, y: h },
2197
+ tr: { x: vw - h, y: h },
2198
+ bl: { x: h, y: vh - h },
2199
+ br: { x: vw - h, y: vh - h }
2200
+ };
2201
+ }
2202
+ function dist2(r, g, b, c) {
2203
+ const dr = r - c[0];
2204
+ const dg = g - c[1];
2205
+ const db = b - c[2];
2206
+ return dr * dr + dg * dg + db * db;
2207
+ }
2208
+ var COLOR_TOLERANCE_2 = 90 * 90;
2209
+ var CLUSTER_CELL = 8;
2210
+ var MAX_CANDIDATES = 4;
2211
+ function findMarkerCandidates(data, width, height) {
2212
+ const cols = Math.ceil(width / CLUSTER_CELL);
2213
+ const rows = Math.ceil(height / CLUSTER_CELL);
2214
+ const cells = {};
2215
+ for (const m of MARKERS) cells[m.key] = new Int32Array(cols * rows);
2216
+ for (let y = 0; y < height; y++) {
2217
+ const row = y * width * 4;
2218
+ const cy = y / CLUSTER_CELL | 0;
2219
+ for (let x = 0; x < width; x++) {
2220
+ const i = row + x * 4;
2221
+ if (data[i + 3] < 128) continue;
2222
+ const r = data[i];
2223
+ const g = data[i + 1];
2224
+ const b = data[i + 2];
2225
+ if (Math.max(r, g, b) - Math.min(r, g, b) < 60) continue;
2226
+ for (const m of MARKERS) {
2227
+ if (dist2(r, g, b, m.color) <= COLOR_TOLERANCE_2) {
2228
+ cells[m.key][cy * cols + (x / CLUSTER_CELL | 0)]++;
2229
+ break;
2230
+ }
2231
+ }
2232
+ }
2233
+ }
2234
+ const out = {};
2235
+ for (const m of MARKERS) {
2236
+ const grid = cells[m.key];
2237
+ const seen = new Uint8Array(cols * rows);
2238
+ const blobs = [];
2239
+ for (let start = 0; start < grid.length; start++) {
2240
+ if (!grid[start] || seen[start]) continue;
2241
+ const queue = [start];
2242
+ seen[start] = 1;
2243
+ let sumX = 0;
2244
+ let sumY = 0;
2245
+ let total = 0;
2246
+ while (queue.length) {
2247
+ const idx = queue.pop();
2248
+ const n = grid[idx];
2249
+ const cx = idx % cols;
2250
+ const cy = idx / cols | 0;
2251
+ sumX += (cx * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
2252
+ sumY += (cy * CLUSTER_CELL + CLUSTER_CELL / 2) * n;
2253
+ total += n;
2254
+ for (let dy = -1; dy <= 1; dy++) {
2255
+ for (let dx = -1; dx <= 1; dx++) {
2256
+ const nx = cx + dx;
2257
+ const ny = cy + dy;
2258
+ if (nx < 0 || ny < 0 || nx >= cols || ny >= rows) continue;
2259
+ const nIdx = ny * cols + nx;
2260
+ if (seen[nIdx] || !grid[nIdx]) continue;
2261
+ seen[nIdx] = 1;
2262
+ queue.push(nIdx);
2263
+ }
2264
+ }
2265
+ }
2266
+ if (total < 16) continue;
2267
+ blobs.push({ x: sumX / total, y: sumY / total, count: total });
2268
+ }
2269
+ blobs.sort((a, b) => b.count - a.count);
2270
+ out[m.key] = blobs.slice(0, MAX_CANDIDATES);
2271
+ }
2272
+ return out;
2273
+ }
2274
+ var VERIFY_TOLERANCE = 0.02;
2275
+ var SQUARENESS_TOLERANCE = 0.05;
2276
+ function deriveMapping(candidates, vw, vh, frameW, frameH) {
2277
+ const tls = candidates.tl ?? [];
2278
+ const brs = candidates.br ?? [];
2279
+ for (const tl of tls) {
2280
+ for (const br of brs) {
2281
+ const m = solveFrom(tl, br, candidates, vw, vh, frameW, frameH);
2282
+ if (m) return m;
2283
+ }
2284
+ }
2285
+ return null;
2286
+ }
2287
+ function solveFrom(tl, br, candidates, vw, vh, frameW, frameH) {
2288
+ if (!(vw > MARKER_SIZE) || !(vh > MARKER_SIZE)) return null;
2289
+ const centres = markerCentres(vw, vh);
2290
+ const spanCssX = centres.br.x - centres.tl.x;
2291
+ const spanCssY = centres.br.y - centres.tl.y;
2292
+ if (spanCssX <= 0 || spanCssY <= 0) return null;
2293
+ const scaleX = (br.x - tl.x) / spanCssX;
2294
+ const scaleY = (br.y - tl.y) / spanCssY;
2295
+ if (!(scaleX > 0.05) || !(scaleY > 0.05)) return null;
2296
+ if (!Number.isFinite(scaleX) || !Number.isFinite(scaleY)) return null;
2297
+ if (Math.abs(scaleX - scaleY) / Math.max(scaleX, scaleY) > SQUARENESS_TOLERANCE) return null;
2298
+ const originX = tl.x - centres.tl.x * scaleX;
2299
+ const originY = tl.y - centres.tl.y * scaleY;
2300
+ const mapping2 = { scaleX, scaleY, originX, originY };
2301
+ const farX = originX + vw * scaleX;
2302
+ const farY = originY + vh * scaleY;
2303
+ const slack = Math.max(4, frameW * 0.01);
2304
+ if (originX < -slack || originY < -slack || farX > frameW + slack || farY > frameH + slack) {
2305
+ return null;
2306
+ }
2307
+ const tolerance = Math.max(6, Math.max(frameW, frameH) * VERIFY_TOLERANCE);
2308
+ let verified = 0;
2309
+ for (const key of ["tr", "bl"]) {
2310
+ const hits = candidates[key] ?? [];
2311
+ if (!hits.length) continue;
2312
+ const want = centres[key];
2313
+ const px = originX + want.x * scaleX;
2314
+ const py = originY + want.y * scaleY;
2315
+ if (!hits.some((h) => Math.hypot(px - h.x, py - h.y) <= tolerance)) return null;
2316
+ verified++;
2317
+ }
2318
+ if (verified === 0) return null;
2319
+ return mapping2;
2320
+ }
2321
+ function mapRectToFrame(rect, m, frameW, frameH) {
2322
+ const x0 = m.originX + rect.left * m.scaleX;
2323
+ const y0 = m.originY + rect.top * m.scaleY;
2324
+ const sx = Math.max(0, Math.min(frameW, Math.round(x0)));
2325
+ const sy = Math.max(0, Math.min(frameH, Math.round(y0)));
2326
+ const sw = Math.min(frameW - sx, Math.round(rect.width * m.scaleX));
2327
+ const sh = Math.min(frameH - sy, Math.round(rect.height * m.scaleY));
2328
+ if (sw < 1 || sh < 1) return null;
2329
+ return { sx, sy, sw, sh };
2330
+ }
2331
+ function environmentSignature() {
2332
+ if (typeof window === "undefined") return "";
2333
+ return [
2334
+ window.innerWidth,
2335
+ window.innerHeight,
2336
+ window.outerWidth,
2337
+ window.outerHeight,
2338
+ window.screenX,
2339
+ window.screenY,
2340
+ Math.round((window.devicePixelRatio || 1) * 100)
2341
+ ].join("x");
2342
+ }
2343
+
2185
2344
  // src/lib/screenCapture.ts
2186
2345
  var ASPECT_TOLERANCE = 0.08;
2346
+ var CALIBRATION_SETTLE_MS = 220;
2187
2347
  var FRESH_FRAME_TIMEOUT_MS = 500;
2188
2348
  var VIDEO_READY_TIMEOUT_MS = 4e3;
2189
2349
  var stream = null;
@@ -2191,22 +2351,14 @@ var track = null;
2191
2351
  var video = null;
2192
2352
  var grabber = null;
2193
2353
  var declined = false;
2194
- function supportsPreferCurrentTab() {
2195
- const nav = navigator;
2196
- const brands = nav.userAgentData?.brands;
2197
- if (Array.isArray(brands) && brands.length) {
2198
- return brands.some((b) => /chromium/i.test(b.brand));
2199
- }
2200
- const ua = navigator.userAgent || "";
2201
- if (/CriOS|FxiOS|EdgiOS|Firefox/i.test(ua)) return false;
2202
- return /Chrome|Chromium|Edg\//i.test(ua);
2203
- }
2204
2354
  function isExactCaptureSupported() {
2205
2355
  if (typeof navigator === "undefined" || typeof document === "undefined") return false;
2206
2356
  const md = navigator.mediaDevices;
2207
- if (!md || typeof md.getDisplayMedia !== "function") return false;
2208
- return supportsPreferCurrentTab();
2357
+ return !!md && typeof md.getDisplayMedia === "function";
2209
2358
  }
2359
+ var mode = "tab";
2360
+ var mapping = null;
2361
+ var mappingSignature = "";
2210
2362
  function getExactCaptureStatus() {
2211
2363
  if (!isExactCaptureSupported()) return "unsupported";
2212
2364
  if (track && track.readyState === "live") return "live";
@@ -2234,6 +2386,9 @@ function teardown() {
2234
2386
  stream = null;
2235
2387
  track = null;
2236
2388
  grabber = null;
2389
+ mode = "tab";
2390
+ mapping = null;
2391
+ mappingSignature = "";
2237
2392
  }
2238
2393
  function stopExactCapture() {
2239
2394
  teardown();
@@ -2288,6 +2443,78 @@ function validateFrameMapping(frameW, frameH) {
2288
2443
  const viewAspect = vw / vh;
2289
2444
  return Math.abs(frameAspect - viewAspect) / viewAspect <= ASPECT_TOLERANCE;
2290
2445
  }
2446
+ async function grabFrameCanvas() {
2447
+ if (!video) return null;
2448
+ await nextPresentedFrame(video);
2449
+ let source = video;
2450
+ let w = video.videoWidth;
2451
+ let h = video.videoHeight;
2452
+ let bitmap = null;
2453
+ if (grabber) {
2454
+ try {
2455
+ bitmap = await grabber.grabFrame();
2456
+ source = bitmap;
2457
+ w = bitmap.width;
2458
+ h = bitmap.height;
2459
+ } catch {
2460
+ }
2461
+ }
2462
+ if (!w || !h) {
2463
+ bitmap?.close?.();
2464
+ return null;
2465
+ }
2466
+ const c = document.createElement("canvas");
2467
+ c.width = w;
2468
+ c.height = h;
2469
+ const ctx = c.getContext("2d", { willReadFrequently: true });
2470
+ if (!ctx) {
2471
+ bitmap?.close?.();
2472
+ return null;
2473
+ }
2474
+ ctx.drawImage(source, 0, 0);
2475
+ bitmap?.close?.();
2476
+ return c;
2477
+ }
2478
+ async function calibrate() {
2479
+ if (!video) return false;
2480
+ const vw = window.innerWidth;
2481
+ const vh = window.innerHeight;
2482
+ if (vw <= MARKER_SIZE || vh <= MARKER_SIZE) return false;
2483
+ const card = document.createElement("div");
2484
+ card.setAttribute("data-qa-calibration", "true");
2485
+ card.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:#0a0a0a;pointer-events:none;contain:strict;";
2486
+ const centres = markerCentres(vw, vh);
2487
+ for (const m of MARKERS) {
2488
+ const dot = document.createElement("div");
2489
+ const c = centres[m.key];
2490
+ dot.style.cssText = `position:absolute;width:${MARKER_SIZE}px;height:${MARKER_SIZE}px;left:${c.x - MARKER_SIZE / 2}px;top:${c.y - MARKER_SIZE / 2}px;background:rgb(${m.color[0]},${m.color[1]},${m.color[2]});`;
2491
+ card.appendChild(dot);
2492
+ }
2493
+ document.body.appendChild(card);
2494
+ try {
2495
+ await new Promise((r) => setTimeout(r, CALIBRATION_SETTLE_MS));
2496
+ const frame = await grabFrameCanvas();
2497
+ if (!frame) return false;
2498
+ const ctx = frame.getContext("2d", { willReadFrequently: true });
2499
+ if (!ctx) return false;
2500
+ const { data } = ctx.getImageData(0, 0, frame.width, frame.height);
2501
+ const found = deriveMapping(
2502
+ findMarkerCandidates(data, frame.width, frame.height),
2503
+ vw,
2504
+ vh,
2505
+ frame.width,
2506
+ frame.height
2507
+ );
2508
+ if (!found) return false;
2509
+ mapping = found;
2510
+ mappingSignature = environmentSignature();
2511
+ return true;
2512
+ } catch {
2513
+ return false;
2514
+ } finally {
2515
+ card.remove();
2516
+ }
2517
+ }
2291
2518
  async function startExactCapture() {
2292
2519
  if (!isExactCaptureSupported()) return false;
2293
2520
  if (track && track.readyState === "live" && video) return true;
@@ -2317,11 +2544,7 @@ async function startExactCapture() {
2317
2544
  return false;
2318
2545
  }
2319
2546
  const settings = t.getSettings();
2320
- if (settings.displaySurface && settings.displaySurface !== "browser") {
2321
- for (const x of s.getTracks()) x.stop();
2322
- declined = true;
2323
- return false;
2324
- }
2547
+ const sharedTab = !settings.displaySurface || settings.displaySurface === "browser";
2325
2548
  const el = document.createElement("video");
2326
2549
  el.setAttribute("data-qa-overlay", "true");
2327
2550
  el.muted = true;
@@ -2334,17 +2557,31 @@ async function startExactCapture() {
2334
2557
  } catch {
2335
2558
  }
2336
2559
  const ready = await waitFor(() => el.videoWidth > 0 && el.videoHeight > 0, VIDEO_READY_TIMEOUT_MS);
2337
- if (!ready || !validateFrameMapping(el.videoWidth, el.videoHeight)) {
2560
+ const giveUp = () => {
2338
2561
  el.remove();
2339
2562
  for (const x of s.getTracks()) x.stop();
2340
2563
  declined = true;
2341
2564
  return false;
2342
- }
2565
+ };
2566
+ if (!ready) return giveUp();
2567
+ const isDirect = sharedTab && validateFrameMapping(el.videoWidth, el.videoHeight);
2343
2568
  stream = s;
2344
2569
  track = t;
2345
2570
  video = el;
2346
2571
  const IC = window.ImageCapture;
2347
2572
  grabber = IC ? new IC(t) : null;
2573
+ if (isDirect) {
2574
+ mode = "tab";
2575
+ mapping = null;
2576
+ mappingSignature = "";
2577
+ } else {
2578
+ mode = "surface";
2579
+ if (!await calibrate()) {
2580
+ teardown();
2581
+ declined = true;
2582
+ return false;
2583
+ }
2584
+ }
2348
2585
  t.addEventListener("ended", () => {
2349
2586
  teardown();
2350
2587
  });
@@ -2357,6 +2594,11 @@ async function startExactCapture() {
2357
2594
  }
2358
2595
  async function grabExactRegion(rect) {
2359
2596
  if (!track || track.readyState !== "live" || !video) return null;
2597
+ if (mode === "surface") {
2598
+ if (!mapping || environmentSignature() !== mappingSignature) {
2599
+ if (!await calibrate()) return null;
2600
+ }
2601
+ }
2360
2602
  await nextPresentedFrame(video);
2361
2603
  let source;
2362
2604
  let frameW;
@@ -2379,13 +2621,24 @@ async function grabExactRegion(rect) {
2379
2621
  frameH = video.videoHeight;
2380
2622
  }
2381
2623
  try {
2382
- if (!validateFrameMapping(frameW, frameH)) return null;
2383
- const scaleX = frameW / window.innerWidth;
2384
- const scaleY = frameH / window.innerHeight;
2385
- const sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
2386
- const sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
2387
- const sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
2388
- const sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
2624
+ let sx;
2625
+ let sy;
2626
+ let sw;
2627
+ let sh;
2628
+ if (mode === "surface") {
2629
+ if (!mapping) return null;
2630
+ const mapped = mapRectToFrame(rect, mapping, frameW, frameH);
2631
+ if (!mapped) return null;
2632
+ ({ sx, sy, sw, sh } = mapped);
2633
+ } else {
2634
+ if (!validateFrameMapping(frameW, frameH)) return null;
2635
+ const scaleX = frameW / window.innerWidth;
2636
+ const scaleY = frameH / window.innerHeight;
2637
+ sx = Math.max(0, Math.min(frameW, Math.round(rect.left * scaleX)));
2638
+ sy = Math.max(0, Math.min(frameH, Math.round(rect.top * scaleY)));
2639
+ sw = Math.max(1, Math.min(frameW - sx, Math.round(rect.width * scaleX)));
2640
+ sh = Math.max(1, Math.min(frameH - sy, Math.round(rect.height * scaleY)));
2641
+ }
2389
2642
  const canvas = document.createElement("canvas");
2390
2643
  canvas.width = sw;
2391
2644
  canvas.height = sh;
@@ -3031,15 +3284,15 @@ function fmtPct(n, d) {
3031
3284
  }
3032
3285
  function summariseSession(notes, stamp) {
3033
3286
  if (!notes.length) return "No points captured.";
3034
- let bugs = 0, questions = 0, polish = 0, verified = 0, awaiting = 0;
3287
+ let bugs = 0, enhancements = 0, designs = 0, verified = 0, awaiting = 0;
3035
3288
  const pages = /* @__PURE__ */ new Set();
3036
3289
  let earliest = Number.POSITIVE_INFINITY;
3037
3290
  let latest = 0;
3038
3291
  for (const n of notes) {
3039
3292
  const sev = n.severity ?? "bug";
3040
3293
  if (sev === "bug") bugs++;
3041
- else if (sev === "question") questions++;
3042
- else polish++;
3294
+ else if (sev === "enhance") enhancements++;
3295
+ else designs++;
3043
3296
  const status = n.status ?? "open";
3044
3297
  if (status === "verified") verified++;
3045
3298
  else if (status === "fixed") awaiting++;
@@ -3054,8 +3307,8 @@ function summariseSession(notes, stamp) {
3054
3307
  `${notes.length} point${notes.length === 1 ? "" : "s"}`,
3055
3308
  `${bugs} bug${bugs === 1 ? "" : "s"}`
3056
3309
  ];
3057
- if (questions) parts.push(`${questions} question${questions === 1 ? "" : "s"}`);
3058
- if (polish) parts.push(`${polish} polish`);
3310
+ if (enhancements) parts.push(`${enhancements} enhancement${enhancements === 1 ? "" : "s"}`);
3311
+ if (designs) parts.push(`${designs} design`);
3059
3312
  if (verified) parts.push(`${verified} verified`);
3060
3313
  if (awaiting) parts.push(`${awaiting} awaiting re-test`);
3061
3314
  parts.push(`${pages.size} page${pages.size === 1 ? "" : "s"}`);
@@ -3086,6 +3339,37 @@ Exported: ${stamp}
3086
3339
  Points: ${noteCount}
3087
3340
 
3088
3341
  **Session:** ${summariseSession(notes, stamp)}`
3342
+ );
3343
+ sections.push(
3344
+ `## What the tags mean
3345
+
3346
+ Every point below is tagged **Bug**, **Design** or **Enhance**. These are not priorities \u2014 they are three different kinds of work. Read the tag before you decide what to do.
3347
+
3348
+ ### Bug \u2014 something is broken
3349
+
3350
+ Find the **root cause**, not the symptom. Do not patch over it.
3351
+
3352
+ State the cause in one plain sentence before you change anything \u2014 e.g. "the date was wrong because the server sends UTC and the list formats in local time". **If you cannot write that sentence, you have not found the cause yet.** Keep looking. The real cause is often something small and silly; that is normal, and it is still the cause.
3353
+
3354
+ **The cause is not always in the code.** If the code does exactly what it was asked to do and the result is still wrong, then nothing is broken \u2014 what was *asked for* and what was *meant* did not match. That mismatch is the bug. Realign to what the owner meant and fix it there. If the note already says what they meant ("this isn't what I wanted, I wanted X"), they have told you \u2014 just do X. Do not stop to ask a question they have already answered.
3355
+
3356
+ ### Design \u2014 it works, but it looks or feels wrong
3357
+
3358
+ The functionality is fine and is not in question. Do not go hunting for a fault; there isn't one. Think properly about the interface and the experience \u2014 layout, spacing, hierarchy, wording, states, what the person is actually trying to do \u2014 and change how it looks and feels.
3359
+
3360
+ ### Enhance \u2014 a new idea, thought of while using the app
3361
+
3362
+ The tester wants something added or changed: a new section, a new feature, a different way of doing it.
3363
+
3364
+ **Plan it before you build it.** Read the surrounding code, work out what it touches and what it might break, and choose an approach that fits how the app is already built. Then build it. You do not need to stop and ask for approval \u2014 plan well, then go.
3365
+
3366
+ Push back only when you have a real reason: it would break something, or it conflicts with an invariant below. Then say so plainly and propose the alternative you would build instead.
3367
+
3368
+ ### When the tag and the words disagree
3369
+
3370
+ 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.
3371
+
3372
+ And if a point is phrased as a question rather than a report, **answer it \u2014 do not change code on a maybe.**`
3089
3373
  );
3090
3374
  const stack = typeof p.stack === "string" && p.stack.trim() ? p.stack.trim() : "(not provided)";
3091
3375
  const runArr = toStrings(p.runCommands);
@@ -3681,12 +3965,12 @@ function campaignJsonText(allNotes) {
3681
3965
  function reportMarkdownText(allNotes) {
3682
3966
  if (!campaign) return "";
3683
3967
  const ordered = orderedKnownNotes(allNotes);
3684
- const counts = { bug: 0, question: 0, polish: 0, verified: 0 };
3968
+ const counts = { bug: 0, design: 0, enhance: 0, verified: 0 };
3685
3969
  for (const n of ordered) {
3686
3970
  const sev = n.severity ?? "bug";
3687
3971
  if (sev === "bug") counts.bug++;
3688
- else if (sev === "question") counts.question++;
3689
- else counts.polish++;
3972
+ else if (sev === "enhance") counts.enhance++;
3973
+ else counts.design++;
3690
3974
  if (n.status === "verified") counts.verified++;
3691
3975
  }
3692
3976
  const header = [
@@ -3695,7 +3979,7 @@ function reportMarkdownText(allNotes) {
3695
3979
  campaign.tester ? `Tester: ${campaign.tester} ` : "",
3696
3980
  `Started: ${campaign.startedAt} `,
3697
3981
  `Updated: ${(/* @__PURE__ */ new Date()).toISOString()} `,
3698
- `Points: ${ordered.length} (${counts.bug} bug, ${counts.question} question, ${counts.polish} polish \xB7 ${counts.verified} verified)`,
3982
+ `Points: ${ordered.length} (${counts.bug} bug, ${counts.design} design, ${counts.enhance} enhance \xB7 ${counts.verified} verified)`,
3699
3983
  "",
3700
3984
  getFsSyncEngine() === "download" ? "Saved by Qapture as the campaign was tested. Screenshots are in" : "Written live by Qapture as each point was saved. Screenshots are in",
3701
3985
  "`screenshots/`, one Markdown file per point in `notes/`.",
@@ -4047,6 +4331,12 @@ function QaProvider({
4047
4331
  for (const id of pendingIds) void idb.delete(id);
4048
4332
  storage.setJSON(PENDING_DELETE_KEY, []);
4049
4333
  }
4334
+ live = live.map((n) => {
4335
+ const legacy = n.severity;
4336
+ if (legacy === "question") return { ...n, severity: "enhance" };
4337
+ if (legacy === "polish") return { ...n, severity: "design" };
4338
+ return n;
4339
+ });
4050
4340
  const sorted = live.slice().sort(
4051
4341
  (a, b) => a.timestamp < b.timestamp ? 1 : -1
4052
4342
  );
@@ -4645,8 +4935,8 @@ function QaProvider({
4645
4935
  const counts = {
4646
4936
  all: notes.length,
4647
4937
  bug: 0,
4648
- question: 0,
4649
- polish: 0,
4938
+ design: 0,
4939
+ enhance: 0,
4650
4940
  open: 0,
4651
4941
  fixed: 0,
4652
4942
  verified: 0,
@@ -4655,8 +4945,8 @@ function QaProvider({
4655
4945
  for (const n of notes) {
4656
4946
  const sev = n.severity ?? "bug";
4657
4947
  if (sev === "bug") counts.bug++;
4658
- else if (sev === "question") counts.question++;
4659
- else counts.polish++;
4948
+ else if (sev === "enhance") counts.enhance++;
4949
+ else counts.design++;
4660
4950
  const status = n.status ?? "open";
4661
4951
  if (status === "verified") counts.verified++;
4662
4952
  else if (status === "fixed") counts.fixed++;
@@ -5045,7 +5335,11 @@ function QaProvider({
5045
5335
  pendingShare,
5046
5336
  sharePending,
5047
5337
  retestNote,
5048
- showWelcome,
5338
+ // The greeting explains how to capture. Once a note exists, the tester has
5339
+ // either done it or found their own way, so the line has nothing left to
5340
+ // say — take it away rather than making them dismiss it. (The stored flag
5341
+ // still governs a first visit; this only hides it early.)
5342
+ showWelcome: showWelcome && notes.length === 0,
5049
5343
  dismissWelcome,
5050
5344
  // v0.6
5051
5345
  panelSide,
@@ -5498,8 +5792,8 @@ function QaFab() {
5498
5792
  }
5499
5793
  var SEVERITIES = [
5500
5794
  { value: "bug", labelKey: "sev_bug", icon: "Bug" },
5501
- { value: "question", labelKey: "sev_question" },
5502
- { value: "polish", labelKey: "sev_polish" }
5795
+ { value: "enhance", labelKey: "sev_enhance" },
5796
+ { value: "design", labelKey: "sev_design" }
5503
5797
  ];
5504
5798
  function SeverityChipRow({
5505
5799
  value,
@@ -6244,13 +6538,13 @@ function KindBadge({
6244
6538
  }
6245
6539
  var SEVERITY_CLASS = {
6246
6540
  bug: "qa-bg-danger-tint qa-text-danger",
6247
- question: "qa-bg-warn-tint qa-text-warn",
6248
- polish: "qa-bg-accent-tint qa-text-accent"
6541
+ enhance: "qa-bg-warn-tint qa-text-warn",
6542
+ design: "qa-bg-accent-tint qa-text-accent"
6249
6543
  };
6250
6544
  var SEVERITY_LABEL_KEY = {
6251
6545
  bug: "sev_bug",
6252
- question: "sev_question",
6253
- polish: "sev_polish"
6546
+ enhance: "sev_enhance",
6547
+ design: "sev_design"
6254
6548
  };
6255
6549
  function SeverityChip({ severity, t }) {
6256
6550
  return /* @__PURE__ */ jsxRuntime.jsxs(
@@ -6355,7 +6649,7 @@ function NoteItem({
6355
6649
  notify(t("copy_failed"), { tone: "error" });
6356
6650
  }
6357
6651
  };
6358
- const severityDot = severity === "bug" ? "var(--qa-danger)" : severity === "question" ? "var(--qa-warn)" : "var(--qa-accent)";
6652
+ const severityDot = severity === "bug" ? "var(--qa-danger)" : severity === "enhance" ? "var(--qa-warn)" : "var(--qa-accent)";
6359
6653
  if (dense && !open && !editing) {
6360
6654
  return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
6361
6655
  "button",
@@ -6898,22 +7192,22 @@ function NoteFilterBar() {
6898
7192
  onClick: sev("bug")
6899
7193
  },
6900
7194
  {
6901
- key: "question",
6902
- label: t("sev_question"),
6903
- count: noteCounts.question,
6904
- active: filter.severity === "question",
6905
- icon: "AlertTriangle",
7195
+ key: "enhance",
7196
+ label: t("sev_enhance"),
7197
+ count: noteCounts.enhance,
7198
+ active: filter.severity === "enhance",
7199
+ icon: "Plus",
6906
7200
  tone: "warn",
6907
- onClick: sev("question")
7201
+ onClick: sev("enhance")
6908
7202
  },
6909
7203
  {
6910
- key: "polish",
6911
- label: t("sev_polish"),
6912
- count: noteCounts.polish,
6913
- active: filter.severity === "polish",
7204
+ key: "design",
7205
+ label: t("sev_design"),
7206
+ count: noteCounts.design,
7207
+ active: filter.severity === "design",
6914
7208
  icon: "Pencil",
6915
7209
  tone: "accent",
6916
- onClick: sev("polish")
7210
+ onClick: sev("design")
6917
7211
  },
6918
7212
  {
6919
7213
  key: "open",
@@ -7352,46 +7646,25 @@ function SettingsSheet({ onClose }) {
7352
7646
  );
7353
7647
  }
7354
7648
  function WelcomeCard() {
7355
- const { t, brand, dismissWelcome } = useQa();
7649
+ const { t, dismissWelcome } = useQa();
7356
7650
  return /* @__PURE__ */ jsxRuntime.jsxs(
7357
7651
  "div",
7358
7652
  {
7359
- className: "qa-rounded-xl qa-border qa-border-subtle qa-bg-1 qa-p-3 qa-space-y-2",
7653
+ className: "qa-flex qa-items-center qa-gap-2 qa-rounded-xl qa-border qa-border-subtle qa-bg-1 qa-px-3 qa-py-2",
7360
7654
  role: "note",
7361
7655
  children: [
7362
- /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "qa-m-0 qa-flex qa-items-center qa-gap-1.5 qa-text-sm qa-font-semibold qa-text-hi", children: [
7363
- /* @__PURE__ */ jsxRuntime.jsx(
7364
- "span",
7365
- {
7366
- "aria-hidden": "true",
7367
- className: "qa-shrink-0",
7368
- style: { width: 6, height: 6, background: "var(--qa-accent)" }
7369
- }
7370
- ),
7371
- t("welcome_title", { brand: brand.label })
7372
- ] }),
7373
- /* @__PURE__ */ jsxRuntime.jsxs("ul", { className: "qa-m-0 qa-space-y-1 qa-text-xs qa-text-mid", children: [
7374
- /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-flex qa-items-start qa-gap-1.5", children: [
7375
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 12, className: "qa-shrink-0 qa-mt-1" }),
7376
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("welcome_capture") })
7377
- ] }),
7378
- /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-flex qa-items-start qa-gap-1.5", children: [
7379
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 12, className: "qa-shrink-0 qa-mt-1" }),
7380
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("welcome_say") })
7381
- ] }),
7382
- /* @__PURE__ */ jsxRuntime.jsxs("li", { className: "qa-flex qa-items-start qa-gap-1.5", children: [
7383
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "CheckCircle2", size: 12, className: "qa-shrink-0 qa-mt-1" }),
7384
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: t("welcome_safe") })
7385
- ] })
7386
- ] }),
7656
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Crosshair", size: 13, className: "qa-shrink-0 qa-text-accent" }),
7657
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-flex-1 qa-text-xs qa-text-mid", children: t("welcome_capture") }),
7387
7658
  /* @__PURE__ */ jsxRuntime.jsx(
7388
7659
  "button",
7389
7660
  {
7390
7661
  type: "button",
7391
7662
  onClick: dismissWelcome,
7392
- className: "qa-tap qa-w-full qa-rounded-lg qa-bg-accent qa-px-3 qa-py-1.5 qa-text-xs qa-font-semibold",
7393
- style: { border: "none", cursor: "pointer" },
7394
- children: t("welcome_got_it")
7663
+ "aria-label": t("welcome_got_it"),
7664
+ title: t("welcome_got_it"),
7665
+ className: "qa-tap-icon qa-shrink-0 qa-rounded-full qa-text-mid qa-focus-ring",
7666
+ style: { background: "transparent", border: "none", cursor: "pointer", padding: 2 },
7667
+ children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "X", size: 13 })
7395
7668
  }
7396
7669
  )
7397
7670
  ]
@@ -8858,16 +9131,16 @@ function unlockPageScroll() {
8858
9131
  var DRAG_THRESHOLD2 = 6;
8859
9132
  var TOUCH_DRAG_THRESHOLD = 12;
8860
9133
  var MIN_REGION_SIZE = 8;
8861
- var SEVERITIES2 = ["bug", "question", "polish"];
9134
+ var SEVERITIES2 = ["bug", "design", "enhance"];
8862
9135
  var SEVERITY_ICON = {
8863
9136
  bug: "Bug",
8864
- question: "AlertTriangle",
8865
- polish: "Pencil"
9137
+ design: "Pencil",
9138
+ enhance: "Plus"
8866
9139
  };
8867
9140
  var SEVERITY_LABEL_KEY2 = {
8868
9141
  bug: "sev_bug",
8869
- question: "sev_question",
8870
- polish: "sev_polish"
9142
+ design: "sev_design",
9143
+ enhance: "sev_enhance"
8871
9144
  };
8872
9145
  function clampRegionRect(rect) {
8873
9146
  const vw = typeof window !== "undefined" ? window.innerWidth : rect.left + rect.width;
@@ -9821,7 +10094,7 @@ function CaptureMode() {
9821
10094
  /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-11 qa-text-mid qa-me-1", children: t("severity_label") }),
9822
10095
  SEVERITIES2.map((sev) => {
9823
10096
  const active = severity === sev;
9824
- const toneClass = sev === "bug" ? "qa-bg-danger-tint qa-text-danger" : sev === "question" ? "qa-bg-warn-tint qa-text-warn" : "qa-bg-accent-tint qa-text-accent";
10097
+ const toneClass = sev === "bug" ? "qa-bg-danger-tint qa-text-danger" : sev === "enhance" ? "qa-bg-warn-tint qa-text-warn" : "qa-bg-accent-tint qa-text-accent";
9825
10098
  return /* @__PURE__ */ jsxRuntime.jsxs(
9826
10099
  "button",
9827
10100
  {
@@ -10101,5 +10374,5 @@ function Qapture({ config }) {
10101
10374
  exports.Qapture = Qapture;
10102
10375
  exports.deleteQaDatabase = deleteQaDatabase;
10103
10376
  exports.initQaStudio = initQaStudio;
10104
- //# sourceMappingURL=chunk-COQKFGKZ.cjs.map
10105
- //# sourceMappingURL=chunk-COQKFGKZ.cjs.map
10377
+ //# sourceMappingURL=chunk-WU7QMHPK.cjs.map
10378
+ //# sourceMappingURL=chunk-WU7QMHPK.cjs.map