@replohq/sandbox-runtime 1.8.0 → 1.10.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/fonts.mjs +18 -6
- 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/generated/selectMode.d.ts +14 -0
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
- package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
- package/_vendor/schemas/selectMode.d.ts +26 -2
- package/_vendor/schemas/selectMode.mjs +1 -1
- package/_vendor/schemas/selectModeIframeRpc.d.ts +53 -4
- package/_vendor/schemas/selectModeIframeRpc.mjs +18 -2
- package/_vendor/schemas/selectModeImage.mjs +6 -2
- package/buildMetadata.js +3 -3
- package/capturePage.d.ts +11 -0
- package/capturePage.js +261 -0
- package/chunk-LIPH4O4F.mjs +67 -0
- package/{chunk-XWNBKE63.mjs → chunk-PJBGUXBG.mjs} +6 -1
- package/design-library-preview/DesignLibraryPreview.js +18 -6
- package/package.json +5 -4
- package/sandbox-runtime.js +2 -0
- package/select-mode/EditorSelection.js +21 -4
- package/select-mode/SelectionFrames.js +56 -23
- package/select-mode/descriptor/elementDescriptor.d.ts +2 -1
- package/select-mode/descriptor/elementDescriptor.js +22 -14
- package/select-mode/hooks/useOptimisticOverrides.js +18 -6
- package/select-mode/hooks/useParentMessaging.d.ts +7 -1
- package/select-mode/hooks/useParentMessaging.js +6 -0
- package/select-mode/hooks/useSelectionHover.js +38 -15
- package/select-mode/hooks/useSelectionInteractions.js +9 -37
- package/select-mode/hooks/useSelectionScrollSync.js +5 -1
- package/select-mode/iframeRpcChannel.d.ts +10 -0
- package/select-mode/iframeRpcChannel.js +2 -0
- package/select-mode/imageOverridePreview.js +14 -5
- package/select-mode/imageSources.js +2 -1
- package/select-mode/inlineEditFeedback.js +9 -3
- package/select-mode/interactions/sectionGeometry.d.ts +3 -0
- package/select-mode/interactions/sectionGeometry.js +200 -0
- package/select-mode/selectModeStore.d.ts +36 -8
- package/select-mode/selectModeStore.js +141 -21
- package/select-mode/selectionFrame.d.ts +3 -1
- package/select-mode/selectionFrame.js +4 -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 };
|
|
@@ -48,6 +48,7 @@ var selectModeBoundingRectSchema = z.object({
|
|
|
48
48
|
width: z.number(),
|
|
49
49
|
height: z.number()
|
|
50
50
|
}).describe("SelectModeBoundingRect");
|
|
51
|
+
var selectModeHoverRectSchema = selectModeBoundingRectSchema.extend({ isSection: z.boolean().optional() }).describe("SelectModeHoverRect");
|
|
51
52
|
var selectModeSelectionRectSchema = z.object({
|
|
52
53
|
id: z.string(),
|
|
53
54
|
rect: selectModeBoundingRectSchema
|
|
@@ -122,6 +123,7 @@ var selectModeImageSchema = z.object({
|
|
|
122
123
|
var selectedElementSchema = z.object({
|
|
123
124
|
kind: z.literal("react-element"),
|
|
124
125
|
selector: z.string(),
|
|
126
|
+
section: z.object({ selector: z.string() }).optional(),
|
|
125
127
|
tagName: z.string(),
|
|
126
128
|
elementType: z.string(),
|
|
127
129
|
textContent: z.string().nullable(),
|
|
@@ -145,6 +147,9 @@ var selectablePlanDataSchema = z.object({
|
|
|
145
147
|
var selectablePlanPayloadSchema = selectablePlanDataSchema.omit({ rect: true }).describe("SelectablePlanPayload");
|
|
146
148
|
var selectableDataPayloadSchema = z.discriminatedUnion("kind", [selectablePlanPayloadSchema]).describe("SelectableDataPayload");
|
|
147
149
|
var selectableDataSchema = z.discriminatedUnion("kind", [selectedElementSchema, selectablePlanDataSchema]).describe("SelectableData");
|
|
150
|
+
function isSelectedSection(data) {
|
|
151
|
+
return data.kind === "react-element" && data.selector === data.section?.selector;
|
|
152
|
+
}
|
|
148
153
|
function transformSelectedElement({
|
|
149
154
|
element,
|
|
150
155
|
offset,
|
|
@@ -232,4 +237,4 @@ var selectModeSelectionSchema = z.discriminatedUnion("type", [
|
|
|
232
237
|
})
|
|
233
238
|
]).describe("SelectModeSelection");
|
|
234
239
|
|
|
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 };
|
|
240
|
+
export { SELECT_MODE_SELECTABLE_ATTRIBUTE, SELECT_MODE_SELECTABLE_PAYLOAD_ATTRIBUTE, displayModeSchema, getIframeOffset, isSelectedSection, readSelectablePayload, selectModeBoundingRectSchema, selectModeClickPositionSchema, selectModeDefinedStyleSchema, selectModeFiberChainEntrySchema, selectModeFormatCommandSchema, selectModeHoverRectSchema, selectModeImageSchema, selectModeImageScopeSchema, selectModeImageSourceSchema, selectModeInlineTextStatusSchema, selectModeOptimisticUpdatePayloadSchema, selectModeSelectionRectSchema, selectModeSelectionSchema, selectModeSelectionTypeSchema, selectModeSourceLocationSchema, selectModeStyleAtRuleSchema, selectModeStyleVariantSchema, selectModeTextMetricsSchema, selectModeTiptapActiveMarksSchema, selectableDataPayloadSchema, selectableDataSchema, selectablePlanDataSchema, selectablePlanPayloadSchema, selectedElementSchema, transformPoint, transformRect, transformSelectableData, transformSelectedElement };
|
|
@@ -14,9 +14,13 @@ function DesignLibraryPreview() {
|
|
|
14
14
|
const ownsDefaultFont = useRef(false);
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
const handleMessage = (event) => {
|
|
17
|
-
if (!isAllowedEditorMessage(event))
|
|
17
|
+
if (!isAllowedEditorMessage(event)) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
18
20
|
const parsed = designLibraryPreviewMessageSchema.safeParse(event.data);
|
|
19
|
-
if (!parsed.success)
|
|
21
|
+
if (!parsed.success) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
20
24
|
const { cssVars, fonts, defaultFontCssVar } = parsed.data;
|
|
21
25
|
const root = document.documentElement;
|
|
22
26
|
const nextVars = { ...cssVars };
|
|
@@ -25,7 +29,9 @@ function DesignLibraryPreview() {
|
|
|
25
29
|
}
|
|
26
30
|
const owned = ownedKeys.current ?? /* @__PURE__ */ new Set();
|
|
27
31
|
for (const key of owned) {
|
|
28
|
-
if (!(key in nextVars))
|
|
32
|
+
if (!(key in nextVars)) {
|
|
33
|
+
root.style.removeProperty(key);
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
ownedKeys.current = new Set(Object.keys(nextVars));
|
|
31
37
|
for (const [key, value] of Object.entries(nextVars)) {
|
|
@@ -44,7 +50,9 @@ function DesignLibraryPreview() {
|
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
for (const href of nextHrefs) {
|
|
47
|
-
if (links.has(href))
|
|
53
|
+
if (links.has(href)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
48
56
|
const link = document.createElement("link");
|
|
49
57
|
link.rel = "stylesheet";
|
|
50
58
|
link.href = href;
|
|
@@ -62,9 +70,13 @@ function DesignLibraryPreview() {
|
|
|
62
70
|
}
|
|
63
71
|
}
|
|
64
72
|
for (const font of customFonts) {
|
|
65
|
-
if (!font.faceUrl)
|
|
73
|
+
if (!font.faceUrl) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
66
76
|
const existing = faces.get(font.family);
|
|
67
|
-
if (existing?.url === font.faceUrl)
|
|
77
|
+
if (existing?.url === font.faceUrl) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
68
80
|
existing?.style.remove();
|
|
69
81
|
const style = document.createElement("style");
|
|
70
82
|
style.dataset[FONT_LINK_DATA_KEY] = "";
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sandbox-runtime",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.10.0",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-09-
|
|
8
|
+
"commit": "604d707e41cac581b43e134371e5e9a0f78fa2b7",
|
|
9
|
+
"builtAt": "2026-09-25T19:36:00.140Z"
|
|
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, {}),
|
|
@@ -18,6 +18,8 @@ function EditorSelection() {
|
|
|
18
18
|
const store = useSelectModeStore();
|
|
19
19
|
const displayMode = useDisplayMode();
|
|
20
20
|
const {
|
|
21
|
+
expandToSection,
|
|
22
|
+
setSectionPreview,
|
|
21
23
|
formatCommand,
|
|
22
24
|
postSelectedElementSnapshot,
|
|
23
25
|
revertInlineEdit,
|
|
@@ -38,7 +40,9 @@ function EditorSelection() {
|
|
|
38
40
|
const scheduleSnapshot = () => {
|
|
39
41
|
clearTimeout(refreshTimeout);
|
|
40
42
|
refreshTimeout = setTimeout(() => {
|
|
41
|
-
if (store.getState().editingElement)
|
|
43
|
+
if (store.getState().editingElement) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
42
46
|
if (postSelectedElementSnapshot()) {
|
|
43
47
|
clearTimeout(missingTimeout);
|
|
44
48
|
missingTimeout = void 0;
|
|
@@ -54,15 +58,26 @@ function EditorSelection() {
|
|
|
54
58
|
}, 250);
|
|
55
59
|
};
|
|
56
60
|
const observer = new MutationObserver((records) => {
|
|
57
|
-
const
|
|
58
|
-
|
|
61
|
+
const {
|
|
62
|
+
actions,
|
|
63
|
+
editingElement,
|
|
64
|
+
interactionSession: session,
|
|
65
|
+
sectionPreviewElement
|
|
66
|
+
} = store.getState();
|
|
67
|
+
if (sectionPreviewElement && !sectionPreviewElement.isConnected) {
|
|
68
|
+
actions.setSectionPreview({ targetSelector: null });
|
|
69
|
+
}
|
|
70
|
+
if (editingElement || session.type !== "element") {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
59
73
|
if (!session.element.isConnected || records.some((record) => session.element.contains(record.target))) {
|
|
60
74
|
scheduleSnapshot();
|
|
61
75
|
}
|
|
62
76
|
});
|
|
63
77
|
const watchRender = () => {
|
|
64
|
-
if (store.getState().editingElement || store.getState().interactionSession.type !== "element")
|
|
78
|
+
if (store.getState().editingElement || store.getState().interactionSession.type !== "element") {
|
|
65
79
|
return;
|
|
80
|
+
}
|
|
66
81
|
clearTimeout(observationTimeout);
|
|
67
82
|
observer.observe(document.body, {
|
|
68
83
|
subtree: true,
|
|
@@ -118,6 +133,8 @@ function EditorSelection() {
|
|
|
118
133
|
useParentMessaging({
|
|
119
134
|
applyOptimisticUpdate,
|
|
120
135
|
markCommitted,
|
|
136
|
+
onExpandToSection: expandToSection,
|
|
137
|
+
onSetSectionPreview: setSectionPreview,
|
|
121
138
|
onExitEditMode: revertInlineEdit,
|
|
122
139
|
onFormatCommand: formatCommand,
|
|
123
140
|
resolveSessionOverrides,
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
+
import { getDocumentScrollOffset } from "./interactions/selectionGeometry";
|
|
4
5
|
import {
|
|
6
|
+
SECTION_SELECTION_FRAME_BACKGROUND,
|
|
7
|
+
SECTION_SELECTION_FRAME_BORDER_HEX,
|
|
5
8
|
SELECTION_FRAME_BORDER_HEX,
|
|
6
9
|
SELECTION_FRAME_EDITING_BORDER_HEX,
|
|
7
10
|
SELECTION_FRAMES_CONTAINER_ID
|
|
8
11
|
} from "./selectionFrame";
|
|
9
12
|
import {
|
|
10
13
|
useIsSelectionFrameEditing,
|
|
14
|
+
useSectionHoverRect,
|
|
11
15
|
useSelectionFrames
|
|
12
16
|
} from "./selectModeStore";
|
|
13
17
|
const CONTAINER_STYLE = {
|
|
@@ -24,32 +28,61 @@ const CONTAINER_STYLE = {
|
|
|
24
28
|
function SelectionFrames() {
|
|
25
29
|
const frames = useSelectionFrames();
|
|
26
30
|
const isEditing = useIsSelectionFrameEditing();
|
|
27
|
-
|
|
31
|
+
const sectionHoverRect = useSectionHoverRect();
|
|
32
|
+
if (typeof document === "undefined" || !document.body || frames.length === 0 && !sectionHoverRect) {
|
|
28
33
|
return null;
|
|
29
34
|
}
|
|
30
|
-
const
|
|
35
|
+
const elementBorderColor = isEditing ? SELECTION_FRAME_EDITING_BORDER_HEX : SELECTION_FRAME_BORDER_HEX;
|
|
36
|
+
const scrollOffset = getDocumentScrollOffset();
|
|
37
|
+
const sectionLabelPosition = sectionHoverRect ? {
|
|
38
|
+
x: sectionHoverRect.x + scrollOffset.left,
|
|
39
|
+
y: sectionHoverRect.y + scrollOffset.top
|
|
40
|
+
} : frames.find((frame) => frame.variant === "section")?.pageRect;
|
|
31
41
|
return createPortal(
|
|
32
|
-
/* @__PURE__ */
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
/* @__PURE__ */ jsxs("div", { id: SELECTION_FRAMES_CONTAINER_ID, "aria-hidden": true, style: CONTAINER_STYLE, children: [
|
|
43
|
+
frames.map((frame, index) => /* @__PURE__ */ jsx(
|
|
44
|
+
"div",
|
|
45
|
+
{
|
|
46
|
+
"aria-hidden": true,
|
|
47
|
+
"data-variant": frame.variant,
|
|
48
|
+
style: {
|
|
49
|
+
position: "absolute",
|
|
50
|
+
boxSizing: "border-box",
|
|
51
|
+
borderRadius: 2,
|
|
52
|
+
pointerEvents: "none",
|
|
53
|
+
margin: 0,
|
|
54
|
+
padding: 0,
|
|
55
|
+
left: frame.pageRect.x,
|
|
56
|
+
top: frame.pageRect.y,
|
|
57
|
+
width: frame.pageRect.width,
|
|
58
|
+
height: frame.pageRect.height,
|
|
59
|
+
border: `2px solid ${frame.variant === "section" ? SECTION_SELECTION_FRAME_BORDER_HEX : elementBorderColor}`,
|
|
60
|
+
background: {
|
|
61
|
+
area: "rgba(192, 38, 211, 0.08)",
|
|
62
|
+
element: "transparent",
|
|
63
|
+
section: SECTION_SELECTION_FRAME_BACKGROUND
|
|
64
|
+
}[frame.variant]
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
`${frame.variant}-${index}`
|
|
68
|
+
)),
|
|
69
|
+
sectionLabelPosition && /* @__PURE__ */ jsx(
|
|
70
|
+
"span",
|
|
71
|
+
{
|
|
72
|
+
style: {
|
|
73
|
+
position: "absolute",
|
|
74
|
+
top: sectionLabelPosition.y + 6,
|
|
75
|
+
left: sectionLabelPosition.x + 6,
|
|
76
|
+
padding: "3px 6px",
|
|
77
|
+
borderRadius: 4,
|
|
78
|
+
background: "#2563eb",
|
|
79
|
+
color: "white",
|
|
80
|
+
font: "500 12px/16px system-ui"
|
|
81
|
+
},
|
|
82
|
+
children: "Section"
|
|
49
83
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
)) }),
|
|
84
|
+
)
|
|
85
|
+
] }),
|
|
53
86
|
document.body
|
|
54
87
|
);
|
|
55
88
|
}
|
|
@@ -17,8 +17,9 @@ export declare function buildSelector(element: Element): string;
|
|
|
17
17
|
* about a selection: identification, geometry, source location, and a
|
|
18
18
|
* style-inlined HTML snippet.
|
|
19
19
|
*/
|
|
20
|
-
export declare function buildElementDescriptor({ element, clickPosition, siblingSelectors, }: {
|
|
20
|
+
export declare function buildElementDescriptor({ element, clickPosition, siblingSelectors, section, }: {
|
|
21
21
|
element: Element;
|
|
22
22
|
clickPosition: SelectModeClickPosition;
|
|
23
23
|
siblingSelectors?: string[];
|
|
24
|
+
section?: Element | null;
|
|
24
25
|
}): SelectableData;
|