@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.
Files changed (33) hide show
  1. package/PageCaptureResponder.d.ts +1 -0
  2. package/PageCaptureResponder.js +12 -0
  3. package/_vendor/replo-utils/lib/crypto.mjs +1 -0
  4. package/_vendor/replo-utils/lib/math.mjs +1 -1
  5. package/_vendor/replo-utils/lib/pageCapture.d.ts +34 -0
  6. package/_vendor/replo-utils/lib/pageCapture.mjs +52 -0
  7. package/_vendor/schemas/errors.d.ts +1 -0
  8. package/_vendor/schemas/generated/selectMode.d.ts +25 -0
  9. package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
  10. package/_vendor/schemas/selectMode.d.ts +101 -2
  11. package/_vendor/schemas/selectMode.mjs +1 -1
  12. package/_vendor/schemas/selectModeIframeRpc.d.ts +148 -4
  13. package/_vendor/schemas/selectModeIframeRpc.mjs +1 -1
  14. package/_vendor/schemas/selectModeImage.d.ts +9 -0
  15. package/_vendor/schemas/selectModeImage.mjs +24 -0
  16. package/buildMetadata.js +3 -3
  17. package/capturePage.d.ts +11 -0
  18. package/capturePage.js +261 -0
  19. package/chunk-LIPH4O4F.mjs +67 -0
  20. package/{chunk-4T7WSMC3.mjs → chunk-XWNBKE63.mjs} +25 -1
  21. package/package.json +5 -4
  22. package/sandbox-runtime.js +2 -0
  23. package/select-mode/descriptor/elementDescriptor.js +2 -0
  24. package/select-mode/hooks/useOptimisticOverrides.js +63 -10
  25. package/select-mode/hooks/useSelectionInteractions.js +1 -10
  26. package/select-mode/iframeRpcChannel.d.ts +42 -0
  27. package/select-mode/imageOverridePreview.d.ts +6 -0
  28. package/select-mode/imageOverridePreview.js +77 -0
  29. package/select-mode/imageSources.d.ts +3 -0
  30. package/select-mode/imageSources.js +68 -0
  31. package/startPageCaptureResponder.d.ts +32 -0
  32. package/startPageCaptureResponder.js +87 -0
  33. 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.7.13",
3
+ "version": "1.9.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.7.13",
6
+ "version": "1.9.0",
7
7
  "branch": "HEAD",
8
- "commit": "48e7e55752c4448369b7a49ce608c6539acb3a24",
9
- "builtAt": "2026-09-23T03:50:04.538Z"
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"
@@ -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 = isImageSourcePayload({ element, payload }) ? [payload.name, "srcset"] : [payload.name];
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 (!dragStart || snapshot.type !== "drag") {
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<{
@@ -0,0 +1,6 @@
1
+ import type { SelectModeImage } from "../_vendor/schemas/generated/selectMode";
2
+ export declare function createImagePreview(image: HTMLImageElement): {
3
+ render(config: SelectModeImage): void;
4
+ isPersisted(config: SelectModeImage): boolean;
5
+ dispose(): void;
6
+ };