qapture2 0.7.1 → 0.7.3

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.
package/README.md CHANGED
@@ -315,6 +315,46 @@ rewritten in place, and a render that still throws is retried once with
315
315
  decoration stripped. `npm run modern-css-test` proves it, and proves itself:
316
316
  it first asserts that raw html2canvas *still* dies on the same fixture.
317
317
 
318
+ **v0.7.2 fixed the last of the mis-framing: clicking an element only captured
319
+ the part of it that was on screen.** Dragged regions are clamped into the
320
+ viewport before capture, so they were always safe — but an element *pick* is a
321
+ raw `getBoundingClientRect()`, and a table column, a sidebar, a long form or a
322
+ wide toolbar routinely extends past the fold. You got a fragment. And when the
323
+ element started *above* the viewport, the crop slid down the page to fill its
324
+ height, so the screenshot was the right size showing the wrong content — a
325
+ failure with nothing on screen to reveal it.
326
+
327
+ Captures now render the union of the viewport and the selection, so an element
328
+ that leaves the screen in any direction is rendered whole, up to 4000px per
329
+ side (past a ~64 MB device-pixel render budget the scale drops, not the
330
+ framing). The exact engine can't photograph off-screen pixels, so there the
331
+ selection is trimmed to the visible part instead — cropped, never displaced.
332
+ `npm run element-capture-test` measures all four cases.
333
+
334
+ **v0.7.3 corrected the half of that which was wrong.** Chasing an element's
335
+ whole box is only right when the whole box is *drawn*. Inside a scroll
336
+ container it isn't: a dashboard scrolls an inner `overflow:auto` box, so a
337
+ 3000px column inside a 700px box is 700px of pixels and 2300px of nothing.
338
+ 0.7.2 rendered the nothing — measured at 23.3% element, 76.7% empty page, with
339
+ the real content squeezed from 1040px wide to 312px by the long-edge cap.
340
+
341
+ Captures are now clipped to what the browser actually paints: the element's box
342
+ intersected with every ancestor that clips it, on either axis, up to `<html>`.
343
+ The viewport is deliberately not a clipper, so a long form on an ordinary
344
+ scrolling page still captures in full. Same fixture after: 100% element, full
345
+ 1040×1400. The hover outline is clipped identically, so what you highlight is
346
+ what you get.
347
+
348
+ ### "The screenshot looks simulated"
349
+
350
+ Because by default it is. The DOM engine re-draws your page; fonts shift,
351
+ shadows and gradients flatten, `<canvas>` and video come out blank. From
352
+ v0.7.3 a redrawn capture says so under the preview and offers a one-tap **Use
353
+ real screenshots**, which grants tab-share and re-shoots the same selection
354
+ with the exact engine below. Chromium desktop only — on Safari and Firefox the
355
+ redraw is the only engine there is, so for screenshot-critical QA, test in
356
+ Chrome.
357
+
318
358
  ### `exact` — opt-in, pixel-for-pixel
319
359
 
320
360
  Uses the Screen Capture API to photograph **this tab's real composited
@@ -1775,6 +1775,8 @@ var STR = {
1775
1775
  expected_label: "Expected",
1776
1776
  exact_label: "Pixel-exact shots",
1777
1777
  exact_hint: "Screenshots become a real photo of the tab instead of a redraw. Asks once to share this tab; nothing leaves your device.",
1778
+ redraw_notice: "This is a redraw of the page, not a real photo.",
1779
+ redraw_action: "Use real screenshots",
1778
1780
  exact_on: "Pixel-exact screenshots on",
1779
1781
  exact_off: "Back to redrawn screenshots",
1780
1782
  exact_declined: "Staying on redrawn screenshots",
@@ -1969,6 +1971,8 @@ var STR = {
1969
1971
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1970
1972
  exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
1971
1973
  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.",
1974
+ 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.",
1975
+ redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1972
1976
  exact_on: "\u062A\u0645 \u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629",
1973
1977
  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",
1974
1978
  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",
@@ -2530,6 +2534,8 @@ var FALLBACK_PAGE_BACKGROUND = "#ffffff";
2530
2534
  var MAX_SHOT_EDGE = 1800;
2531
2535
  var WEBP_QUALITY = 0.92;
2532
2536
  var STUCK_ATTR = "data-qa-stuck";
2537
+ var MAX_CAPTURE_EDGE = 4e3;
2538
+ var MAX_RENDER_DEVICE_PIXELS = 16e6;
2533
2539
  function withTimeout(promise, ms) {
2534
2540
  let timer;
2535
2541
  return Promise.race([
@@ -2695,20 +2701,94 @@ function cropCanvas(full, rect, scale) {
2695
2701
  ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
2696
2702
  return out;
2697
2703
  }
2704
+ function viewportSize() {
2705
+ return {
2706
+ vw: document.documentElement.clientWidth || window.innerWidth,
2707
+ vh: document.documentElement.clientHeight || window.innerHeight
2708
+ };
2709
+ }
2710
+ function clipToPaintedArea(el, rect) {
2711
+ let left = rect.left;
2712
+ let top = rect.top;
2713
+ let right = rect.left + rect.width;
2714
+ let bottom = rect.top + rect.height;
2715
+ let node = el.parentElement;
2716
+ while (node) {
2717
+ let style;
2718
+ try {
2719
+ style = getComputedStyle(node);
2720
+ } catch {
2721
+ break;
2722
+ }
2723
+ const clipsX = style.overflowX !== "visible";
2724
+ const clipsY = style.overflowY !== "visible";
2725
+ if (clipsX || clipsY) {
2726
+ const b = node.getBoundingClientRect();
2727
+ const padLeft = b.left + node.clientLeft;
2728
+ const padTop = b.top + node.clientTop;
2729
+ if (clipsX) {
2730
+ left = Math.max(left, padLeft);
2731
+ right = Math.min(right, padLeft + node.clientWidth);
2732
+ }
2733
+ if (clipsY) {
2734
+ top = Math.max(top, padTop);
2735
+ bottom = Math.min(bottom, padTop + node.clientHeight);
2736
+ }
2737
+ }
2738
+ node = node.parentElement;
2739
+ }
2740
+ if (right - left < 2 || bottom - top < 2) return rect;
2741
+ return { left, top, width: right - left, height: bottom - top };
2742
+ }
2743
+ function intersectViewport(rect) {
2744
+ const { vw, vh } = viewportSize();
2745
+ const left = Math.max(0, rect.left);
2746
+ const top = Math.max(0, rect.top);
2747
+ const right = Math.min(vw, rect.left + rect.width);
2748
+ const bottom = Math.min(vh, rect.top + rect.height);
2749
+ if (right - left < 2 || bottom - top < 2) return null;
2750
+ return { left, top, width: right - left, height: bottom - top };
2751
+ }
2752
+ function planRender(rect, sx, sy, vw, vh) {
2753
+ let left = rect.left + sx;
2754
+ let top = rect.top + sy;
2755
+ let width = rect.width;
2756
+ let height = rect.height;
2757
+ if (width > MAX_CAPTURE_EDGE) {
2758
+ left = Math.max(left, Math.min(sx, left + width - MAX_CAPTURE_EDGE));
2759
+ width = MAX_CAPTURE_EDGE;
2760
+ }
2761
+ if (height > MAX_CAPTURE_EDGE) {
2762
+ top = Math.max(top, Math.min(sy, top + height - MAX_CAPTURE_EDGE));
2763
+ height = MAX_CAPTURE_EDGE;
2764
+ }
2765
+ const x = Math.floor(Math.min(sx, left));
2766
+ const y = Math.floor(Math.min(sy, top));
2767
+ const right = Math.ceil(Math.max(sx + vw, left + width));
2768
+ const bottom = Math.ceil(Math.max(sy + vh, top + height));
2769
+ return {
2770
+ render: { x, y, width: right - x, height: bottom - y },
2771
+ crop: { left: left - x, top: top - y, width, height }
2772
+ };
2773
+ }
2698
2774
  async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2699
2775
  const { default: html2canvas } = await import('html2canvas');
2700
- const scale = Math.min(window.devicePixelRatio || 1, 2);
2701
- const vw = document.documentElement.clientWidth || window.innerWidth;
2702
- const vh = document.documentElement.clientHeight || window.innerHeight;
2776
+ const { vw, vh } = viewportSize();
2777
+ const plan = planRender(rect, sx, sy, vw, vh);
2778
+ let scale = Math.min(window.devicePixelRatio || 1, 2);
2779
+ const area = plan.render.width * plan.render.height;
2780
+ if (area * scale * scale > MAX_RENDER_DEVICE_PIXELS) {
2781
+ scale = Math.sqrt(MAX_RENDER_DEVICE_PIXELS / area);
2782
+ }
2703
2783
  const marked = markStuckElements();
2704
2784
  try {
2705
2785
  const full = await withTimeout(
2706
2786
  html2canvas(document.body, {
2707
- // Render exactly the current viewport, in document coordinates.
2708
- x: sx,
2709
- y: sy,
2710
- width: vw,
2711
- height: vh,
2787
+ // The box to render, in document coordinates.
2788
+ x: plan.render.x,
2789
+ y: plan.render.y,
2790
+ width: plan.render.width,
2791
+ height: plan.render.height,
2712
2792
  scale,
2713
2793
  useCORS: true,
2714
2794
  allowTaint: true,
@@ -2717,6 +2797,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2717
2797
  // CSS Color 4 treatment the document walk applies.
2718
2798
  backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
2719
2799
  logging: false,
2800
+ // The clone's own scroll + viewport, which is what `position: fixed`
2801
+ // resolves against. It stays the LIVE viewport even when the render
2802
+ // box is bigger, so fixed chrome is drawn where the tester sees it
2803
+ // rather than smeared over the whole render.
2720
2804
  scrollX: sx,
2721
2805
  scrollY: sy,
2722
2806
  windowWidth: vw,
@@ -2730,7 +2814,7 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2730
2814
  HTML2CANVAS_TIMEOUT_MS
2731
2815
  );
2732
2816
  if (!full) return null;
2733
- return cropCanvas(full, rect, scale);
2817
+ return cropCanvas(full, plan.crop, scale);
2734
2818
  } finally {
2735
2819
  unmarkStuckElements(marked);
2736
2820
  }
@@ -2743,8 +2827,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
2743
2827
  const sx = scroll?.x ?? window.scrollX;
2744
2828
  const sy = scroll?.y ?? window.scrollY;
2745
2829
  if (prefer !== "dom" && getExactCaptureStatus() === "live") {
2830
+ const visible = intersectViewport(rect);
2831
+ if (!visible) return { status: "empty" };
2746
2832
  try {
2747
- const canvas = await withOverlayHidden(() => grabExactRegion(rect));
2833
+ const canvas = await withOverlayHidden(() => grabExactRegion(visible));
2748
2834
  if (canvas) {
2749
2835
  const blob = await encodeShot(canvas);
2750
2836
  if (blob) return { status: "ok", blob, engine: "exact" };
@@ -8825,6 +8911,7 @@ function CaptureMode() {
8825
8911
  const [candidate, setCandidate] = useState(null);
8826
8912
  const [regionMode, setRegionMode] = useState(false);
8827
8913
  const [shot, setShot] = useState(null);
8914
+ const [shotEngine, setShotEngine] = useState(null);
8828
8915
  const [shotUrl, setShotUrl] = useState(null);
8829
8916
  const [capturing, setCapturing] = useState(false);
8830
8917
  const [captureError, setCaptureError] = useState(false);
@@ -8867,6 +8954,7 @@ function CaptureMode() {
8867
8954
  return;
8868
8955
  }
8869
8956
  setCaptureError(outcome.status === "failed");
8957
+ setShotEngine(outcome.status === "ok" ? outcome.engine : null);
8870
8958
  setShot(blob);
8871
8959
  setShotUrl((old) => {
8872
8960
  if (old) URL.revokeObjectURL(old);
@@ -8931,7 +9019,9 @@ function CaptureMode() {
8931
9019
  }
8932
9020
  const r = el.getBoundingClientRect();
8933
9021
  setHover({
8934
- rect: { top: r.top, left: r.left, width: r.width, height: r.height },
9022
+ // Highlight only what will actually be captured, so the outline is a
9023
+ // promise the screenshot can keep.
9024
+ rect: clipToPaintedArea(el, { top: r.top, left: r.left, width: r.width, height: r.height }),
8935
9025
  selector: getStableSelector(el)
8936
9026
  });
8937
9027
  }
@@ -8978,7 +9068,10 @@ function CaptureMode() {
8978
9068
  const r = el.getBoundingClientRect();
8979
9069
  const sel = {
8980
9070
  kind: "element",
8981
- rect: { top: r.top, left: r.left, width: r.width, height: r.height },
9071
+ // What the browser PAINTS of this element, not its full bounding box —
9072
+ // a row inside a scrolled table is only ever drawn where the table shows
9073
+ // it. See clipToPaintedArea().
9074
+ rect: clipToPaintedArea(el, { top: r.top, left: r.left, width: r.width, height: r.height }),
8982
9075
  selector: getStableSelector(el),
8983
9076
  text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
8984
9077
  tagName: el.tagName.toLowerCase()
@@ -9589,7 +9682,7 @@ function CaptureMode() {
9589
9682
  )
9590
9683
  ] }),
9591
9684
  /* @__PURE__ */ jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
9592
- /* @__PURE__ */ jsx(
9685
+ /* @__PURE__ */ jsxs(
9593
9686
  "div",
9594
9687
  {
9595
9688
  className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
@@ -9597,66 +9690,95 @@ function CaptureMode() {
9597
9690
  borderColor: "var(--qa-border-subtle)",
9598
9691
  background: "var(--qa-surface-0)"
9599
9692
  },
9600
- children: capturing ? /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
9601
- /* @__PURE__ */ jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
9602
- t("capturing")
9603
- ] }) : shotUrl ? /* @__PURE__ */ jsxs(
9604
- "button",
9605
- {
9606
- type: "button",
9607
- onClick: () => setDrawing(true),
9608
- title: t("draw_label"),
9609
- "aria-label": t("draw_label"),
9610
- className: "qa-relative qa-group qa-tap",
9611
- style: { background: "transparent", border: "none", padding: 0, cursor: "pointer" },
9612
- children: [
9613
- /* @__PURE__ */ jsx(
9614
- "img",
9615
- {
9616
- src: shotUrl,
9617
- alt: "capture",
9618
- className: "qa-max-h-32 qa-rounded-md"
9619
- }
9620
- ),
9693
+ children: [
9694
+ capturing ? /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
9695
+ /* @__PURE__ */ jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
9696
+ t("capturing")
9697
+ ] }) : shotUrl ? /* @__PURE__ */ jsxs(
9698
+ "button",
9699
+ {
9700
+ type: "button",
9701
+ onClick: () => setDrawing(true),
9702
+ title: t("draw_label"),
9703
+ "aria-label": t("draw_label"),
9704
+ className: "qa-relative qa-group qa-tap",
9705
+ style: { background: "transparent", border: "none", padding: 0, cursor: "pointer" },
9706
+ children: [
9707
+ /* @__PURE__ */ jsx(
9708
+ "img",
9709
+ {
9710
+ src: shotUrl,
9711
+ alt: "capture",
9712
+ className: "qa-max-h-32 qa-rounded-md"
9713
+ }
9714
+ ),
9715
+ /* @__PURE__ */ jsxs(
9716
+ "span",
9717
+ {
9718
+ className: "qa-absolute qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10",
9719
+ style: {
9720
+ bottom: 4,
9721
+ insetInlineEnd: 4,
9722
+ background: "var(--qa-surface-1)",
9723
+ color: "var(--qa-ink-hi)",
9724
+ border: "1px solid var(--qa-border-subtle)"
9725
+ },
9726
+ children: [
9727
+ /* @__PURE__ */ jsx(Icon, { name: "Pencil", size: 10 }),
9728
+ t("draw_label")
9729
+ ]
9730
+ }
9731
+ )
9732
+ ]
9733
+ }
9734
+ ) : captureError ? (
9735
+ // The render broke rather than being skipped — offer a retry
9736
+ // against the same selection instead of a dead-end message.
9737
+ /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
9738
+ /* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
9621
9739
  /* @__PURE__ */ jsxs(
9622
- "span",
9740
+ "button",
9623
9741
  {
9624
- className: "qa-absolute qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10",
9625
- style: {
9626
- bottom: 4,
9627
- insetInlineEnd: 4,
9628
- background: "var(--qa-surface-1)",
9629
- color: "var(--qa-ink-hi)",
9630
- border: "1px solid var(--qa-border-subtle)"
9631
- },
9742
+ type: "button",
9743
+ onClick: () => selection && void runCapture(selection.rect),
9744
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
9745
+ style: { background: "transparent", cursor: "pointer" },
9632
9746
  children: [
9633
- /* @__PURE__ */ jsx(Icon, { name: "Pencil", size: 10 }),
9634
- t("draw_label")
9747
+ /* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
9748
+ t("retry")
9635
9749
  ]
9636
9750
  }
9637
9751
  )
9638
- ]
9639
- }
9640
- ) : captureError ? (
9641
- // The render broke rather than being skipped — offer a retry
9642
- // against the same selection instead of a dead-end message.
9643
- /* @__PURE__ */ jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
9644
- /* @__PURE__ */ jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
9645
- /* @__PURE__ */ jsxs(
9646
- "button",
9647
- {
9648
- type: "button",
9649
- onClick: () => selection && void runCapture(selection.rect),
9650
- className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
9651
- style: { background: "transparent", cursor: "pointer" },
9652
- children: [
9653
- /* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
9654
- t("retry")
9655
- ]
9656
- }
9657
- )
9658
- ] })
9659
- ) : /* @__PURE__ */ jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
9752
+ ] })
9753
+ ) : /* @__PURE__ */ jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") }),
9754
+ shotUrl && shotEngine === "dom" && exactShots.supported && exactShots.status !== "live" && /* @__PURE__ */ jsxs(
9755
+ "span",
9756
+ {
9757
+ "data-qa-redraw-offer": "true",
9758
+ className: "qa-mt-1.5 qa-flex qa-items-center qa-justify-center qa-flex-wrap qa-gap-1.5 qa-text-10 qa-text-mid",
9759
+ children: [
9760
+ t("redraw_notice"),
9761
+ /* @__PURE__ */ jsxs(
9762
+ "button",
9763
+ {
9764
+ type: "button",
9765
+ onClick: () => void (async () => {
9766
+ const granted = await enableExactShots();
9767
+ if (granted && selection) void runCapture(selection.rect);
9768
+ })(),
9769
+ title: t("exact_hint"),
9770
+ 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",
9771
+ style: { background: "transparent", cursor: "pointer" },
9772
+ children: [
9773
+ /* @__PURE__ */ jsx(Icon, { name: "Camera", size: 10 }),
9774
+ t("redraw_action")
9775
+ ]
9776
+ }
9777
+ )
9778
+ ]
9779
+ }
9780
+ )
9781
+ ]
9660
9782
  }
9661
9783
  ),
9662
9784
  /* @__PURE__ */ jsx(LocationReveal, { target: selection }),
@@ -9948,5 +10070,5 @@ function Qapture({ config }) {
9948
10070
  }
9949
10071
 
9950
10072
  export { Qapture, deleteQaDatabase, initQaStudio };
9951
- //# sourceMappingURL=chunk-JSEFC2VP.js.map
9952
- //# sourceMappingURL=chunk-JSEFC2VP.js.map
10073
+ //# sourceMappingURL=chunk-BM3ESR2O.js.map
10074
+ //# sourceMappingURL=chunk-BM3ESR2O.js.map
@@ -1782,6 +1782,8 @@ var STR = {
1782
1782
  expected_label: "Expected",
1783
1783
  exact_label: "Pixel-exact shots",
1784
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.",
1785
+ redraw_notice: "This is a redraw of the page, not a real photo.",
1786
+ redraw_action: "Use real screenshots",
1785
1787
  exact_on: "Pixel-exact screenshots on",
1786
1788
  exact_off: "Back to redrawn screenshots",
1787
1789
  exact_declined: "Staying on redrawn screenshots",
@@ -1976,6 +1978,8 @@ var STR = {
1976
1978
  expected_label: "\u0627\u0644\u0645\u062A\u0648\u0642\u0639",
1977
1979
  exact_label: "\u0644\u0642\u0637\u0627\u062A \u0645\u0637\u0627\u0628\u0642\u0629 \u062A\u0645\u0627\u0645\u064B\u0627",
1978
1980
  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.",
1981
+ 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.",
1982
+ redraw_action: "\u0627\u0633\u062A\u062E\u062F\u0645 \u0644\u0642\u0637\u0627\u062A \u062D\u0642\u064A\u0642\u064A\u0629",
1979
1983
  exact_on: "\u062A\u0645 \u062A\u0641\u0639\u064A\u0644 \u0627\u0644\u0644\u0642\u0637\u0627\u062A \u0627\u0644\u0645\u0637\u0627\u0628\u0642\u0629",
1980
1984
  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",
1981
1985
  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",
@@ -2537,6 +2541,8 @@ var FALLBACK_PAGE_BACKGROUND = "#ffffff";
2537
2541
  var MAX_SHOT_EDGE = 1800;
2538
2542
  var WEBP_QUALITY = 0.92;
2539
2543
  var STUCK_ATTR = "data-qa-stuck";
2544
+ var MAX_CAPTURE_EDGE = 4e3;
2545
+ var MAX_RENDER_DEVICE_PIXELS = 16e6;
2540
2546
  function withTimeout(promise, ms) {
2541
2547
  let timer;
2542
2548
  return Promise.race([
@@ -2702,20 +2708,94 @@ function cropCanvas(full, rect, scale) {
2702
2708
  ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
2703
2709
  return out;
2704
2710
  }
2711
+ function viewportSize() {
2712
+ return {
2713
+ vw: document.documentElement.clientWidth || window.innerWidth,
2714
+ vh: document.documentElement.clientHeight || window.innerHeight
2715
+ };
2716
+ }
2717
+ function clipToPaintedArea(el, rect) {
2718
+ let left = rect.left;
2719
+ let top = rect.top;
2720
+ let right = rect.left + rect.width;
2721
+ let bottom = rect.top + rect.height;
2722
+ let node = el.parentElement;
2723
+ while (node) {
2724
+ let style;
2725
+ try {
2726
+ style = getComputedStyle(node);
2727
+ } catch {
2728
+ break;
2729
+ }
2730
+ const clipsX = style.overflowX !== "visible";
2731
+ const clipsY = style.overflowY !== "visible";
2732
+ if (clipsX || clipsY) {
2733
+ const b = node.getBoundingClientRect();
2734
+ const padLeft = b.left + node.clientLeft;
2735
+ const padTop = b.top + node.clientTop;
2736
+ if (clipsX) {
2737
+ left = Math.max(left, padLeft);
2738
+ right = Math.min(right, padLeft + node.clientWidth);
2739
+ }
2740
+ if (clipsY) {
2741
+ top = Math.max(top, padTop);
2742
+ bottom = Math.min(bottom, padTop + node.clientHeight);
2743
+ }
2744
+ }
2745
+ node = node.parentElement;
2746
+ }
2747
+ if (right - left < 2 || bottom - top < 2) return rect;
2748
+ return { left, top, width: right - left, height: bottom - top };
2749
+ }
2750
+ function intersectViewport(rect) {
2751
+ const { vw, vh } = viewportSize();
2752
+ const left = Math.max(0, rect.left);
2753
+ const top = Math.max(0, rect.top);
2754
+ const right = Math.min(vw, rect.left + rect.width);
2755
+ const bottom = Math.min(vh, rect.top + rect.height);
2756
+ if (right - left < 2 || bottom - top < 2) return null;
2757
+ return { left, top, width: right - left, height: bottom - top };
2758
+ }
2759
+ function planRender(rect, sx, sy, vw, vh) {
2760
+ let left = rect.left + sx;
2761
+ let top = rect.top + sy;
2762
+ let width = rect.width;
2763
+ let height = rect.height;
2764
+ if (width > MAX_CAPTURE_EDGE) {
2765
+ left = Math.max(left, Math.min(sx, left + width - MAX_CAPTURE_EDGE));
2766
+ width = MAX_CAPTURE_EDGE;
2767
+ }
2768
+ if (height > MAX_CAPTURE_EDGE) {
2769
+ top = Math.max(top, Math.min(sy, top + height - MAX_CAPTURE_EDGE));
2770
+ height = MAX_CAPTURE_EDGE;
2771
+ }
2772
+ const x = Math.floor(Math.min(sx, left));
2773
+ const y = Math.floor(Math.min(sy, top));
2774
+ const right = Math.ceil(Math.max(sx + vw, left + width));
2775
+ const bottom = Math.ceil(Math.max(sy + vh, top + height));
2776
+ return {
2777
+ render: { x, y, width: right - x, height: bottom - y },
2778
+ crop: { left: left - x, top: top - y, width, height }
2779
+ };
2780
+ }
2705
2781
  async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2706
2782
  const { default: html2canvas } = await import('html2canvas');
2707
- const scale = Math.min(window.devicePixelRatio || 1, 2);
2708
- const vw = document.documentElement.clientWidth || window.innerWidth;
2709
- const vh = document.documentElement.clientHeight || window.innerHeight;
2783
+ const { vw, vh } = viewportSize();
2784
+ const plan = planRender(rect, sx, sy, vw, vh);
2785
+ let scale = Math.min(window.devicePixelRatio || 1, 2);
2786
+ const area = plan.render.width * plan.render.height;
2787
+ if (area * scale * scale > MAX_RENDER_DEVICE_PIXELS) {
2788
+ scale = Math.sqrt(MAX_RENDER_DEVICE_PIXELS / area);
2789
+ }
2710
2790
  const marked = markStuckElements();
2711
2791
  try {
2712
2792
  const full = await withTimeout(
2713
2793
  html2canvas(document.body, {
2714
- // Render exactly the current viewport, in document coordinates.
2715
- x: sx,
2716
- y: sy,
2717
- width: vw,
2718
- height: vh,
2794
+ // The box to render, in document coordinates.
2795
+ x: plan.render.x,
2796
+ y: plan.render.y,
2797
+ width: plan.render.width,
2798
+ height: plan.render.height,
2719
2799
  scale,
2720
2800
  useCORS: true,
2721
2801
  allowTaint: true,
@@ -2724,6 +2804,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2724
2804
  // CSS Color 4 treatment the document walk applies.
2725
2805
  backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
2726
2806
  logging: false,
2807
+ // The clone's own scroll + viewport, which is what `position: fixed`
2808
+ // resolves against. It stays the LIVE viewport even when the render
2809
+ // box is bigger, so fixed chrome is drawn where the tester sees it
2810
+ // rather than smeared over the whole render.
2727
2811
  scrollX: sx,
2728
2812
  scrollY: sy,
2729
2813
  windowWidth: vw,
@@ -2737,7 +2821,7 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
2737
2821
  HTML2CANVAS_TIMEOUT_MS
2738
2822
  );
2739
2823
  if (!full) return null;
2740
- return cropCanvas(full, rect, scale);
2824
+ return cropCanvas(full, plan.crop, scale);
2741
2825
  } finally {
2742
2826
  unmarkStuckElements(marked);
2743
2827
  }
@@ -2750,8 +2834,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
2750
2834
  const sx = scroll?.x ?? window.scrollX;
2751
2835
  const sy = scroll?.y ?? window.scrollY;
2752
2836
  if (prefer !== "dom" && getExactCaptureStatus() === "live") {
2837
+ const visible = intersectViewport(rect);
2838
+ if (!visible) return { status: "empty" };
2753
2839
  try {
2754
- const canvas = await withOverlayHidden(() => grabExactRegion(rect));
2840
+ const canvas = await withOverlayHidden(() => grabExactRegion(visible));
2755
2841
  if (canvas) {
2756
2842
  const blob = await encodeShot(canvas);
2757
2843
  if (blob) return { status: "ok", blob, engine: "exact" };
@@ -8832,6 +8918,7 @@ function CaptureMode() {
8832
8918
  const [candidate, setCandidate] = React.useState(null);
8833
8919
  const [regionMode, setRegionMode] = React.useState(false);
8834
8920
  const [shot, setShot] = React.useState(null);
8921
+ const [shotEngine, setShotEngine] = React.useState(null);
8835
8922
  const [shotUrl, setShotUrl] = React.useState(null);
8836
8923
  const [capturing, setCapturing] = React.useState(false);
8837
8924
  const [captureError, setCaptureError] = React.useState(false);
@@ -8874,6 +8961,7 @@ function CaptureMode() {
8874
8961
  return;
8875
8962
  }
8876
8963
  setCaptureError(outcome.status === "failed");
8964
+ setShotEngine(outcome.status === "ok" ? outcome.engine : null);
8877
8965
  setShot(blob);
8878
8966
  setShotUrl((old) => {
8879
8967
  if (old) URL.revokeObjectURL(old);
@@ -8938,7 +9026,9 @@ function CaptureMode() {
8938
9026
  }
8939
9027
  const r = el.getBoundingClientRect();
8940
9028
  setHover({
8941
- rect: { top: r.top, left: r.left, width: r.width, height: r.height },
9029
+ // Highlight only what will actually be captured, so the outline is a
9030
+ // promise the screenshot can keep.
9031
+ rect: clipToPaintedArea(el, { top: r.top, left: r.left, width: r.width, height: r.height }),
8942
9032
  selector: getStableSelector(el)
8943
9033
  });
8944
9034
  }
@@ -8985,7 +9075,10 @@ function CaptureMode() {
8985
9075
  const r = el.getBoundingClientRect();
8986
9076
  const sel = {
8987
9077
  kind: "element",
8988
- rect: { top: r.top, left: r.left, width: r.width, height: r.height },
9078
+ // What the browser PAINTS of this element, not its full bounding box —
9079
+ // a row inside a scrolled table is only ever drawn where the table shows
9080
+ // it. See clipToPaintedArea().
9081
+ rect: clipToPaintedArea(el, { top: r.top, left: r.left, width: r.width, height: r.height }),
8989
9082
  selector: getStableSelector(el),
8990
9083
  text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
8991
9084
  tagName: el.tagName.toLowerCase()
@@ -9596,7 +9689,7 @@ function CaptureMode() {
9596
9689
  )
9597
9690
  ] }),
9598
9691
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
9599
- /* @__PURE__ */ jsxRuntime.jsx(
9692
+ /* @__PURE__ */ jsxRuntime.jsxs(
9600
9693
  "div",
9601
9694
  {
9602
9695
  className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
@@ -9604,66 +9697,95 @@ function CaptureMode() {
9604
9697
  borderColor: "var(--qa-border-subtle)",
9605
9698
  background: "var(--qa-surface-0)"
9606
9699
  },
9607
- children: capturing ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
9608
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
9609
- t("capturing")
9610
- ] }) : shotUrl ? /* @__PURE__ */ jsxRuntime.jsxs(
9611
- "button",
9612
- {
9613
- type: "button",
9614
- onClick: () => setDrawing(true),
9615
- title: t("draw_label"),
9616
- "aria-label": t("draw_label"),
9617
- className: "qa-relative qa-group qa-tap",
9618
- style: { background: "transparent", border: "none", padding: 0, cursor: "pointer" },
9619
- children: [
9620
- /* @__PURE__ */ jsxRuntime.jsx(
9621
- "img",
9622
- {
9623
- src: shotUrl,
9624
- alt: "capture",
9625
- className: "qa-max-h-32 qa-rounded-md"
9626
- }
9627
- ),
9700
+ children: [
9701
+ capturing ? /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-items-center qa-gap-2 qa-py-4 qa-text-xs qa-text-accent", children: [
9702
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Loader2", size: 16, className: "qa-animate-spin" }),
9703
+ t("capturing")
9704
+ ] }) : shotUrl ? /* @__PURE__ */ jsxRuntime.jsxs(
9705
+ "button",
9706
+ {
9707
+ type: "button",
9708
+ onClick: () => setDrawing(true),
9709
+ title: t("draw_label"),
9710
+ "aria-label": t("draw_label"),
9711
+ className: "qa-relative qa-group qa-tap",
9712
+ style: { background: "transparent", border: "none", padding: 0, cursor: "pointer" },
9713
+ children: [
9714
+ /* @__PURE__ */ jsxRuntime.jsx(
9715
+ "img",
9716
+ {
9717
+ src: shotUrl,
9718
+ alt: "capture",
9719
+ className: "qa-max-h-32 qa-rounded-md"
9720
+ }
9721
+ ),
9722
+ /* @__PURE__ */ jsxRuntime.jsxs(
9723
+ "span",
9724
+ {
9725
+ className: "qa-absolute qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10",
9726
+ style: {
9727
+ bottom: 4,
9728
+ insetInlineEnd: 4,
9729
+ background: "var(--qa-surface-1)",
9730
+ color: "var(--qa-ink-hi)",
9731
+ border: "1px solid var(--qa-border-subtle)"
9732
+ },
9733
+ children: [
9734
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 10 }),
9735
+ t("draw_label")
9736
+ ]
9737
+ }
9738
+ )
9739
+ ]
9740
+ }
9741
+ ) : captureError ? (
9742
+ // The render broke rather than being skipped — offer a retry
9743
+ // against the same selection instead of a dead-end message.
9744
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
9745
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
9628
9746
  /* @__PURE__ */ jsxRuntime.jsxs(
9629
- "span",
9747
+ "button",
9630
9748
  {
9631
- className: "qa-absolute qa-inline-flex qa-items-center qa-gap-1 qa-rounded-full qa-px-1.5 qa-py-0.5 qa-text-10",
9632
- style: {
9633
- bottom: 4,
9634
- insetInlineEnd: 4,
9635
- background: "var(--qa-surface-1)",
9636
- color: "var(--qa-ink-hi)",
9637
- border: "1px solid var(--qa-border-subtle)"
9638
- },
9749
+ type: "button",
9750
+ onClick: () => selection && void runCapture(selection.rect),
9751
+ className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
9752
+ style: { background: "transparent", cursor: "pointer" },
9639
9753
  children: [
9640
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Pencil", size: 10 }),
9641
- t("draw_label")
9754
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
9755
+ t("retry")
9642
9756
  ]
9643
9757
  }
9644
9758
  )
9645
- ]
9646
- }
9647
- ) : captureError ? (
9648
- // The render broke rather than being skipped — offer a retry
9649
- // against the same selection instead of a dead-end message.
9650
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "qa-flex qa-flex-col qa-items-center qa-gap-2 qa-py-3", children: [
9651
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-text-xs qa-text-red-600", children: t("capture_failed") }),
9652
- /* @__PURE__ */ jsxRuntime.jsxs(
9653
- "button",
9654
- {
9655
- type: "button",
9656
- onClick: () => selection && void runCapture(selection.rect),
9657
- className: "qa-tap qa-inline-flex qa-items-center qa-gap-1.5 qa-rounded-md qa-border qa-border-subtle qa-px-2 qa-py-1 qa-text-xs qa-text-mid qa-focus-ring",
9658
- style: { background: "transparent", cursor: "pointer" },
9659
- children: [
9660
- /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
9661
- t("retry")
9662
- ]
9663
- }
9664
- )
9665
- ] })
9666
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") })
9759
+ ] })
9760
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "qa-py-4 qa-text-xs qa-text-slate-400", children: t("no_shot") }),
9761
+ shotUrl && shotEngine === "dom" && exactShots.supported && exactShots.status !== "live" && /* @__PURE__ */ jsxRuntime.jsxs(
9762
+ "span",
9763
+ {
9764
+ "data-qa-redraw-offer": "true",
9765
+ className: "qa-mt-1.5 qa-flex qa-items-center qa-justify-center qa-flex-wrap qa-gap-1.5 qa-text-10 qa-text-mid",
9766
+ children: [
9767
+ t("redraw_notice"),
9768
+ /* @__PURE__ */ jsxRuntime.jsxs(
9769
+ "button",
9770
+ {
9771
+ type: "button",
9772
+ onClick: () => void (async () => {
9773
+ const granted = await enableExactShots();
9774
+ if (granted && selection) void runCapture(selection.rect);
9775
+ })(),
9776
+ title: t("exact_hint"),
9777
+ 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",
9778
+ style: { background: "transparent", cursor: "pointer" },
9779
+ children: [
9780
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "Camera", size: 10 }),
9781
+ t("redraw_action")
9782
+ ]
9783
+ }
9784
+ )
9785
+ ]
9786
+ }
9787
+ )
9788
+ ]
9667
9789
  }
9668
9790
  ),
9669
9791
  /* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
@@ -9957,5 +10079,5 @@ function Qapture({ config }) {
9957
10079
  exports.Qapture = Qapture;
9958
10080
  exports.deleteQaDatabase = deleteQaDatabase;
9959
10081
  exports.initQaStudio = initQaStudio;
9960
- //# sourceMappingURL=chunk-WAOYPLZA.cjs.map
9961
- //# sourceMappingURL=chunk-WAOYPLZA.cjs.map
10082
+ //# sourceMappingURL=chunk-C5WJFSMN.cjs.map
10083
+ //# sourceMappingURL=chunk-C5WJFSMN.cjs.map
package/dist/index.cjs CHANGED
@@ -1,24 +1,24 @@
1
1
  'use strict';
2
2
 
3
- var chunkWAOYPLZA_cjs = require('./chunk-WAOYPLZA.cjs');
3
+ var chunkC5WJFSMN_cjs = require('./chunk-C5WJFSMN.cjs');
4
4
 
5
5
 
6
6
 
7
7
  Object.defineProperty(exports, "QaStudio", {
8
8
  enumerable: true,
9
- get: function () { return chunkWAOYPLZA_cjs.Qapture; }
9
+ get: function () { return chunkC5WJFSMN_cjs.Qapture; }
10
10
  });
11
11
  Object.defineProperty(exports, "Qapture", {
12
12
  enumerable: true,
13
- get: function () { return chunkWAOYPLZA_cjs.Qapture; }
13
+ get: function () { return chunkC5WJFSMN_cjs.Qapture; }
14
14
  });
15
15
  Object.defineProperty(exports, "deleteQaDatabase", {
16
16
  enumerable: true,
17
- get: function () { return chunkWAOYPLZA_cjs.deleteQaDatabase; }
17
+ get: function () { return chunkC5WJFSMN_cjs.deleteQaDatabase; }
18
18
  });
19
19
  Object.defineProperty(exports, "initQaStudio", {
20
20
  enumerable: true,
21
- get: function () { return chunkWAOYPLZA_cjs.initQaStudio; }
21
+ get: function () { return chunkC5WJFSMN_cjs.initQaStudio; }
22
22
  });
23
23
  //# sourceMappingURL=index.cjs.map
24
24
  //# sourceMappingURL=index.cjs.map
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { Qapture as QaStudio, Qapture, deleteQaDatabase, initQaStudio } from './chunk-JSEFC2VP.js';
1
+ export { Qapture as QaStudio, Qapture, deleteQaDatabase, initQaStudio } from './chunk-BM3ESR2O.js';
2
2
  //# sourceMappingURL=index.js.map
3
3
  //# sourceMappingURL=index.js.map
package/dist/next.cjs CHANGED
@@ -1,21 +1,21 @@
1
1
  "use client";
2
2
  'use strict';
3
3
 
4
- var chunkWAOYPLZA_cjs = require('./chunk-WAOYPLZA.cjs');
4
+ var chunkC5WJFSMN_cjs = require('./chunk-C5WJFSMN.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "QaStudio", {
9
9
  enumerable: true,
10
- get: function () { return chunkWAOYPLZA_cjs.Qapture; }
10
+ get: function () { return chunkC5WJFSMN_cjs.Qapture; }
11
11
  });
12
12
  Object.defineProperty(exports, "Qapture", {
13
13
  enumerable: true,
14
- get: function () { return chunkWAOYPLZA_cjs.Qapture; }
14
+ get: function () { return chunkC5WJFSMN_cjs.Qapture; }
15
15
  });
16
16
  Object.defineProperty(exports, "initQaStudio", {
17
17
  enumerable: true,
18
- get: function () { return chunkWAOYPLZA_cjs.initQaStudio; }
18
+ get: function () { return chunkC5WJFSMN_cjs.initQaStudio; }
19
19
  });
20
20
  //# sourceMappingURL=next.cjs.map
21
21
  //# sourceMappingURL=next.cjs.map
package/dist/next.js CHANGED
@@ -1,4 +1,4 @@
1
1
  "use client";
2
- export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-JSEFC2VP.js';
2
+ export { Qapture as QaStudio, Qapture, initQaStudio } from './chunk-BM3ESR2O.js';
3
3
  //# sourceMappingURL=next.js.map
4
4
  //# sourceMappingURL=next.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkWAOYPLZA_cjs = require('./chunk-WAOYPLZA.cjs');
3
+ var chunkC5WJFSMN_cjs = require('./chunk-C5WJFSMN.cjs');
4
4
 
5
5
  // src/standalone.ts
6
6
  if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
@@ -38,7 +38,7 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
38
38
 
39
39
  Object.defineProperty(exports, "initQaStudio", {
40
40
  enumerable: true,
41
- get: function () { return chunkWAOYPLZA_cjs.initQaStudio; }
41
+ get: function () { return chunkC5WJFSMN_cjs.initQaStudio; }
42
42
  });
43
43
  //# sourceMappingURL=standalone.cjs.map
44
44
  //# sourceMappingURL=standalone.cjs.map
@@ -1,4 +1,4 @@
1
- export { initQaStudio } from './chunk-JSEFC2VP.js';
1
+ export { initQaStudio } from './chunk-BM3ESR2O.js';
2
2
 
3
3
  // src/standalone.ts
4
4
  if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qapture2",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Drop-in, AI-aware in-browser QA capture widget. A tester annotates the live app (element/region + auto-screenshot + note), tracks a graded testing journey, and exports a ZIP your own coding agent reads. Ships zero AI.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -89,6 +89,7 @@
89
89
  "standalone-smoke": "node scripts/standalone-smoke.mjs",
90
90
  "browser-test": "node scripts/browser-test.mjs",
91
91
  "capture-accuracy-test": "node scripts/capture-accuracy-test.mjs",
92
+ "element-capture-test": "node scripts/element-capture-test.mjs",
92
93
  "loop-features-test": "node scripts/loop-features-test.mjs",
93
94
  "walk-test": "node scripts/walk-test.mjs",
94
95
  "modern-css-test": "node scripts/modern-css-test.mjs",