@insitue/sdk 0.1.2 → 0.1.4
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/dist/capture-only.js +2 -2
- package/dist/{chunk-X7V2UEBO.js → chunk-65BGOX2M.js} +417 -83
- package/dist/{chunk-VMBBJKFF.js → chunk-7GRJ5SZQ.js} +97 -1
- package/dist/{chunk-YUNYW2IC.js → chunk-TTH4NBFA.js} +80 -1
- package/dist/index.js +3 -3
- package/dist/overlay.js +2 -2
- package/package.json +1 -1
package/dist/capture-only.js
CHANGED
|
@@ -582,7 +582,7 @@ function resolveTarget(el) {
|
|
|
582
582
|
}
|
|
583
583
|
return source === void 0 ? { confidence, componentStack, selector } : { source, confidence, componentStack, selector };
|
|
584
584
|
}
|
|
585
|
-
var CAPTURE_SCHEMA_VERSION =
|
|
585
|
+
var CAPTURE_SCHEMA_VERSION = 3;
|
|
586
586
|
var PROTOCOL_VERSION = 4;
|
|
587
587
|
function toIssueDraft(bundle) {
|
|
588
588
|
const t3 = bundle.target;
|
|
@@ -597,7 +597,7 @@ function toIssueDraft(bundle) {
|
|
|
597
597
|
`**Viewport:** ${bundle.viewport.w}\xD7${bundle.viewport.h}${bundle.viewport.breakpoint ? ` (${bundle.viewport.breakpoint})` : ""}`,
|
|
598
598
|
`**Tailwind:** ${bundle.tailwindClasses.join(" ") || "\u2014"}`,
|
|
599
599
|
`**Runtime:** ${bundle.runtime.console.length} log \xB7 ${bundle.runtime.network.length} net \xB7 ${errs} err`,
|
|
600
|
-
`**Screenshot:** ${bundle.screenshot ?
|
|
600
|
+
`**Screenshot:** ${bundle.screenshot ? `attached` + (bundle.screenshot.source ? ` (${bundle.screenshot.source})` : "") + (bundle.screenshot.qualityNote ? ` \u2014 ${bundle.screenshot.qualityNote}` : "") : bundle.screenshotUnavailable ? `unavailable \u2014 ${bundle.screenshotUnavailable}` : "\u2014"}`,
|
|
601
601
|
bundle.userNote ? `
|
|
602
602
|
> ${bundle.userNote}` : "",
|
|
603
603
|
`
|
|
@@ -802,6 +802,53 @@ function beginPick(mode = "element") {
|
|
|
802
802
|
});
|
|
803
803
|
}
|
|
804
804
|
|
|
805
|
+
// src/capture-settings.ts
|
|
806
|
+
var DEFAULT_SETTINGS = {
|
|
807
|
+
alwaysPixelPerfect: false
|
|
808
|
+
};
|
|
809
|
+
function storageKey() {
|
|
810
|
+
if (typeof location === "undefined") return "insitu:capture-settings";
|
|
811
|
+
return `insitu:capture-settings:${location.host}`;
|
|
812
|
+
}
|
|
813
|
+
var cached = null;
|
|
814
|
+
function getCaptureSettings() {
|
|
815
|
+
if (cached) return cached;
|
|
816
|
+
if (typeof localStorage === "undefined") {
|
|
817
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
818
|
+
return cached;
|
|
819
|
+
}
|
|
820
|
+
try {
|
|
821
|
+
const raw = localStorage.getItem(storageKey());
|
|
822
|
+
if (!raw) {
|
|
823
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
824
|
+
return cached;
|
|
825
|
+
}
|
|
826
|
+
const parsed = JSON.parse(raw);
|
|
827
|
+
cached = { ...DEFAULT_SETTINGS, ...parsed };
|
|
828
|
+
return cached;
|
|
829
|
+
} catch {
|
|
830
|
+
cached = { ...DEFAULT_SETTINGS };
|
|
831
|
+
return cached;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
function setCaptureSettings(patch) {
|
|
835
|
+
const next = { ...getCaptureSettings(), ...patch };
|
|
836
|
+
cached = next;
|
|
837
|
+
if (typeof localStorage !== "undefined") {
|
|
838
|
+
try {
|
|
839
|
+
localStorage.setItem(storageKey(), JSON.stringify(next));
|
|
840
|
+
} catch {
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
for (const l3 of listeners) l3(next);
|
|
844
|
+
}
|
|
845
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
846
|
+
function onCaptureSettingsChange(l3) {
|
|
847
|
+
listeners.add(l3);
|
|
848
|
+
l3(getCaptureSettings());
|
|
849
|
+
return () => listeners.delete(l3);
|
|
850
|
+
}
|
|
851
|
+
|
|
805
852
|
// ../../node_modules/.pnpm/html-to-image@1.11.13/node_modules/html-to-image/es/util.js
|
|
806
853
|
function resolveUrl(url, baseUrl) {
|
|
807
854
|
if (url.match(/^[a-z]+:\/\//i)) {
|
|
@@ -1594,39 +1641,61 @@ async function toCanvas(node, options = {}) {
|
|
|
1594
1641
|
}
|
|
1595
1642
|
|
|
1596
1643
|
// src/capture.ts
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
if (
|
|
1612
|
-
|
|
1613
|
-
try {
|
|
1614
|
-
return new URL(el.currentSrc || el.src).host;
|
|
1615
|
-
} catch {
|
|
1616
|
-
return "cross-origin";
|
|
1617
|
-
}
|
|
1618
|
-
})();
|
|
1619
|
-
return `cross-origin <img> (${host})`;
|
|
1644
|
+
var IMAGE_PLACEHOLDER = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(
|
|
1645
|
+
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" fill="#e8e8e8"/><path d="M0 0 L32 32 M32 0 L0 32" stroke="#b0b0b0" stroke-width="1.5"/></svg>'
|
|
1646
|
+
) : "");
|
|
1647
|
+
async function preResolveImages() {
|
|
1648
|
+
const restorations = [];
|
|
1649
|
+
const failedImages = /* @__PURE__ */ new Set();
|
|
1650
|
+
const images = Array.from(
|
|
1651
|
+
document.querySelectorAll("img")
|
|
1652
|
+
).filter(
|
|
1653
|
+
(img) => !img.closest?.("#insitu-root, [data-insitu-layer]")
|
|
1654
|
+
);
|
|
1655
|
+
await Promise.all(
|
|
1656
|
+
images.map(async (img) => {
|
|
1657
|
+
const srcToFetch = img.currentSrc || img.src;
|
|
1658
|
+
if (!srcToFetch || srcToFetch.startsWith("data:") || srcToFetch.startsWith("blob:")) {
|
|
1659
|
+
return;
|
|
1620
1660
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1661
|
+
try {
|
|
1662
|
+
const res = await fetch(srcToFetch, { cache: "force-cache" });
|
|
1663
|
+
if (!res.ok) {
|
|
1664
|
+
failedImages.add(img);
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
const blob = await res.blob();
|
|
1668
|
+
const dataUrl = await new Promise((resolve, reject) => {
|
|
1669
|
+
const reader = new FileReader();
|
|
1670
|
+
reader.onload = () => resolve(reader.result);
|
|
1671
|
+
reader.onerror = () => reject(reader.error);
|
|
1672
|
+
reader.readAsDataURL(blob);
|
|
1673
|
+
});
|
|
1674
|
+
const origSrc = img.getAttribute("src");
|
|
1675
|
+
const origSrcset = img.getAttribute("srcset");
|
|
1676
|
+
restorations.push(() => {
|
|
1677
|
+
if (origSrc != null) img.setAttribute("src", origSrc);
|
|
1678
|
+
else img.removeAttribute("src");
|
|
1679
|
+
if (origSrcset != null) img.setAttribute("srcset", origSrcset);
|
|
1680
|
+
else img.removeAttribute("srcset");
|
|
1681
|
+
});
|
|
1682
|
+
img.removeAttribute("srcset");
|
|
1683
|
+
img.src = dataUrl;
|
|
1684
|
+
try {
|
|
1685
|
+
await img.decode();
|
|
1686
|
+
} catch {
|
|
1687
|
+
}
|
|
1688
|
+
} catch {
|
|
1689
|
+
failedImages.add(img);
|
|
1690
|
+
}
|
|
1691
|
+
})
|
|
1692
|
+
);
|
|
1693
|
+
return {
|
|
1694
|
+
restore: () => {
|
|
1695
|
+
for (const r3 of restorations) r3();
|
|
1696
|
+
},
|
|
1697
|
+
failedImages
|
|
1698
|
+
};
|
|
1630
1699
|
}
|
|
1631
1700
|
function findContextAncestor(el) {
|
|
1632
1701
|
const minW = 420;
|
|
@@ -1649,41 +1718,265 @@ async function renderViewportCrop(cropRect, pixelRatio) {
|
|
|
1649
1718
|
const bodyBg = getComputedStyle(document.body).backgroundColor;
|
|
1650
1719
|
const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
|
|
1651
1720
|
const backgroundColor = bodyBg && bodyBg !== "rgba(0, 0, 0, 0)" && bodyBg !== "transparent" ? bodyBg : htmlBg && htmlBg !== "rgba(0, 0, 0, 0)" && htmlBg !== "transparent" ? htmlBg : "#ffffff";
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1721
|
+
const { restore: restoreImages, failedImages } = await preResolveImages();
|
|
1722
|
+
try {
|
|
1723
|
+
const fullCanvas = await toCanvas(document.documentElement, {
|
|
1724
|
+
pixelRatio,
|
|
1725
|
+
// cacheBust off — we've already swapped srcs to data URLs.
|
|
1726
|
+
cacheBust: false,
|
|
1727
|
+
backgroundColor,
|
|
1728
|
+
imagePlaceholder: IMAGE_PLACEHOLDER,
|
|
1729
|
+
// Strip only our own overlay layers.
|
|
1730
|
+
filter: (n2) => !(n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]"))
|
|
1731
|
+
});
|
|
1732
|
+
const sx = window.scrollX;
|
|
1733
|
+
const sy = window.scrollY;
|
|
1734
|
+
const out = document.createElement("canvas");
|
|
1735
|
+
out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
|
|
1736
|
+
out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
|
|
1737
|
+
const ctx = out.getContext("2d");
|
|
1738
|
+
if (!ctx) return { dataUrl: null, failedImages };
|
|
1739
|
+
ctx.drawImage(
|
|
1740
|
+
fullCanvas,
|
|
1741
|
+
Math.round((cropRect.x + sx) * pixelRatio),
|
|
1742
|
+
Math.round((cropRect.y + sy) * pixelRatio),
|
|
1743
|
+
Math.round(cropRect.width * pixelRatio),
|
|
1744
|
+
Math.round(cropRect.height * pixelRatio),
|
|
1745
|
+
0,
|
|
1746
|
+
0,
|
|
1747
|
+
out.width,
|
|
1748
|
+
out.height
|
|
1749
|
+
);
|
|
1750
|
+
if (looksBlankUniform(ctx, out.width, out.height)) {
|
|
1751
|
+
return { dataUrl: null, failedImages };
|
|
1752
|
+
}
|
|
1753
|
+
return { dataUrl: out.toDataURL("image/png"), failedImages };
|
|
1754
|
+
} finally {
|
|
1755
|
+
restoreImages();
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function looksBlankUniform(ctx, w3, h3) {
|
|
1759
|
+
if (w3 < 4 || h3 < 4) return false;
|
|
1760
|
+
const samples = [];
|
|
1761
|
+
for (let i3 = 0; i3 < 4; i3++) {
|
|
1762
|
+
for (let j3 = 0; j3 < 4; j3++) {
|
|
1763
|
+
const x2 = Math.floor(w3 * (i3 + 0.5) / 4);
|
|
1764
|
+
const y3 = Math.floor(h3 * (j3 + 0.5) / 4);
|
|
1765
|
+
try {
|
|
1766
|
+
const px2 = ctx.getImageData(x2, y3, 1, 1).data;
|
|
1767
|
+
samples.push(`${px2[0]},${px2[1]},${px2[2]},${px2[3]}`);
|
|
1768
|
+
} catch {
|
|
1769
|
+
return true;
|
|
1664
1770
|
}
|
|
1665
|
-
return true;
|
|
1666
1771
|
}
|
|
1667
|
-
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
out
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1772
|
+
}
|
|
1773
|
+
return new Set(samples).size === 1;
|
|
1774
|
+
}
|
|
1775
|
+
function assessCaptureQuality(cropRect, failedImages) {
|
|
1776
|
+
const out = {
|
|
1777
|
+
unembeddableImages: 0,
|
|
1778
|
+
hasVideo: false,
|
|
1779
|
+
hasCanvas: false
|
|
1780
|
+
};
|
|
1781
|
+
const all = document.querySelectorAll("img, video, canvas");
|
|
1782
|
+
for (const el of all) {
|
|
1783
|
+
if (el instanceof Element && el.closest?.("#insitu-root, [data-insitu-layer]")) {
|
|
1784
|
+
continue;
|
|
1785
|
+
}
|
|
1786
|
+
const r3 = el.getBoundingClientRect();
|
|
1787
|
+
const overlaps = r3.right >= cropRect.x && r3.left <= cropRect.x + cropRect.width && r3.bottom >= cropRect.y && r3.top <= cropRect.y + cropRect.height;
|
|
1788
|
+
if (!overlaps) continue;
|
|
1789
|
+
if (el instanceof HTMLImageElement) {
|
|
1790
|
+
if (failedImages.has(el)) out.unembeddableImages++;
|
|
1791
|
+
} else if (el instanceof HTMLVideoElement) {
|
|
1792
|
+
if (r3.width > 0 && r3.height > 0) out.hasVideo = true;
|
|
1793
|
+
} else if (el instanceof HTMLCanvasElement) {
|
|
1794
|
+
if (r3.width > 0 && r3.height > 0) out.hasCanvas = true;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return out;
|
|
1798
|
+
}
|
|
1799
|
+
function describeImperfection(q2) {
|
|
1800
|
+
const parts = [];
|
|
1801
|
+
if (q2.unembeddableImages > 0) {
|
|
1802
|
+
parts.push(
|
|
1803
|
+
`${q2.unembeddableImages} non-CORS image${q2.unembeddableImages > 1 ? "s" : ""}`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1806
|
+
if (q2.hasVideo) parts.push("video frame");
|
|
1807
|
+
if (q2.hasCanvas) parts.push("canvas content");
|
|
1808
|
+
return parts.join(" + ");
|
|
1809
|
+
}
|
|
1810
|
+
var displayMediaState = {
|
|
1811
|
+
stream: null,
|
|
1812
|
+
trackEndedHandler: null,
|
|
1813
|
+
idleTimer: null,
|
|
1814
|
+
deniedAt: null
|
|
1815
|
+
};
|
|
1816
|
+
var IDLE_MS = 9e4;
|
|
1817
|
+
var displayMediaListeners = /* @__PURE__ */ new Set();
|
|
1818
|
+
function onDisplayMediaChange(l3) {
|
|
1819
|
+
displayMediaListeners.add(l3);
|
|
1820
|
+
l3(displayMediaState.stream != null);
|
|
1821
|
+
return () => displayMediaListeners.delete(l3);
|
|
1822
|
+
}
|
|
1823
|
+
function notifyDisplayMedia(reason) {
|
|
1824
|
+
const active = displayMediaState.stream != null;
|
|
1825
|
+
for (const l3 of displayMediaListeners) l3(active, reason);
|
|
1826
|
+
}
|
|
1827
|
+
function stopDisplayMedia(reason = "stopped") {
|
|
1828
|
+
if (displayMediaState.stream) {
|
|
1829
|
+
for (const t3 of displayMediaState.stream.getTracks()) t3.stop();
|
|
1830
|
+
}
|
|
1831
|
+
if (displayMediaState.idleTimer) clearTimeout(displayMediaState.idleTimer);
|
|
1832
|
+
if (displayMediaState.trackEndedHandler && displayMediaState.stream) {
|
|
1833
|
+
for (const t3 of displayMediaState.stream.getTracks()) {
|
|
1834
|
+
t3.removeEventListener("ended", displayMediaState.trackEndedHandler);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
displayMediaState.stream = null;
|
|
1838
|
+
displayMediaState.trackEndedHandler = null;
|
|
1839
|
+
displayMediaState.idleTimer = null;
|
|
1840
|
+
notifyDisplayMedia(reason);
|
|
1841
|
+
}
|
|
1842
|
+
function bumpIdleTimer() {
|
|
1843
|
+
if (displayMediaState.idleTimer) clearTimeout(displayMediaState.idleTimer);
|
|
1844
|
+
displayMediaState.idleTimer = setTimeout(
|
|
1845
|
+
() => stopDisplayMedia("idle"),
|
|
1846
|
+
IDLE_MS
|
|
1685
1847
|
);
|
|
1686
|
-
|
|
1848
|
+
}
|
|
1849
|
+
function supportsDisplayMedia() {
|
|
1850
|
+
return typeof navigator !== "undefined" && typeof navigator.mediaDevices?.getDisplayMedia === "function";
|
|
1851
|
+
}
|
|
1852
|
+
async function ensureDisplayMediaStream() {
|
|
1853
|
+
if (!supportsDisplayMedia()) return null;
|
|
1854
|
+
if (displayMediaState.stream) {
|
|
1855
|
+
bumpIdleTimer();
|
|
1856
|
+
return displayMediaState.stream;
|
|
1857
|
+
}
|
|
1858
|
+
if (displayMediaState.deniedAt && Date.now() - displayMediaState.deniedAt < 6e4) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({
|
|
1863
|
+
// `displaySurface: 'browser'` + `preferCurrentTab: true` makes
|
|
1864
|
+
// Chrome/Edge default-select the current tab in the prompt.
|
|
1865
|
+
// Other browsers ignore the hints; user still picks manually.
|
|
1866
|
+
video: {
|
|
1867
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1868
|
+
displaySurface: "browser"
|
|
1869
|
+
},
|
|
1870
|
+
audio: false,
|
|
1871
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1872
|
+
preferCurrentTab: true
|
|
1873
|
+
});
|
|
1874
|
+
displayMediaState.stream = stream;
|
|
1875
|
+
displayMediaState.deniedAt = null;
|
|
1876
|
+
const handler = () => stopDisplayMedia("track-ended");
|
|
1877
|
+
for (const t3 of stream.getTracks()) t3.addEventListener("ended", handler);
|
|
1878
|
+
displayMediaState.trackEndedHandler = handler;
|
|
1879
|
+
bumpIdleTimer();
|
|
1880
|
+
notifyDisplayMedia("granted");
|
|
1881
|
+
return stream;
|
|
1882
|
+
} catch {
|
|
1883
|
+
displayMediaState.deniedAt = Date.now();
|
|
1884
|
+
notifyDisplayMedia("denied");
|
|
1885
|
+
return null;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
async function retryDisplayMedia() {
|
|
1889
|
+
displayMediaState.deniedAt = null;
|
|
1890
|
+
const s3 = await ensureDisplayMediaStream();
|
|
1891
|
+
return s3 != null;
|
|
1892
|
+
}
|
|
1893
|
+
function hideOverlayLayersBriefly() {
|
|
1894
|
+
const id = "insitu-capture-hide";
|
|
1895
|
+
const style = document.createElement("style");
|
|
1896
|
+
style.id = id;
|
|
1897
|
+
style.textContent = `
|
|
1898
|
+
#insitu-root, [data-insitu-layer] { visibility: hidden !important; }
|
|
1899
|
+
`;
|
|
1900
|
+
document.head.appendChild(style);
|
|
1901
|
+
return () => {
|
|
1902
|
+
style.remove();
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
async function tryGrabViaDisplayMedia(cropRect, pixelRatio) {
|
|
1906
|
+
const wasActive = displayMediaState.stream != null;
|
|
1907
|
+
const stream = await ensureDisplayMediaStream();
|
|
1908
|
+
if (!stream) return null;
|
|
1909
|
+
const fresh = !wasActive;
|
|
1910
|
+
bumpIdleTimer();
|
|
1911
|
+
const restoreOverlay = hideOverlayLayersBriefly();
|
|
1912
|
+
await new Promise(
|
|
1913
|
+
(r3) => requestAnimationFrame(() => requestAnimationFrame(() => r3()))
|
|
1914
|
+
);
|
|
1915
|
+
try {
|
|
1916
|
+
const track = stream.getVideoTracks()[0];
|
|
1917
|
+
if (!track) return null;
|
|
1918
|
+
let bitmap = null;
|
|
1919
|
+
const Ctor = window.ImageCapture;
|
|
1920
|
+
if (Ctor) {
|
|
1921
|
+
bitmap = await new Ctor(track).grabFrame();
|
|
1922
|
+
} else {
|
|
1923
|
+
bitmap = await grabFrameViaVideo(stream);
|
|
1924
|
+
}
|
|
1925
|
+
if (!bitmap) return null;
|
|
1926
|
+
const frameW = bitmap.width;
|
|
1927
|
+
const frameH = bitmap.height;
|
|
1928
|
+
const scaleX = frameW / window.innerWidth;
|
|
1929
|
+
const scaleY = frameH / window.innerHeight;
|
|
1930
|
+
const sx = Math.max(0, Math.round(cropRect.x * scaleX));
|
|
1931
|
+
const sy = Math.max(0, Math.round(cropRect.y * scaleY));
|
|
1932
|
+
const sw = Math.min(frameW - sx, Math.round(cropRect.width * scaleX));
|
|
1933
|
+
const sh = Math.min(frameH - sy, Math.round(cropRect.height * scaleY));
|
|
1934
|
+
const out = document.createElement("canvas");
|
|
1935
|
+
out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
|
|
1936
|
+
out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
|
|
1937
|
+
const ctx = out.getContext("2d");
|
|
1938
|
+
if (!ctx) return null;
|
|
1939
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, out.width, out.height);
|
|
1940
|
+
bitmap.close?.();
|
|
1941
|
+
return { dataUrl: out.toDataURL("image/png"), fresh };
|
|
1942
|
+
} finally {
|
|
1943
|
+
restoreOverlay();
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
async function grabFrameViaVideo(stream) {
|
|
1947
|
+
const video = document.createElement("video");
|
|
1948
|
+
video.srcObject = stream;
|
|
1949
|
+
video.muted = true;
|
|
1950
|
+
video.playsInline = true;
|
|
1951
|
+
video.style.position = "fixed";
|
|
1952
|
+
video.style.pointerEvents = "none";
|
|
1953
|
+
video.style.opacity = "0";
|
|
1954
|
+
video.style.width = "1px";
|
|
1955
|
+
video.style.height = "1px";
|
|
1956
|
+
document.body.appendChild(video);
|
|
1957
|
+
try {
|
|
1958
|
+
await video.play().catch(() => void 0);
|
|
1959
|
+
await new Promise((resolve, reject) => {
|
|
1960
|
+
const onReady = () => {
|
|
1961
|
+
video.removeEventListener("loadeddata", onReady);
|
|
1962
|
+
resolve();
|
|
1963
|
+
};
|
|
1964
|
+
video.addEventListener("loadeddata", onReady, { once: true });
|
|
1965
|
+
setTimeout(() => reject(new Error("video timeout")), 2e3);
|
|
1966
|
+
});
|
|
1967
|
+
const tmp = document.createElement("canvas");
|
|
1968
|
+
tmp.width = video.videoWidth;
|
|
1969
|
+
tmp.height = video.videoHeight;
|
|
1970
|
+
const ctx = tmp.getContext("2d");
|
|
1971
|
+
if (!ctx) throw new Error("no 2d ctx");
|
|
1972
|
+
ctx.drawImage(video, 0, 0);
|
|
1973
|
+
return await createImageBitmap(tmp);
|
|
1974
|
+
} finally {
|
|
1975
|
+
video.remove();
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
if (typeof window !== "undefined") {
|
|
1979
|
+
window.addEventListener("pagehide", () => stopDisplayMedia("pagehide"));
|
|
1687
1980
|
}
|
|
1688
1981
|
function elementFor(sel) {
|
|
1689
1982
|
if (sel.mode === "element") return sel.pointerPath?.[0] ?? null;
|
|
@@ -1698,6 +1991,7 @@ async function buildBundle(sel) {
|
|
|
1698
1991
|
const el = elementFor(sel);
|
|
1699
1992
|
const rt = runtimeSnapshot();
|
|
1700
1993
|
const dpr = window.devicePixelRatio || 1;
|
|
1994
|
+
const settings = getCaptureSettings();
|
|
1701
1995
|
let screenshot;
|
|
1702
1996
|
let screenshotUnavailable;
|
|
1703
1997
|
if (el instanceof HTMLElement) {
|
|
@@ -1716,32 +2010,66 @@ async function buildBundle(sel) {
|
|
|
1716
2010
|
el.style.outline = "3px solid #ff6b00";
|
|
1717
2011
|
el.style.outlineOffset = "2px";
|
|
1718
2012
|
try {
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
2013
|
+
const skipLayer1 = settings.alwaysPixelPerfect;
|
|
2014
|
+
let layer1Result = null;
|
|
2015
|
+
let quality = null;
|
|
2016
|
+
if (!skipLayer1) {
|
|
2017
|
+
const r3 = await renderViewportCrop(cropRect, Math.min(dpr, 1.5));
|
|
2018
|
+
layer1Result = r3.dataUrl;
|
|
2019
|
+
quality = assessCaptureQuality(cropRect, r3.failedImages);
|
|
2020
|
+
}
|
|
2021
|
+
const imperfect = !layer1Result || quality != null && (quality.unembeddableImages > 0 || quality.hasVideo || quality.hasCanvas);
|
|
2022
|
+
if (imperfect || skipLayer1) {
|
|
2023
|
+
const grab = await tryGrabViaDisplayMedia(
|
|
2024
|
+
cropRect,
|
|
2025
|
+
Math.min(dpr, 2)
|
|
2026
|
+
);
|
|
2027
|
+
if (grab) {
|
|
2028
|
+
screenshot = {
|
|
2029
|
+
mime: "image/png",
|
|
2030
|
+
dataUrl: grab.dataUrl,
|
|
2031
|
+
bounds: {
|
|
2032
|
+
x: cropRect.x,
|
|
2033
|
+
y: cropRect.y,
|
|
2034
|
+
width: cropRect.width,
|
|
2035
|
+
height: cropRect.height
|
|
2036
|
+
},
|
|
2037
|
+
source: "display-media"
|
|
2038
|
+
};
|
|
2039
|
+
} else if (layer1Result) {
|
|
2040
|
+
const reason = quality ? describeImperfection(quality) : "non-CORS content";
|
|
2041
|
+
screenshot = {
|
|
2042
|
+
mime: "image/png",
|
|
2043
|
+
dataUrl: layer1Result,
|
|
2044
|
+
bounds: {
|
|
2045
|
+
x: cropRect.x,
|
|
2046
|
+
y: cropRect.y,
|
|
2047
|
+
width: cropRect.width,
|
|
2048
|
+
height: cropRect.height
|
|
2049
|
+
},
|
|
2050
|
+
source: "rasterise",
|
|
2051
|
+
qualityNote: `${reason} couldn't be embedded \u2014 grant tab capture for pixel-perfect screenshots`
|
|
2052
|
+
};
|
|
2053
|
+
} else {
|
|
2054
|
+
screenshotUnavailable = supportsDisplayMedia() ? "rasterise failed \u2014 grant tab capture for pixel-perfect screenshots" : "rasterise failed and tab capture unsupported in this browser";
|
|
2055
|
+
}
|
|
2056
|
+
} else if (layer1Result) {
|
|
1730
2057
|
screenshot = {
|
|
1731
2058
|
mime: "image/png",
|
|
1732
|
-
dataUrl,
|
|
1733
|
-
// Bounds describe the SCREENSHOT (the crop region) so
|
|
1734
|
-
// the dashboard knows what slice of viewport this is.
|
|
2059
|
+
dataUrl: layer1Result,
|
|
1735
2060
|
bounds: {
|
|
1736
2061
|
x: cropRect.x,
|
|
1737
2062
|
y: cropRect.y,
|
|
1738
2063
|
width: cropRect.width,
|
|
1739
2064
|
height: cropRect.height
|
|
1740
|
-
}
|
|
2065
|
+
},
|
|
2066
|
+
source: "rasterise"
|
|
1741
2067
|
};
|
|
2068
|
+
} else {
|
|
2069
|
+
screenshotUnavailable = "rasterise produced an empty image";
|
|
1742
2070
|
}
|
|
1743
|
-
} catch {
|
|
1744
|
-
screenshotUnavailable = "rasterise failed";
|
|
2071
|
+
} catch (err) {
|
|
2072
|
+
screenshotUnavailable = err instanceof Error ? `rasterise failed: ${err.message}` : "rasterise failed";
|
|
1745
2073
|
} finally {
|
|
1746
2074
|
el.style.outline = orig.outline;
|
|
1747
2075
|
el.style.outlineOffset = orig.outlineOffset;
|
|
@@ -1784,5 +2112,11 @@ export {
|
|
|
1784
2112
|
installRuntimeCollectors,
|
|
1785
2113
|
runtimeErrorCount,
|
|
1786
2114
|
beginPick,
|
|
2115
|
+
getCaptureSettings,
|
|
2116
|
+
setCaptureSettings,
|
|
2117
|
+
onCaptureSettingsChange,
|
|
2118
|
+
onDisplayMediaChange,
|
|
2119
|
+
stopDisplayMedia,
|
|
2120
|
+
retryDisplayMedia,
|
|
1787
2121
|
buildBundle
|
|
1788
2122
|
};
|
|
@@ -5,11 +5,17 @@ import {
|
|
|
5
5
|
beginPick,
|
|
6
6
|
buildBundle,
|
|
7
7
|
d,
|
|
8
|
+
getCaptureSettings,
|
|
8
9
|
installRuntimeCollectors,
|
|
9
10
|
k,
|
|
11
|
+
onCaptureSettingsChange,
|
|
12
|
+
onDisplayMediaChange,
|
|
13
|
+
retryDisplayMedia,
|
|
10
14
|
runtimeErrorCount,
|
|
15
|
+
setCaptureSettings,
|
|
16
|
+
stopDisplayMedia,
|
|
11
17
|
y
|
|
12
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-65BGOX2M.js";
|
|
13
19
|
|
|
14
20
|
// src/client.ts
|
|
15
21
|
var CompanionClient = class {
|
|
@@ -238,6 +244,11 @@ function App(props) {
|
|
|
238
244
|
const [showCtx, setShowCtx] = d(false);
|
|
239
245
|
const [showSettings, setShowSettings] = d(false);
|
|
240
246
|
const [autoApply, setAutoApply] = d(false);
|
|
247
|
+
const [captureSettings, setCaptureSettingsState] = d(
|
|
248
|
+
getCaptureSettings()
|
|
249
|
+
);
|
|
250
|
+
const [displayMediaActive, setDisplayMediaActive] = d(false);
|
|
251
|
+
const [displayMediaDenied, setDisplayMediaDenied] = d(false);
|
|
241
252
|
const [agentReady, setAgentReady] = d(null);
|
|
242
253
|
const [agentNote, setAgentNote] = d("");
|
|
243
254
|
const [chatInput, setChatInput] = d("");
|
|
@@ -433,6 +444,18 @@ function App(props) {
|
|
|
433
444
|
const el = threadRef.current;
|
|
434
445
|
if (el) el.scrollTop = el.scrollHeight;
|
|
435
446
|
}, [messages, changes, turnBusy, activity]);
|
|
447
|
+
y(() => {
|
|
448
|
+
const off1 = onDisplayMediaChange((active, reason) => {
|
|
449
|
+
setDisplayMediaActive(active);
|
|
450
|
+
if (reason === "denied") setDisplayMediaDenied(true);
|
|
451
|
+
if (reason === "granted") setDisplayMediaDenied(false);
|
|
452
|
+
});
|
|
453
|
+
const off2 = onCaptureSettingsChange((s) => setCaptureSettingsState(s));
|
|
454
|
+
return () => {
|
|
455
|
+
off1();
|
|
456
|
+
off2();
|
|
457
|
+
};
|
|
458
|
+
}, []);
|
|
436
459
|
y(() => {
|
|
437
460
|
const onKey = (ev) => {
|
|
438
461
|
const meta = ev.metaKey || ev.ctrlKey;
|
|
@@ -964,8 +987,79 @@ ${resolved.snippet}`
|
|
|
964
987
|
"div",
|
|
965
988
|
{ style: `color:${muted};margin-top:4px` },
|
|
966
989
|
"Writes proposed changes immediately. Still checkpointed & undoable; no manual gate. Resets on reload."
|
|
990
|
+
),
|
|
991
|
+
k(
|
|
992
|
+
"label",
|
|
993
|
+
{
|
|
994
|
+
style: "display:flex;gap:8px;align-items:center;cursor:pointer;color:#ececef;margin-top:10px"
|
|
995
|
+
},
|
|
996
|
+
[
|
|
997
|
+
k("input", {
|
|
998
|
+
type: "checkbox",
|
|
999
|
+
checked: captureSettings.alwaysPixelPerfect,
|
|
1000
|
+
onChange: (ev) => setCaptureSettings({
|
|
1001
|
+
alwaysPixelPerfect: ev.target.checked
|
|
1002
|
+
})
|
|
1003
|
+
}),
|
|
1004
|
+
k("span", {}, "Always pixel-perfect screenshots")
|
|
1005
|
+
]
|
|
1006
|
+
),
|
|
1007
|
+
k(
|
|
1008
|
+
"div",
|
|
1009
|
+
{ style: `color:${muted};margin-top:4px` },
|
|
1010
|
+
"Skips the silent rasterise path and always uses tab capture. One permission per session; every screenshot is OS-pixel accurate."
|
|
967
1011
|
)
|
|
968
1012
|
]) : null;
|
|
1013
|
+
const captureActivePill = displayMediaActive ? k(
|
|
1014
|
+
"div",
|
|
1015
|
+
{
|
|
1016
|
+
style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#1a2a1f;border:1px solid #2f5040;color:#9fe7b8;font-size:11px"
|
|
1017
|
+
},
|
|
1018
|
+
[
|
|
1019
|
+
k(
|
|
1020
|
+
"span",
|
|
1021
|
+
{ style: "display:inline-flex;align-items:center;gap:6px" },
|
|
1022
|
+
[
|
|
1023
|
+
k("span", {
|
|
1024
|
+
style: "width:8px;height:8px;border-radius:50%;background:#2fd16b;box-shadow:0 0 6px #2fd16b"
|
|
1025
|
+
}),
|
|
1026
|
+
k("span", {}, "Tab capture active")
|
|
1027
|
+
]
|
|
1028
|
+
),
|
|
1029
|
+
k(
|
|
1030
|
+
"button",
|
|
1031
|
+
{
|
|
1032
|
+
style: `${btn};margin-left:auto`,
|
|
1033
|
+
onClick: () => stopDisplayMedia("user"),
|
|
1034
|
+
title: "Stop sharing this tab"
|
|
1035
|
+
},
|
|
1036
|
+
"Stop"
|
|
1037
|
+
)
|
|
1038
|
+
]
|
|
1039
|
+
) : null;
|
|
1040
|
+
const captureDeniedNudge = displayMediaDenied && !displayMediaActive ? k(
|
|
1041
|
+
"div",
|
|
1042
|
+
{
|
|
1043
|
+
style: "display:flex;align-items:center;gap:8px;padding:6px 10px;margin:6px 0;border-radius:4px;background:#2a1f1a;border:1px solid #5a3a2a;color:#e8c69f;font-size:11px"
|
|
1044
|
+
},
|
|
1045
|
+
[
|
|
1046
|
+
k(
|
|
1047
|
+
"span",
|
|
1048
|
+
{ style: "flex:1" },
|
|
1049
|
+
"Screenshot missed some cross-origin content. Grant tab capture for pixel-perfect captures."
|
|
1050
|
+
),
|
|
1051
|
+
k(
|
|
1052
|
+
"button",
|
|
1053
|
+
{
|
|
1054
|
+
style: btn,
|
|
1055
|
+
onClick: () => {
|
|
1056
|
+
void retryDisplayMedia();
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
"Enable"
|
|
1060
|
+
)
|
|
1061
|
+
]
|
|
1062
|
+
) : null;
|
|
969
1063
|
const panel = open ? k(
|
|
970
1064
|
"div",
|
|
971
1065
|
{
|
|
@@ -1021,6 +1115,8 @@ ${resolved.snippet}`
|
|
|
1021
1115
|
]
|
|
1022
1116
|
),
|
|
1023
1117
|
settings,
|
|
1118
|
+
captureActivePill,
|
|
1119
|
+
captureDeniedNudge,
|
|
1024
1120
|
ctx,
|
|
1025
1121
|
conversation,
|
|
1026
1122
|
timeline,
|
|
@@ -6,8 +6,11 @@ import {
|
|
|
6
6
|
d,
|
|
7
7
|
installRuntimeCollectors,
|
|
8
8
|
k,
|
|
9
|
+
onDisplayMediaChange,
|
|
10
|
+
retryDisplayMedia,
|
|
11
|
+
stopDisplayMedia,
|
|
9
12
|
y
|
|
10
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-65BGOX2M.js";
|
|
11
14
|
|
|
12
15
|
// src/capture-only.ts
|
|
13
16
|
var DEFAULT_INGEST = "https://www.insitue.com/api/v1/capture";
|
|
@@ -68,6 +71,32 @@ function CaptureOnlyApp(props) {
|
|
|
68
71
|
const [phase, setPhase] = d("idle");
|
|
69
72
|
const [bundle, setBundle] = d(null);
|
|
70
73
|
const [note, setNote] = d("");
|
|
74
|
+
const [tabCaptureActive, setTabCaptureActive] = d(false);
|
|
75
|
+
const [tabCaptureDenied, setTabCaptureDenied] = d(false);
|
|
76
|
+
y(() => {
|
|
77
|
+
return onDisplayMediaChange((active, reason) => {
|
|
78
|
+
setTabCaptureActive(active);
|
|
79
|
+
if (reason === "denied") setTabCaptureDenied(true);
|
|
80
|
+
if (reason === "granted") setTabCaptureDenied(false);
|
|
81
|
+
});
|
|
82
|
+
}, []);
|
|
83
|
+
const retryWithPixelPerfect = async () => {
|
|
84
|
+
if (!bundle?.target?.selector) {
|
|
85
|
+
await retryDisplayMedia();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const granted = await retryDisplayMedia();
|
|
89
|
+
if (granted) {
|
|
90
|
+
setPhase("picking");
|
|
91
|
+
const sel = await beginPick("element").catch(() => null);
|
|
92
|
+
if (sel) {
|
|
93
|
+
setBundle(await buildBundle(sel));
|
|
94
|
+
setPhase("compose");
|
|
95
|
+
} else {
|
|
96
|
+
setPhase("compose");
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
71
100
|
const sink = new IssueTrackerSink(async (draft) => {
|
|
72
101
|
globalThis.__insitu_capture__ = {
|
|
73
102
|
title: draft.title,
|
|
@@ -235,6 +264,56 @@ function CaptureOnlyApp(props) {
|
|
|
235
264
|
},
|
|
236
265
|
"Screenshot unavailable \u2014 sending the rest."
|
|
237
266
|
) : null,
|
|
267
|
+
// Nudge: the screenshot is structurally OK but some content
|
|
268
|
+
// couldn't be embedded (non-CORS img / video / canvas). Offer
|
|
269
|
+
// a one-tap upgrade to pixel-perfect mode + re-pick.
|
|
270
|
+
bundle?.screenshot?.qualityNote && !tabCaptureActive ? k(
|
|
271
|
+
"div",
|
|
272
|
+
{
|
|
273
|
+
style: `display:flex;align-items:center;gap:8px;padding:9px 11px;background:#fff7ed;border:1px solid #fbd9b1;color:#8a4b00;border-radius:10px;font-size:12px;margin-bottom:12px`
|
|
274
|
+
},
|
|
275
|
+
[
|
|
276
|
+
k(
|
|
277
|
+
"span",
|
|
278
|
+
{ style: "flex:1" },
|
|
279
|
+
"Some content didn't capture cleanly. Enable tab capture for a pixel-perfect screenshot."
|
|
280
|
+
),
|
|
281
|
+
k(
|
|
282
|
+
"button",
|
|
283
|
+
{
|
|
284
|
+
onClick: () => void retryWithPixelPerfect(),
|
|
285
|
+
style: `all:unset;cursor:pointer;color:#5751e6;font-weight:600;padding:2px 8px;border:1px solid #c5c2ff;border-radius:6px;background:#fff`
|
|
286
|
+
},
|
|
287
|
+
"Enable"
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
) : null,
|
|
291
|
+
// Active badge — tells the user why the browser's red "tab
|
|
292
|
+
// sharing" indicator is on + lets them stop it.
|
|
293
|
+
tabCaptureActive ? k(
|
|
294
|
+
"div",
|
|
295
|
+
{
|
|
296
|
+
style: `display:flex;align-items:center;gap:8px;padding:7px 11px;background:#ecfdf5;border:1px solid #b6e6cf;color:#117a52;border-radius:10px;font-size:11.5px;margin-bottom:12px`
|
|
297
|
+
},
|
|
298
|
+
[
|
|
299
|
+
k("span", {
|
|
300
|
+
style: "width:8px;height:8px;border-radius:50%;background:#117a52;box-shadow:0 0 6px #117a52"
|
|
301
|
+
}),
|
|
302
|
+
k(
|
|
303
|
+
"span",
|
|
304
|
+
{ style: "flex:1" },
|
|
305
|
+
"Tab capture active \u2014 screenshots are pixel-perfect."
|
|
306
|
+
),
|
|
307
|
+
k(
|
|
308
|
+
"button",
|
|
309
|
+
{
|
|
310
|
+
onClick: () => stopDisplayMedia("user"),
|
|
311
|
+
style: `all:unset;cursor:pointer;color:#117a52;font-weight:600;padding:2px 6px`
|
|
312
|
+
},
|
|
313
|
+
"Stop"
|
|
314
|
+
)
|
|
315
|
+
]
|
|
316
|
+
) : null,
|
|
238
317
|
k("textarea", {
|
|
239
318
|
value: note,
|
|
240
319
|
rows: 3,
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountCaptureOnly
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-TTH4NBFA.js";
|
|
4
4
|
import {
|
|
5
5
|
mountInSitue
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-7GRJ5SZQ.js";
|
|
7
|
+
import "./chunk-65BGOX2M.js";
|
|
8
8
|
|
|
9
9
|
// src/InSitue.tsx
|
|
10
10
|
import { useEffect } from "react";
|
package/dist/overlay.js
CHANGED
package/package.json
CHANGED