@replohq/sandbox-runtime 1.7.13 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PageCaptureResponder.d.ts +1 -0
- package/PageCaptureResponder.js +12 -0
- package/_vendor/replo-utils/lib/crypto.mjs +1 -0
- package/_vendor/replo-utils/lib/math.mjs +1 -1
- package/_vendor/replo-utils/lib/pageCapture.d.ts +34 -0
- package/_vendor/replo-utils/lib/pageCapture.mjs +52 -0
- package/_vendor/schemas/errors.d.ts +1 -0
- package/_vendor/schemas/generated/selectMode.d.ts +25 -0
- package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
- package/_vendor/schemas/selectMode.d.ts +101 -2
- package/_vendor/schemas/selectMode.mjs +1 -1
- package/_vendor/schemas/selectModeIframeRpc.d.ts +148 -4
- package/_vendor/schemas/selectModeIframeRpc.mjs +1 -1
- package/_vendor/schemas/selectModeImage.d.ts +9 -0
- package/_vendor/schemas/selectModeImage.mjs +24 -0
- package/buildMetadata.js +3 -3
- package/capturePage.d.ts +11 -0
- package/capturePage.js +261 -0
- package/chunk-LIPH4O4F.mjs +67 -0
- package/{chunk-4T7WSMC3.mjs → chunk-XWNBKE63.mjs} +25 -1
- package/package.json +5 -4
- package/sandbox-runtime.js +2 -0
- package/select-mode/descriptor/elementDescriptor.js +2 -0
- package/select-mode/hooks/useOptimisticOverrides.js +63 -10
- package/select-mode/hooks/useSelectionInteractions.js +1 -10
- package/select-mode/iframeRpcChannel.d.ts +42 -0
- package/select-mode/imageOverridePreview.d.ts +6 -0
- package/select-mode/imageOverridePreview.js +77 -0
- package/select-mode/imageSources.d.ts +3 -0
- package/select-mode/imageSources.js +68 -0
- package/startPageCaptureResponder.d.ts +32 -0
- package/startPageCaptureResponder.js +87 -0
- package/chunk-SGKQ5HGT.mjs +0 -7
package/capturePage.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { snapdom } from "@zumer/snapdom";
|
|
2
|
+
import { PageCaptureError } from "./_vendor/replo-utils/lib/pageCapture.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const JPEG_QUALITY = 0.8;
|
|
5
|
+
const VIDEO_FRAME_QUALITY = 0.85;
|
|
6
|
+
const VIDEO_LOAD_TIMEOUT_MS = 4e3;
|
|
7
|
+
const VIDEO_SEEK_TIMEOUT_MS = 1500;
|
|
8
|
+
const PLACEHOLDER_BACKGROUND = "#d9d9d6";
|
|
9
|
+
const IFRAME_PLACEHOLDER_MARKER = "repeating-linear-gradient(45deg";
|
|
10
|
+
const vimeoOembedSchema = z.object({ thumbnail_url: z.string() });
|
|
11
|
+
function parseYouTubeId(src) {
|
|
12
|
+
const match = src.match(
|
|
13
|
+
/(?:youtube(?:-nocookie)?\.com\/embed\/|youtu\.be\/)([\w-]{11})/
|
|
14
|
+
);
|
|
15
|
+
return match?.[1] ?? null;
|
|
16
|
+
}
|
|
17
|
+
function parseVimeoId(src) {
|
|
18
|
+
const match = src.match(/player\.vimeo\.com\/video\/(\d+)/);
|
|
19
|
+
return match?.[1] ?? null;
|
|
20
|
+
}
|
|
21
|
+
function canReadIframeDocument(iframe) {
|
|
22
|
+
try {
|
|
23
|
+
return Boolean(iframe.contentDocument ?? iframe.contentWindow?.document);
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function fetchAsDataUrl(url) {
|
|
29
|
+
const blob = await (await fetch(url)).blob();
|
|
30
|
+
return await new Promise((resolve, reject) => {
|
|
31
|
+
const reader = new FileReader();
|
|
32
|
+
reader.addEventListener("load", () => {
|
|
33
|
+
resolve(String(reader.result));
|
|
34
|
+
});
|
|
35
|
+
reader.addEventListener("error", () => {
|
|
36
|
+
reject(reader.error ?? new PageCaptureError("image read failed"));
|
|
37
|
+
});
|
|
38
|
+
reader.readAsDataURL(blob);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function isCanvasReadable(source) {
|
|
42
|
+
try {
|
|
43
|
+
const canvas = document.createElement("canvas");
|
|
44
|
+
canvas.width = 1;
|
|
45
|
+
canvas.height = 1;
|
|
46
|
+
canvas.getContext("2d")?.drawImage(source, 0, 0, 1, 1);
|
|
47
|
+
canvas.toDataURL();
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function videoFrameDataUrl(video) {
|
|
54
|
+
const canvas = document.createElement("canvas");
|
|
55
|
+
canvas.width = video.videoWidth;
|
|
56
|
+
canvas.height = video.videoHeight;
|
|
57
|
+
const context = canvas.getContext("2d");
|
|
58
|
+
if (!context) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
context.drawImage(video, 0, 0);
|
|
62
|
+
return canvas.toDataURL("image/jpeg", VIDEO_FRAME_QUALITY);
|
|
63
|
+
}
|
|
64
|
+
function isOnScreen({
|
|
65
|
+
element,
|
|
66
|
+
height
|
|
67
|
+
}) {
|
|
68
|
+
const rect = element.getBoundingClientRect();
|
|
69
|
+
return rect.bottom > 0 && rect.top < height && rect.width > 0;
|
|
70
|
+
}
|
|
71
|
+
async function readVideoFrame({
|
|
72
|
+
video,
|
|
73
|
+
height
|
|
74
|
+
}) {
|
|
75
|
+
if (!isOnScreen({ element: video, height })) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA && isCanvasReadable(video)) {
|
|
79
|
+
return videoFrameDataUrl(video);
|
|
80
|
+
}
|
|
81
|
+
const copy = document.createElement("video");
|
|
82
|
+
copy.crossOrigin = "anonymous";
|
|
83
|
+
copy.muted = true;
|
|
84
|
+
copy.preload = "auto";
|
|
85
|
+
try {
|
|
86
|
+
copy.src = video.currentSrc || video.src;
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
copy.addEventListener("loadeddata", () => {
|
|
89
|
+
resolve();
|
|
90
|
+
});
|
|
91
|
+
copy.addEventListener("error", () => {
|
|
92
|
+
reject(new PageCaptureError("video load failed"));
|
|
93
|
+
});
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
reject(new PageCaptureError("video load timed out"));
|
|
96
|
+
}, VIDEO_LOAD_TIMEOUT_MS);
|
|
97
|
+
});
|
|
98
|
+
if (video.currentTime > 0.1) {
|
|
99
|
+
copy.currentTime = video.currentTime;
|
|
100
|
+
await new Promise((resolve) => {
|
|
101
|
+
copy.addEventListener("seeked", () => {
|
|
102
|
+
resolve();
|
|
103
|
+
});
|
|
104
|
+
setTimeout(resolve, VIDEO_SEEK_TIMEOUT_MS);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return isCanvasReadable(copy) ? videoFrameDataUrl(copy) : null;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
} finally {
|
|
111
|
+
copy.removeAttribute("src");
|
|
112
|
+
copy.load();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
async function readIframeThumbnail(iframe) {
|
|
116
|
+
const src = iframe.src;
|
|
117
|
+
let url = null;
|
|
118
|
+
const youTubeId = parseYouTubeId(src);
|
|
119
|
+
if (youTubeId) {
|
|
120
|
+
url = `https://i.ytimg.com/vi/${youTubeId}/hqdefault.jpg`;
|
|
121
|
+
}
|
|
122
|
+
const vimeoId = parseVimeoId(src);
|
|
123
|
+
if (vimeoId) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(
|
|
126
|
+
`https://vimeo.com/api/oembed.json?url=https://vimeo.com/${vimeoId}&width=1280`
|
|
127
|
+
);
|
|
128
|
+
const body = vimeoOembedSchema.safeParse(await response.json());
|
|
129
|
+
url = body.success ? body.data.thumbnail_url : null;
|
|
130
|
+
} catch {
|
|
131
|
+
url = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!url) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return fetchAsDataUrl(url).catch(() => null);
|
|
138
|
+
}
|
|
139
|
+
function createStandInsPlugin({
|
|
140
|
+
objectFitByFrame,
|
|
141
|
+
iframeThumbnails
|
|
142
|
+
}) {
|
|
143
|
+
return {
|
|
144
|
+
name: "replo-page-capture-stand-ins",
|
|
145
|
+
afterClone(context) {
|
|
146
|
+
const clone = context.clone;
|
|
147
|
+
if (!clone) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
for (const image of clone.querySelectorAll("img")) {
|
|
151
|
+
const objectFit = objectFitByFrame.get(image.getAttribute("src") ?? "");
|
|
152
|
+
if (objectFit) {
|
|
153
|
+
image.style.objectFit = objectFit;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const placeholders = Array.from(
|
|
157
|
+
clone.querySelectorAll("div")
|
|
158
|
+
).filter((element) => {
|
|
159
|
+
return (element.getAttribute("style") ?? "").includes(
|
|
160
|
+
IFRAME_PLACEHOLDER_MARKER
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
placeholders.forEach((placeholder, index) => {
|
|
164
|
+
const thumbnail = iframeThumbnails[index];
|
|
165
|
+
placeholder.style.backgroundImage = thumbnail ? `url("${thumbnail}")` : "none";
|
|
166
|
+
placeholder.style.backgroundColor = PLACEHOLDER_BACKGROUND;
|
|
167
|
+
placeholder.style.backgroundSize = "cover";
|
|
168
|
+
placeholder.style.backgroundPosition = "center";
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function pageBackgroundColor() {
|
|
174
|
+
for (const element of [document.body, document.documentElement]) {
|
|
175
|
+
const color = getComputedStyle(element).backgroundColor;
|
|
176
|
+
if (color && color !== "transparent" && color !== "rgba(0, 0, 0, 0)") {
|
|
177
|
+
return color;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return "#fff";
|
|
181
|
+
}
|
|
182
|
+
function canvasToJpeg(canvas) {
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
canvas.toBlob(
|
|
185
|
+
(blob) => {
|
|
186
|
+
if (blob) {
|
|
187
|
+
resolve(blob);
|
|
188
|
+
} else {
|
|
189
|
+
reject(new PageCaptureError("jpeg encode failed"));
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
"image/jpeg",
|
|
193
|
+
JPEG_QUALITY
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async function capturePage({
|
|
198
|
+
width,
|
|
199
|
+
height
|
|
200
|
+
}) {
|
|
201
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
202
|
+
const iframes = Array.from(document.querySelectorAll("iframe")).filter(
|
|
203
|
+
(iframe) => !canReadIframeDocument(iframe)
|
|
204
|
+
);
|
|
205
|
+
const [videoFrames, iframeThumbnails] = await Promise.all([
|
|
206
|
+
Promise.all(
|
|
207
|
+
videos.map((video) => {
|
|
208
|
+
return readVideoFrame({ video, height });
|
|
209
|
+
})
|
|
210
|
+
),
|
|
211
|
+
Promise.all(iframes.map(readIframeThumbnail))
|
|
212
|
+
]);
|
|
213
|
+
const originalPosters = [];
|
|
214
|
+
const objectFitByFrame = /* @__PURE__ */ new Map();
|
|
215
|
+
videos.forEach((video, index) => {
|
|
216
|
+
const frame = videoFrames[index];
|
|
217
|
+
if (!frame) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
originalPosters.push({ video, poster: video.getAttribute("poster") });
|
|
221
|
+
video.setAttribute("poster", frame);
|
|
222
|
+
objectFitByFrame.set(frame, getComputedStyle(video).objectFit);
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
const raw = await snapdom.toRaw(document.body, {
|
|
226
|
+
scale: 1,
|
|
227
|
+
embedFonts: true,
|
|
228
|
+
plugins: [createStandInsPlugin({ objectFitByFrame, iframeThumbnails })]
|
|
229
|
+
});
|
|
230
|
+
const image = new Image();
|
|
231
|
+
image.src = raw;
|
|
232
|
+
await image.decode();
|
|
233
|
+
await new Promise((resolve) => {
|
|
234
|
+
setTimeout(resolve, 100);
|
|
235
|
+
});
|
|
236
|
+
const canvas = document.createElement("canvas");
|
|
237
|
+
canvas.width = width;
|
|
238
|
+
canvas.height = height;
|
|
239
|
+
const context = canvas.getContext("2d");
|
|
240
|
+
if (!context) {
|
|
241
|
+
throw new PageCaptureError("canvas 2d context unavailable");
|
|
242
|
+
}
|
|
243
|
+
context.fillStyle = pageBackgroundColor();
|
|
244
|
+
context.fillRect(0, 0, width, height);
|
|
245
|
+
context.drawImage(image, 0, 0);
|
|
246
|
+
return await canvasToJpeg(canvas);
|
|
247
|
+
} finally {
|
|
248
|
+
for (const { video, poster } of originalPosters) {
|
|
249
|
+
if (poster === null) {
|
|
250
|
+
video.removeAttribute("poster");
|
|
251
|
+
} else {
|
|
252
|
+
video.setAttribute("poster", poster);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
export {
|
|
258
|
+
capturePage,
|
|
259
|
+
parseVimeoId,
|
|
260
|
+
parseYouTubeId
|
|
261
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { v4 } from 'uuid';
|
|
2
|
+
|
|
3
|
+
function bytesToBase64Url(bytes) {
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (const byte of bytes) {
|
|
6
|
+
binary += String.fromCharCode(byte);
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
9
|
+
}
|
|
10
|
+
function base64UrlToBytes(input) {
|
|
11
|
+
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
12
|
+
const padded = normalized.padEnd(
|
|
13
|
+
normalized.length + (4 - normalized.length % 4) % 4,
|
|
14
|
+
"="
|
|
15
|
+
);
|
|
16
|
+
const binary = atob(padded);
|
|
17
|
+
const bytes = new Uint8Array(binary.length);
|
|
18
|
+
for (let i = 0; i < binary.length; i += 1) {
|
|
19
|
+
bytes[i] = binary.charCodeAt(i);
|
|
20
|
+
}
|
|
21
|
+
return bytes;
|
|
22
|
+
}
|
|
23
|
+
function randomBase64Url(byteLength) {
|
|
24
|
+
return bytesToBase64Url(crypto.getRandomValues(new Uint8Array(byteLength)));
|
|
25
|
+
}
|
|
26
|
+
function randomUUID() {
|
|
27
|
+
return v4();
|
|
28
|
+
}
|
|
29
|
+
async function sha256Hex(input) {
|
|
30
|
+
const buffer = typeof input === "string" ? new TextEncoder().encode(input) : input;
|
|
31
|
+
const hash = await crypto.subtle.digest("SHA-256", buffer);
|
|
32
|
+
return [...new Uint8Array(hash)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
33
|
+
}
|
|
34
|
+
function constantTimeEqual({
|
|
35
|
+
left,
|
|
36
|
+
right
|
|
37
|
+
}) {
|
|
38
|
+
if (left.length !== right.length) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
let difference = 0;
|
|
42
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
43
|
+
difference |= (left[index] ?? 0) ^ (right[index] ?? 0);
|
|
44
|
+
}
|
|
45
|
+
return difference === 0;
|
|
46
|
+
}
|
|
47
|
+
async function getHmacSha256Hex({
|
|
48
|
+
rawBody,
|
|
49
|
+
signingKey
|
|
50
|
+
}) {
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const key = await crypto.subtle.importKey(
|
|
53
|
+
"raw",
|
|
54
|
+
encoder.encode(signingKey),
|
|
55
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
56
|
+
false,
|
|
57
|
+
["sign"]
|
|
58
|
+
);
|
|
59
|
+
const signature = await crypto.subtle.sign(
|
|
60
|
+
"HMAC",
|
|
61
|
+
key,
|
|
62
|
+
encoder.encode(rawBody)
|
|
63
|
+
);
|
|
64
|
+
return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { base64UrlToBytes, bytesToBase64Url, constantTimeEqual, getHmacSha256Hex, randomBase64Url, randomUUID, sha256Hex };
|
|
@@ -100,6 +100,25 @@ function getIframeOffset(iframe) {
|
|
|
100
100
|
const iframeRect = iframe.getBoundingClientRect();
|
|
101
101
|
return { left: iframeRect.left, top: iframeRect.top };
|
|
102
102
|
}
|
|
103
|
+
var selectModeImageScopeSchema = z.discriminatedUnion("kind", [
|
|
104
|
+
z.object({ kind: z.literal("all") }),
|
|
105
|
+
z.object({ kind: z.literal("media"), media: z.string() })
|
|
106
|
+
]).describe("SelectModeImageScope");
|
|
107
|
+
var selectModeImageSourceSchema = z.object({
|
|
108
|
+
srcSet: z.string(),
|
|
109
|
+
sizes: z.string(),
|
|
110
|
+
media: z.string(),
|
|
111
|
+
type: z.string(),
|
|
112
|
+
previewUrl: z.string().nullable()
|
|
113
|
+
}).describe("SelectModeImageSource");
|
|
114
|
+
var selectModeImageSchema = z.object({
|
|
115
|
+
src: z.string(),
|
|
116
|
+
currentSrc: z.string(),
|
|
117
|
+
srcSet: z.string(),
|
|
118
|
+
sizes: z.string(),
|
|
119
|
+
previewUrl: z.string().nullable(),
|
|
120
|
+
sources: z.array(selectModeImageSourceSchema)
|
|
121
|
+
}).describe("SelectModeImage");
|
|
103
122
|
var selectedElementSchema = z.object({
|
|
104
123
|
kind: z.literal("react-element"),
|
|
105
124
|
selector: z.string(),
|
|
@@ -109,6 +128,7 @@ var selectedElementSchema = z.object({
|
|
|
109
128
|
rect: selectModeBoundingRectSchema,
|
|
110
129
|
textMetrics: selectModeTextMetricsSchema.nullable(),
|
|
111
130
|
outerHTML: z.string(),
|
|
131
|
+
image: selectModeImageSchema.optional(),
|
|
112
132
|
definedStyles: z.array(selectModeDefinedStyleSchema).default([]),
|
|
113
133
|
inheritedStyles: z.record(z.string(), z.string()).default({}),
|
|
114
134
|
clickPosition: selectModeClickPositionSchema,
|
|
@@ -166,6 +186,10 @@ function readSelectablePayload(rawPayload) {
|
|
|
166
186
|
return parseSchemaOrNull(parsedPayload, selectableDataPayloadSchema);
|
|
167
187
|
}
|
|
168
188
|
var selectModeOptimisticUpdatePayloadSchema = z.discriminatedUnion("kind", [
|
|
189
|
+
z.object({
|
|
190
|
+
kind: z.literal("image"),
|
|
191
|
+
image: selectModeImageSchema
|
|
192
|
+
}),
|
|
169
193
|
z.object({
|
|
170
194
|
kind: z.literal("css"),
|
|
171
195
|
fontSource: z.enum(["google", "site"]).optional(),
|
|
@@ -208,4 +232,4 @@ var selectModeSelectionSchema = z.discriminatedUnion("type", [
|
|
|
208
232
|
})
|
|
209
233
|
]).describe("SelectModeSelection");
|
|
210
234
|
|
|
211
|
-
export { SELECT_MODE_SELECTABLE_ATTRIBUTE, SELECT_MODE_SELECTABLE_PAYLOAD_ATTRIBUTE, displayModeSchema, getIframeOffset, readSelectablePayload, selectModeBoundingRectSchema, selectModeClickPositionSchema, selectModeDefinedStyleSchema, selectModeFiberChainEntrySchema, selectModeFormatCommandSchema, selectModeInlineTextStatusSchema, selectModeOptimisticUpdatePayloadSchema, selectModeSelectionRectSchema, selectModeSelectionSchema, selectModeSelectionTypeSchema, selectModeSourceLocationSchema, selectModeStyleAtRuleSchema, selectModeStyleVariantSchema, selectModeTextMetricsSchema, selectModeTiptapActiveMarksSchema, selectableDataPayloadSchema, selectableDataSchema, selectablePlanDataSchema, selectablePlanPayloadSchema, selectedElementSchema, transformPoint, transformRect, transformSelectableData, transformSelectedElement };
|
|
235
|
+
export { SELECT_MODE_SELECTABLE_ATTRIBUTE, SELECT_MODE_SELECTABLE_PAYLOAD_ATTRIBUTE, displayModeSchema, getIframeOffset, readSelectablePayload, selectModeBoundingRectSchema, selectModeClickPositionSchema, selectModeDefinedStyleSchema, selectModeFiberChainEntrySchema, selectModeFormatCommandSchema, selectModeImageSchema, selectModeImageScopeSchema, selectModeImageSourceSchema, selectModeInlineTextStatusSchema, selectModeOptimisticUpdatePayloadSchema, selectModeSelectionRectSchema, selectModeSelectionSchema, selectModeSelectionTypeSchema, selectModeSourceLocationSchema, selectModeStyleAtRuleSchema, selectModeStyleVariantSchema, selectModeTextMetricsSchema, selectModeTiptapActiveMarksSchema, selectableDataPayloadSchema, selectableDataSchema, selectablePlanDataSchema, selectablePlanPayloadSchema, selectedElementSchema, transformPoint, transformRect, transformSelectableData, transformSelectedElement };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sandbox-runtime",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.9.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-09-
|
|
8
|
+
"commit": "455eef2903f4cc6a8f457d5ec77be1e1dc95aad9",
|
|
9
|
+
"builtAt": "2026-09-25T00:24:57.880Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@tiptap/extension-text": "^2.25.0",
|
|
41
41
|
"@tiptap/extension-underline": "^2.25.0",
|
|
42
42
|
"@tiptap/pm": "^2.25.0",
|
|
43
|
+
"@zumer/snapdom": "^3.0.0",
|
|
43
44
|
"uuid": "^11.1.0",
|
|
44
45
|
"zod": "^4.3.6",
|
|
45
46
|
"zustand": "^4.5.7"
|
package/sandbox-runtime.js
CHANGED
|
@@ -8,6 +8,7 @@ import { DocumentReadyBeacon } from "./DocumentReadyBeacon";
|
|
|
8
8
|
import LinkInterceptor from "./LinkInterceptor";
|
|
9
9
|
import { NavigationReceiver } from "./NavigationReceiver";
|
|
10
10
|
import { NextErrorReporter } from "./NextErrorReporter";
|
|
11
|
+
import { PageCaptureResponder } from "./PageCaptureResponder";
|
|
11
12
|
import PostHogRelay from "./posthog-relay";
|
|
12
13
|
import { EditorSelection } from "./select-mode/EditorSelection";
|
|
13
14
|
function ViewportSnapshotResponder() {
|
|
@@ -24,6 +25,7 @@ function SandboxRuntime({ children }) {
|
|
|
24
25
|
/* @__PURE__ */ jsx(NavigationReceiver, {}),
|
|
25
26
|
/* @__PURE__ */ jsx(PostHogRelay, {}),
|
|
26
27
|
/* @__PURE__ */ jsx(ViewportSnapshotResponder, {}),
|
|
28
|
+
/* @__PURE__ */ jsx(PageCaptureResponder, {}),
|
|
27
29
|
/* @__PURE__ */ jsx(EditorSelection, {}),
|
|
28
30
|
/* @__PURE__ */ jsx(AnnotationLayerMount, {}),
|
|
29
31
|
/* @__PURE__ */ jsx(DesignLibraryPreview, {}),
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
SELECT_MODE_SELECTABLE_PAYLOAD_ATTRIBUTE
|
|
5
5
|
} from "../../_vendor/schemas/selectMode.mjs";
|
|
6
6
|
import { getFiberChain } from "../fiber";
|
|
7
|
+
import { getImageDescriptor } from "../imageSources";
|
|
7
8
|
import { skipInlineTextMarkElements } from "../inlineTextMarks";
|
|
8
9
|
import { getElementBoundingRect } from "../interactions/selectionGeometry";
|
|
9
10
|
import { getDefinedStyles, getInheritedStyles } from "./styleExtraction";
|
|
@@ -185,6 +186,7 @@ function buildElementDescriptor({
|
|
|
185
186
|
rect: getElementBoundingRect(element),
|
|
186
187
|
textMetrics,
|
|
187
188
|
outerHTML: element.outerHTML.slice(0, 4e3),
|
|
189
|
+
image: getImageDescriptor(element),
|
|
188
190
|
definedStyles: getDefinedStyles(element),
|
|
189
191
|
inheritedStyles: getInheritedStyles(element),
|
|
190
192
|
clickPosition,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { buildGoogleFontsCssUrl, quoteFontFamily } from "../../_vendor/replo-utils/lib/fonts.mjs";
|
|
3
3
|
import { buildSelector } from "../descriptor/elementDescriptor";
|
|
4
|
+
import { createImagePreview } from "../imageOverridePreview";
|
|
4
5
|
import {
|
|
5
6
|
SELECT_MODE_OVERRIDE_ATTRIBUTE,
|
|
6
7
|
SELECT_MODE_OVERRIDE_STYLE_ATTRIBUTE
|
|
@@ -10,6 +11,7 @@ const OVERRIDE_REGISTRATION_TIMEOUT_MS = 15e3;
|
|
|
10
11
|
const SESSION_OUTCOME_RETENTION_MS = 6e4;
|
|
11
12
|
const OVERRIDE_REGISTRY_KEY = "reploSelectOverrideRegistryV2";
|
|
12
13
|
const SESSION_OUTCOME_REGISTRY_KEY = "reploSelectOverrideSessionOutcomesV2";
|
|
14
|
+
const imagePreviews = /* @__PURE__ */ new Map();
|
|
13
15
|
function getOverrideList() {
|
|
14
16
|
const existing = Reflect.get(window, OVERRIDE_REGISTRY_KEY);
|
|
15
17
|
if (Array.isArray(existing)) {
|
|
@@ -45,18 +47,12 @@ function getElementsForTargetMode({
|
|
|
45
47
|
})
|
|
46
48
|
];
|
|
47
49
|
}
|
|
48
|
-
function isImageSourcePayload({
|
|
49
|
-
element,
|
|
50
|
-
payload
|
|
51
|
-
}) {
|
|
52
|
-
return payload.kind === "attribute" && payload.name.toLowerCase() === "src" && element instanceof HTMLImageElement;
|
|
53
|
-
}
|
|
54
50
|
function getOriginalState({
|
|
55
51
|
element,
|
|
56
52
|
payload
|
|
57
53
|
}) {
|
|
58
54
|
if (payload.kind === "attribute") {
|
|
59
|
-
const names =
|
|
55
|
+
const names = [payload.name];
|
|
60
56
|
return {
|
|
61
57
|
kind: "attributes",
|
|
62
58
|
values: names.map((name) => ({
|
|
@@ -95,14 +91,12 @@ function applyPayload({
|
|
|
95
91
|
payload,
|
|
96
92
|
token
|
|
97
93
|
}) {
|
|
94
|
+
if (payload.kind === "image") return;
|
|
98
95
|
if (payload.kind === "css") {
|
|
99
96
|
addToken(element, token);
|
|
100
97
|
return;
|
|
101
98
|
}
|
|
102
99
|
if (payload.kind === "attribute") {
|
|
103
|
-
if (isImageSourcePayload({ element, payload })) {
|
|
104
|
-
element.removeAttribute("srcset");
|
|
105
|
-
}
|
|
106
100
|
element.setAttribute(payload.name, payload.value);
|
|
107
101
|
return;
|
|
108
102
|
}
|
|
@@ -207,6 +201,7 @@ function reapplyOverride({
|
|
|
207
201
|
override,
|
|
208
202
|
skipElement
|
|
209
203
|
}) {
|
|
204
|
+
if (override.payload.kind === "image") return;
|
|
210
205
|
for (const target of override.targets) {
|
|
211
206
|
const element = resolveTarget(target);
|
|
212
207
|
if (!element || skipElement && element.contains(skipElement)) {
|
|
@@ -222,6 +217,51 @@ function reapplyOverride({
|
|
|
222
217
|
applyPayload({ element, payload: override.payload, token: override.token });
|
|
223
218
|
}
|
|
224
219
|
}
|
|
220
|
+
function syncImagePreviews() {
|
|
221
|
+
const configurations = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const override of getOverrideList()) {
|
|
223
|
+
if (override.payload.kind !== "image") continue;
|
|
224
|
+
for (const target of override.targets) {
|
|
225
|
+
const element = resolveTarget(target);
|
|
226
|
+
if (!(element instanceof HTMLImageElement)) continue;
|
|
227
|
+
const previous = configurations.get(element);
|
|
228
|
+
configurations.set(element, {
|
|
229
|
+
image: override.payload.image,
|
|
230
|
+
pending: !override.settled || Boolean(previous?.pending)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const [element, preview] of imagePreviews) {
|
|
235
|
+
if (configurations.has(element)) continue;
|
|
236
|
+
preview.dispose();
|
|
237
|
+
imagePreviews.delete(element);
|
|
238
|
+
}
|
|
239
|
+
const acknowledged = /* @__PURE__ */ new Set();
|
|
240
|
+
for (const [element, configuration] of configurations) {
|
|
241
|
+
let preview = imagePreviews.get(element);
|
|
242
|
+
if (!preview) {
|
|
243
|
+
preview = createImagePreview(element);
|
|
244
|
+
imagePreviews.set(element, preview);
|
|
245
|
+
}
|
|
246
|
+
if (!configuration.pending && preview.isPersisted(configuration.image)) {
|
|
247
|
+
preview.dispose();
|
|
248
|
+
imagePreviews.delete(element);
|
|
249
|
+
acknowledged.add(element);
|
|
250
|
+
} else {
|
|
251
|
+
preview.render(configuration.image);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
setOverrideList(
|
|
255
|
+
getOverrideList().filter((entry) => {
|
|
256
|
+
if (entry.payload.kind !== "image") return true;
|
|
257
|
+
entry.targets = entry.targets.filter((target) => {
|
|
258
|
+
const element = resolveTarget(target);
|
|
259
|
+
return !element || !acknowledged.has(element);
|
|
260
|
+
});
|
|
261
|
+
return entry.targets.length > 0;
|
|
262
|
+
})
|
|
263
|
+
);
|
|
264
|
+
}
|
|
225
265
|
function removeOverride({
|
|
226
266
|
editId,
|
|
227
267
|
outcome
|
|
@@ -233,6 +273,16 @@ function removeOverride({
|
|
|
233
273
|
return;
|
|
234
274
|
}
|
|
235
275
|
clearTimeout(override.registrationTimeout);
|
|
276
|
+
if (override.payload.kind === "image") {
|
|
277
|
+
override.settled = true;
|
|
278
|
+
setOverrideList(
|
|
279
|
+
getOverrideList().filter(
|
|
280
|
+
(entry) => entry !== override || outcome === "persisted"
|
|
281
|
+
)
|
|
282
|
+
);
|
|
283
|
+
syncImagePreviews();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
236
286
|
getStyleElement(override.token)?.remove();
|
|
237
287
|
for (const target of override.targets) {
|
|
238
288
|
const element = resolveTarget(target);
|
|
@@ -279,6 +329,7 @@ function useOptimisticOverrides() {
|
|
|
279
329
|
});
|
|
280
330
|
const token = `override-${editId}`;
|
|
281
331
|
const override = {
|
|
332
|
+
settled: false,
|
|
282
333
|
editId,
|
|
283
334
|
injectedFontFamily: null,
|
|
284
335
|
payload,
|
|
@@ -296,6 +347,7 @@ function useOptimisticOverrides() {
|
|
|
296
347
|
getOverrideList().push(override);
|
|
297
348
|
installCssRule(override);
|
|
298
349
|
reapplyOverride({ override, skipElement: null });
|
|
350
|
+
syncImagePreviews();
|
|
299
351
|
},
|
|
300
352
|
[]
|
|
301
353
|
);
|
|
@@ -304,6 +356,7 @@ function useOptimisticOverrides() {
|
|
|
304
356
|
for (const override of getOverrideList()) {
|
|
305
357
|
reapplyOverride({ override, skipElement });
|
|
306
358
|
}
|
|
359
|
+
syncImagePreviews();
|
|
307
360
|
},
|
|
308
361
|
[]
|
|
309
362
|
);
|
|
@@ -402,16 +402,7 @@ function useSelectionInteractions() {
|
|
|
402
402
|
const scrollOffset = snapshot.type === "drag" ? snapshot.scrollOffset : getDocumentScrollOffset();
|
|
403
403
|
const candidates = store().dragCandidates;
|
|
404
404
|
resetDragState({ notifyParentDragCleared: false });
|
|
405
|
-
if (
|
|
406
|
-
const targetElement = event.target;
|
|
407
|
-
if (!(targetElement instanceof Element)) {
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
commitClickSelection({
|
|
411
|
-
targetElement,
|
|
412
|
-
pointerEvent: event,
|
|
413
|
-
candidates
|
|
414
|
-
});
|
|
405
|
+
if (snapshot.type !== "drag" || !dragStart) {
|
|
415
406
|
return;
|
|
416
407
|
}
|
|
417
408
|
const resolvedDragCurrent = dragCurrent ?? dragStart;
|
|
@@ -32,6 +32,20 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
|
|
|
32
32
|
lineHeight: import("zod").ZodNumber;
|
|
33
33
|
}, import("zod/v4/core").$strip>>;
|
|
34
34
|
outerHTML: import("zod").ZodString;
|
|
35
|
+
image: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
36
|
+
src: import("zod").ZodString;
|
|
37
|
+
currentSrc: import("zod").ZodString;
|
|
38
|
+
srcSet: import("zod").ZodString;
|
|
39
|
+
sizes: import("zod").ZodString;
|
|
40
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
41
|
+
sources: import("zod").ZodArray<import("zod").ZodObject<{
|
|
42
|
+
srcSet: import("zod").ZodString;
|
|
43
|
+
sizes: import("zod").ZodString;
|
|
44
|
+
media: import("zod").ZodString;
|
|
45
|
+
type: import("zod").ZodString;
|
|
46
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
47
|
+
}, import("zod/v4/core").$strip>>;
|
|
48
|
+
}, import("zod/v4/core").$strip>>;
|
|
35
49
|
definedStyles: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodObject<{
|
|
36
50
|
property: import("zod").ZodString;
|
|
37
51
|
variant: import("zod").ZodNullable<import("zod").ZodObject<{
|
|
@@ -109,6 +123,20 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
|
|
|
109
123
|
lineHeight: import("zod").ZodNumber;
|
|
110
124
|
}, import("zod/v4/core").$strip>>;
|
|
111
125
|
outerHTML: import("zod").ZodString;
|
|
126
|
+
image: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
127
|
+
src: import("zod").ZodString;
|
|
128
|
+
currentSrc: import("zod").ZodString;
|
|
129
|
+
srcSet: import("zod").ZodString;
|
|
130
|
+
sizes: import("zod").ZodString;
|
|
131
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
132
|
+
sources: import("zod").ZodArray<import("zod").ZodObject<{
|
|
133
|
+
srcSet: import("zod").ZodString;
|
|
134
|
+
sizes: import("zod").ZodString;
|
|
135
|
+
media: import("zod").ZodString;
|
|
136
|
+
type: import("zod").ZodString;
|
|
137
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
138
|
+
}, import("zod/v4/core").$strip>>;
|
|
139
|
+
}, import("zod/v4/core").$strip>>;
|
|
112
140
|
definedStyles: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodObject<{
|
|
113
141
|
property: import("zod").ZodString;
|
|
114
142
|
variant: import("zod").ZodNullable<import("zod").ZodObject<{
|
|
@@ -274,6 +302,20 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
|
|
|
274
302
|
lineHeight: import("zod").ZodNumber;
|
|
275
303
|
}, import("zod/v4/core").$strip>>;
|
|
276
304
|
outerHTML: import("zod").ZodString;
|
|
305
|
+
image: import("zod").ZodOptional<import("zod").ZodObject<{
|
|
306
|
+
src: import("zod").ZodString;
|
|
307
|
+
currentSrc: import("zod").ZodString;
|
|
308
|
+
srcSet: import("zod").ZodString;
|
|
309
|
+
sizes: import("zod").ZodString;
|
|
310
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
311
|
+
sources: import("zod").ZodArray<import("zod").ZodObject<{
|
|
312
|
+
srcSet: import("zod").ZodString;
|
|
313
|
+
sizes: import("zod").ZodString;
|
|
314
|
+
media: import("zod").ZodString;
|
|
315
|
+
type: import("zod").ZodString;
|
|
316
|
+
previewUrl: import("zod").ZodNullable<import("zod").ZodString>;
|
|
317
|
+
}, import("zod/v4/core").$strip>>;
|
|
318
|
+
}, import("zod/v4/core").$strip>>;
|
|
277
319
|
definedStyles: import("zod").ZodDefault<import("zod").ZodArray<import("zod").ZodObject<{
|
|
278
320
|
property: import("zod").ZodString;
|
|
279
321
|
variant: import("zod").ZodNullable<import("zod").ZodObject<{
|