qapture2 0.7.2 → 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 +28 -4
- package/dist/{chunk-NVFFN7SC.js → chunk-BM3ESR2O.js} +136 -62
- package/dist/{chunk-CIFJMD2B.cjs → chunk-C5WJFSMN.cjs} +136 -62
- package/dist/index.cjs +5 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -326,10 +326,34 @@ failure with nothing on screen to reveal it.
|
|
|
326
326
|
|
|
327
327
|
Captures now render the union of the viewport and the selection, so an element
|
|
328
328
|
that leaves the screen in any direction is rendered whole, up to 4000px per
|
|
329
|
-
side (past a
|
|
330
|
-
The exact engine can't photograph off-screen pixels, so there the
|
|
331
|
-
trimmed to the visible part instead — cropped, never displaced.
|
|
332
|
-
element-capture-test` measures all
|
|
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.
|
|
333
357
|
|
|
334
358
|
### `exact` — opt-in, pixel-for-pixel
|
|
335
359
|
|
|
@@ -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",
|
|
@@ -2531,7 +2535,7 @@ var MAX_SHOT_EDGE = 1800;
|
|
|
2531
2535
|
var WEBP_QUALITY = 0.92;
|
|
2532
2536
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2533
2537
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2534
|
-
var
|
|
2538
|
+
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2535
2539
|
function withTimeout(promise, ms) {
|
|
2536
2540
|
let timer;
|
|
2537
2541
|
return Promise.race([
|
|
@@ -2703,6 +2707,39 @@ function viewportSize() {
|
|
|
2703
2707
|
vh: document.documentElement.clientHeight || window.innerHeight
|
|
2704
2708
|
};
|
|
2705
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
|
+
}
|
|
2706
2743
|
function intersectViewport(rect) {
|
|
2707
2744
|
const { vw, vh } = viewportSize();
|
|
2708
2745
|
const left = Math.max(0, rect.left);
|
|
@@ -2739,9 +2776,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2739
2776
|
const { vw, vh } = viewportSize();
|
|
2740
2777
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
2741
2778
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2742
|
-
const budget = vw * vh * MAX_RENDER_VIEWPORTS;
|
|
2743
2779
|
const area = plan.render.width * plan.render.height;
|
|
2744
|
-
if (area
|
|
2780
|
+
if (area * scale * scale > MAX_RENDER_DEVICE_PIXELS) {
|
|
2781
|
+
scale = Math.sqrt(MAX_RENDER_DEVICE_PIXELS / area);
|
|
2782
|
+
}
|
|
2745
2783
|
const marked = markStuckElements();
|
|
2746
2784
|
try {
|
|
2747
2785
|
const full = await withTimeout(
|
|
@@ -8873,6 +8911,7 @@ function CaptureMode() {
|
|
|
8873
8911
|
const [candidate, setCandidate] = useState(null);
|
|
8874
8912
|
const [regionMode, setRegionMode] = useState(false);
|
|
8875
8913
|
const [shot, setShot] = useState(null);
|
|
8914
|
+
const [shotEngine, setShotEngine] = useState(null);
|
|
8876
8915
|
const [shotUrl, setShotUrl] = useState(null);
|
|
8877
8916
|
const [capturing, setCapturing] = useState(false);
|
|
8878
8917
|
const [captureError, setCaptureError] = useState(false);
|
|
@@ -8915,6 +8954,7 @@ function CaptureMode() {
|
|
|
8915
8954
|
return;
|
|
8916
8955
|
}
|
|
8917
8956
|
setCaptureError(outcome.status === "failed");
|
|
8957
|
+
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
8918
8958
|
setShot(blob);
|
|
8919
8959
|
setShotUrl((old) => {
|
|
8920
8960
|
if (old) URL.revokeObjectURL(old);
|
|
@@ -8979,7 +9019,9 @@ function CaptureMode() {
|
|
|
8979
9019
|
}
|
|
8980
9020
|
const r = el.getBoundingClientRect();
|
|
8981
9021
|
setHover({
|
|
8982
|
-
|
|
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 }),
|
|
8983
9025
|
selector: getStableSelector(el)
|
|
8984
9026
|
});
|
|
8985
9027
|
}
|
|
@@ -9026,7 +9068,10 @@ function CaptureMode() {
|
|
|
9026
9068
|
const r = el.getBoundingClientRect();
|
|
9027
9069
|
const sel = {
|
|
9028
9070
|
kind: "element",
|
|
9029
|
-
|
|
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 }),
|
|
9030
9075
|
selector: getStableSelector(el),
|
|
9031
9076
|
text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
|
|
9032
9077
|
tagName: el.tagName.toLowerCase()
|
|
@@ -9637,7 +9682,7 @@ function CaptureMode() {
|
|
|
9637
9682
|
)
|
|
9638
9683
|
] }),
|
|
9639
9684
|
/* @__PURE__ */ jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
|
|
9640
|
-
/* @__PURE__ */
|
|
9685
|
+
/* @__PURE__ */ jsxs(
|
|
9641
9686
|
"div",
|
|
9642
9687
|
{
|
|
9643
9688
|
className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
|
|
@@ -9645,66 +9690,95 @@ function CaptureMode() {
|
|
|
9645
9690
|
borderColor: "var(--qa-border-subtle)",
|
|
9646
9691
|
background: "var(--qa-surface-0)"
|
|
9647
9692
|
},
|
|
9648
|
-
children:
|
|
9649
|
-
/* @__PURE__ */
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
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") }),
|
|
9669
9739
|
/* @__PURE__ */ jsxs(
|
|
9670
|
-
"
|
|
9740
|
+
"button",
|
|
9671
9741
|
{
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
background: "var(--qa-surface-1)",
|
|
9677
|
-
color: "var(--qa-ink-hi)",
|
|
9678
|
-
border: "1px solid var(--qa-border-subtle)"
|
|
9679
|
-
},
|
|
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" },
|
|
9680
9746
|
children: [
|
|
9681
|
-
/* @__PURE__ */ jsx(Icon, { name: "
|
|
9682
|
-
t("
|
|
9747
|
+
/* @__PURE__ */ jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
9748
|
+
t("retry")
|
|
9683
9749
|
]
|
|
9684
9750
|
}
|
|
9685
9751
|
)
|
|
9686
|
-
]
|
|
9687
|
-
}
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
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
|
+
]
|
|
9708
9782
|
}
|
|
9709
9783
|
),
|
|
9710
9784
|
/* @__PURE__ */ jsx(LocationReveal, { target: selection }),
|
|
@@ -9996,5 +10070,5 @@ function Qapture({ config }) {
|
|
|
9996
10070
|
}
|
|
9997
10071
|
|
|
9998
10072
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
9999
|
-
//# sourceMappingURL=chunk-
|
|
10000
|
-
//# sourceMappingURL=chunk-
|
|
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",
|
|
@@ -2538,7 +2542,7 @@ var MAX_SHOT_EDGE = 1800;
|
|
|
2538
2542
|
var WEBP_QUALITY = 0.92;
|
|
2539
2543
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2540
2544
|
var MAX_CAPTURE_EDGE = 4e3;
|
|
2541
|
-
var
|
|
2545
|
+
var MAX_RENDER_DEVICE_PIXELS = 16e6;
|
|
2542
2546
|
function withTimeout(promise, ms) {
|
|
2543
2547
|
let timer;
|
|
2544
2548
|
return Promise.race([
|
|
@@ -2710,6 +2714,39 @@ function viewportSize() {
|
|
|
2710
2714
|
vh: document.documentElement.clientHeight || window.innerHeight
|
|
2711
2715
|
};
|
|
2712
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
|
+
}
|
|
2713
2750
|
function intersectViewport(rect) {
|
|
2714
2751
|
const { vw, vh } = viewportSize();
|
|
2715
2752
|
const left = Math.max(0, rect.left);
|
|
@@ -2746,9 +2783,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2746
2783
|
const { vw, vh } = viewportSize();
|
|
2747
2784
|
const plan = planRender(rect, sx, sy, vw, vh);
|
|
2748
2785
|
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2749
|
-
const budget = vw * vh * MAX_RENDER_VIEWPORTS;
|
|
2750
2786
|
const area = plan.render.width * plan.render.height;
|
|
2751
|
-
if (area
|
|
2787
|
+
if (area * scale * scale > MAX_RENDER_DEVICE_PIXELS) {
|
|
2788
|
+
scale = Math.sqrt(MAX_RENDER_DEVICE_PIXELS / area);
|
|
2789
|
+
}
|
|
2752
2790
|
const marked = markStuckElements();
|
|
2753
2791
|
try {
|
|
2754
2792
|
const full = await withTimeout(
|
|
@@ -8880,6 +8918,7 @@ function CaptureMode() {
|
|
|
8880
8918
|
const [candidate, setCandidate] = React.useState(null);
|
|
8881
8919
|
const [regionMode, setRegionMode] = React.useState(false);
|
|
8882
8920
|
const [shot, setShot] = React.useState(null);
|
|
8921
|
+
const [shotEngine, setShotEngine] = React.useState(null);
|
|
8883
8922
|
const [shotUrl, setShotUrl] = React.useState(null);
|
|
8884
8923
|
const [capturing, setCapturing] = React.useState(false);
|
|
8885
8924
|
const [captureError, setCaptureError] = React.useState(false);
|
|
@@ -8922,6 +8961,7 @@ function CaptureMode() {
|
|
|
8922
8961
|
return;
|
|
8923
8962
|
}
|
|
8924
8963
|
setCaptureError(outcome.status === "failed");
|
|
8964
|
+
setShotEngine(outcome.status === "ok" ? outcome.engine : null);
|
|
8925
8965
|
setShot(blob);
|
|
8926
8966
|
setShotUrl((old) => {
|
|
8927
8967
|
if (old) URL.revokeObjectURL(old);
|
|
@@ -8986,7 +9026,9 @@ function CaptureMode() {
|
|
|
8986
9026
|
}
|
|
8987
9027
|
const r = el.getBoundingClientRect();
|
|
8988
9028
|
setHover({
|
|
8989
|
-
|
|
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 }),
|
|
8990
9032
|
selector: getStableSelector(el)
|
|
8991
9033
|
});
|
|
8992
9034
|
}
|
|
@@ -9033,7 +9075,10 @@ function CaptureMode() {
|
|
|
9033
9075
|
const r = el.getBoundingClientRect();
|
|
9034
9076
|
const sel = {
|
|
9035
9077
|
kind: "element",
|
|
9036
|
-
|
|
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 }),
|
|
9037
9082
|
selector: getStableSelector(el),
|
|
9038
9083
|
text: (el.innerText ?? el.textContent ?? "").trim().slice(0, 120),
|
|
9039
9084
|
tagName: el.tagName.toLowerCase()
|
|
@@ -9644,7 +9689,7 @@ function CaptureMode() {
|
|
|
9644
9689
|
)
|
|
9645
9690
|
] }),
|
|
9646
9691
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "qa-space-y-2 qa-p-3", children: [
|
|
9647
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9692
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9648
9693
|
"div",
|
|
9649
9694
|
{
|
|
9650
9695
|
className: "qa-flex qa-min-h-16 qa-items-center qa-justify-center qa-rounded-lg qa-border",
|
|
@@ -9652,66 +9697,95 @@ function CaptureMode() {
|
|
|
9652
9697
|
borderColor: "var(--qa-border-subtle)",
|
|
9653
9698
|
background: "var(--qa-surface-0)"
|
|
9654
9699
|
},
|
|
9655
|
-
children:
|
|
9656
|
-
/* @__PURE__ */ jsxRuntime.
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
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") }),
|
|
9676
9746
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
9677
|
-
"
|
|
9747
|
+
"button",
|
|
9678
9748
|
{
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
background: "var(--qa-surface-1)",
|
|
9684
|
-
color: "var(--qa-ink-hi)",
|
|
9685
|
-
border: "1px solid var(--qa-border-subtle)"
|
|
9686
|
-
},
|
|
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" },
|
|
9687
9753
|
children: [
|
|
9688
|
-
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "
|
|
9689
|
-
t("
|
|
9754
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { name: "RotateCcw", size: 13 }),
|
|
9755
|
+
t("retry")
|
|
9690
9756
|
]
|
|
9691
9757
|
}
|
|
9692
9758
|
)
|
|
9693
|
-
]
|
|
9694
|
-
}
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
9710
|
-
|
|
9711
|
-
|
|
9712
|
-
|
|
9713
|
-
|
|
9714
|
-
|
|
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
|
+
]
|
|
9715
9789
|
}
|
|
9716
9790
|
),
|
|
9717
9791
|
/* @__PURE__ */ jsxRuntime.jsx(LocationReveal, { target: selection }),
|
|
@@ -10005,5 +10079,5 @@ function Qapture({ config }) {
|
|
|
10005
10079
|
exports.Qapture = Qapture;
|
|
10006
10080
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
10007
10081
|
exports.initQaStudio = initQaStudio;
|
|
10008
|
-
//# sourceMappingURL=chunk-
|
|
10009
|
-
//# sourceMappingURL=chunk-
|
|
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
|
|
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
|
|
9
|
+
get: function () { return chunkC5WJFSMN_cjs.Qapture; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "Qapture", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkC5WJFSMN_cjs.Qapture; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "deleteQaDatabase", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkC5WJFSMN_cjs.deleteQaDatabase; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "initQaStudio", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
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
package/dist/next.cjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
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
|
|
10
|
+
get: function () { return chunkC5WJFSMN_cjs.Qapture; }
|
|
11
11
|
});
|
|
12
12
|
Object.defineProperty(exports, "Qapture", {
|
|
13
13
|
enumerable: true,
|
|
14
|
-
get: function () { return
|
|
14
|
+
get: function () { return chunkC5WJFSMN_cjs.Qapture; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "initQaStudio", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
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
package/dist/standalone.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
41
|
+
get: function () { return chunkC5WJFSMN_cjs.initQaStudio; }
|
|
42
42
|
});
|
|
43
43
|
//# sourceMappingURL=standalone.cjs.map
|
|
44
44
|
//# sourceMappingURL=standalone.cjs.map
|
package/dist/standalone.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qapture2",
|
|
3
|
-
"version": "0.7.
|
|
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",
|