@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.
Files changed (47) 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/fonts.mjs +18 -6
  5. package/_vendor/replo-utils/lib/math.mjs +1 -1
  6. package/_vendor/replo-utils/lib/pageCapture.d.ts +34 -0
  7. package/_vendor/replo-utils/lib/pageCapture.mjs +52 -0
  8. package/_vendor/schemas/generated/selectMode.d.ts +14 -0
  9. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
  10. package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
  11. package/_vendor/schemas/selectMode.d.ts +26 -2
  12. package/_vendor/schemas/selectMode.mjs +1 -1
  13. package/_vendor/schemas/selectModeIframeRpc.d.ts +53 -4
  14. package/_vendor/schemas/selectModeIframeRpc.mjs +18 -2
  15. package/_vendor/schemas/selectModeImage.mjs +6 -2
  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-XWNBKE63.mjs → chunk-PJBGUXBG.mjs} +6 -1
  21. package/design-library-preview/DesignLibraryPreview.js +18 -6
  22. package/package.json +5 -4
  23. package/sandbox-runtime.js +2 -0
  24. package/select-mode/EditorSelection.js +21 -4
  25. package/select-mode/SelectionFrames.js +56 -23
  26. package/select-mode/descriptor/elementDescriptor.d.ts +2 -1
  27. package/select-mode/descriptor/elementDescriptor.js +22 -14
  28. package/select-mode/hooks/useOptimisticOverrides.js +18 -6
  29. package/select-mode/hooks/useParentMessaging.d.ts +7 -1
  30. package/select-mode/hooks/useParentMessaging.js +6 -0
  31. package/select-mode/hooks/useSelectionHover.js +38 -15
  32. package/select-mode/hooks/useSelectionInteractions.js +9 -37
  33. package/select-mode/hooks/useSelectionScrollSync.js +5 -1
  34. package/select-mode/iframeRpcChannel.d.ts +10 -0
  35. package/select-mode/iframeRpcChannel.js +2 -0
  36. package/select-mode/imageOverridePreview.js +14 -5
  37. package/select-mode/imageSources.js +2 -1
  38. package/select-mode/inlineEditFeedback.js +9 -3
  39. package/select-mode/interactions/sectionGeometry.d.ts +3 -0
  40. package/select-mode/interactions/sectionGeometry.js +200 -0
  41. package/select-mode/selectModeStore.d.ts +36 -8
  42. package/select-mode/selectModeStore.js +141 -21
  43. package/select-mode/selectionFrame.d.ts +3 -1
  44. package/select-mode/selectionFrame.js +4 -0
  45. package/startPageCaptureResponder.d.ts +32 -0
  46. package/startPageCaptureResponder.js +87 -0
  47. 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)) return;
17
+ if (!isAllowedEditorMessage(event)) {
18
+ return;
19
+ }
18
20
  const parsed = designLibraryPreviewMessageSchema.safeParse(event.data);
19
- if (!parsed.success) return;
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)) root.style.removeProperty(key);
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)) continue;
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) continue;
73
+ if (!font.faceUrl) {
74
+ continue;
75
+ }
66
76
  const existing = faces.get(font.family);
67
- if (existing?.url === font.faceUrl) continue;
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.8.0",
3
+ "version": "1.10.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.8.0",
6
+ "version": "1.10.0",
7
7
  "branch": "HEAD",
8
- "commit": "6ad933540e110b76d214b8700e7a356a551c869a",
9
- "builtAt": "2026-09-24T18:57:19.451Z"
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"
@@ -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) return;
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 session = store.getState().interactionSession;
58
- if (store.getState().editingElement || session.type !== "element") return;
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
- if (typeof document === "undefined" || !document.body || frames.length === 0) {
31
+ const sectionHoverRect = useSectionHoverRect();
32
+ if (typeof document === "undefined" || !document.body || frames.length === 0 && !sectionHoverRect) {
28
33
  return null;
29
34
  }
30
- const borderColor = isEditing ? SELECTION_FRAME_EDITING_BORDER_HEX : SELECTION_FRAME_BORDER_HEX;
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__ */ jsx("div", { id: SELECTION_FRAMES_CONTAINER_ID, "aria-hidden": true, style: CONTAINER_STYLE, children: frames.map((frame, index) => /* @__PURE__ */ jsx(
33
- "div",
34
- {
35
- "aria-hidden": true,
36
- style: {
37
- position: "absolute",
38
- boxSizing: "border-box",
39
- borderRadius: 2,
40
- pointerEvents: "none",
41
- margin: 0,
42
- padding: 0,
43
- left: frame.pageRect.x,
44
- top: frame.pageRect.y,
45
- width: frame.pageRect.width,
46
- height: frame.pageRect.height,
47
- border: `2px solid ${borderColor}`,
48
- background: frame.variant === "area" ? "rgba(192, 38, 211, 0.08)" : "transparent"
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
- `${frame.variant}-${index}`
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;