@replohq/sandbox-runtime 1.9.0 → 1.10.1

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/_vendor/replo-utils/lib/fonts.mjs +18 -6
  2. package/_vendor/schemas/generated/selectMode.d.ts +15 -0
  3. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
  4. package/_vendor/schemas/selectMode.d.ts +28 -0
  5. package/_vendor/schemas/selectMode.mjs +1 -1
  6. package/_vendor/schemas/selectModeIframeRpc.d.ts +57 -0
  7. package/_vendor/schemas/selectModeIframeRpc.mjs +18 -2
  8. package/_vendor/schemas/selectModeImage.mjs +6 -2
  9. package/buildMetadata.js +3 -3
  10. package/{chunk-XWNBKE63.mjs → chunk-NBVMONRN.mjs} +7 -1
  11. package/design-library-preview/DesignLibraryPreview.js +18 -6
  12. package/package.json +4 -4
  13. package/select-mode/EditorSelection.js +21 -4
  14. package/select-mode/SelectionFrames.js +56 -23
  15. package/select-mode/descriptor/elementDescriptor.d.ts +2 -1
  16. package/select-mode/descriptor/elementDescriptor.js +61 -14
  17. package/select-mode/hooks/useOptimisticOverrides.js +18 -6
  18. package/select-mode/hooks/useParentMessaging.d.ts +7 -1
  19. package/select-mode/hooks/useParentMessaging.js +6 -0
  20. package/select-mode/hooks/useSelectionHover.js +38 -15
  21. package/select-mode/hooks/useSelectionInteractions.js +11 -37
  22. package/select-mode/hooks/useSelectionScrollSync.js +5 -1
  23. package/select-mode/iframeRpcChannel.d.ts +13 -0
  24. package/select-mode/iframeRpcChannel.js +2 -0
  25. package/select-mode/imageOverridePreview.js +14 -5
  26. package/select-mode/imageSources.js +2 -1
  27. package/select-mode/inlineEditFeedback.js +9 -3
  28. package/select-mode/interactions/sectionGeometry.d.ts +3 -0
  29. package/select-mode/interactions/sectionGeometry.js +200 -0
  30. package/select-mode/selectModeStore.d.ts +36 -8
  31. package/select-mode/selectModeStore.js +141 -21
  32. package/select-mode/selectionFrame.d.ts +3 -1
  33. package/select-mode/selectionFrame.js +4 -0
@@ -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;
@@ -8,6 +8,43 @@ import { getImageDescriptor } from "../imageSources";
8
8
  import { skipInlineTextMarkElements } from "../inlineTextMarks";
9
9
  import { getElementBoundingRect } from "../interactions/selectionGeometry";
10
10
  import { getDefinedStyles, getInheritedStyles } from "./styleExtraction";
11
+ const SECTION_SELECTOR = 'section, article, aside, [role="region"]';
12
+ const SECTION_LABEL_LENGTH = 60;
13
+ function normalizeSectionLabel(value) {
14
+ return value?.replace(/\s+/g, " ").trim().slice(0, SECTION_LABEL_LENGTH) || void 0;
15
+ }
16
+ function findSectionLabel(section) {
17
+ if (!section) {
18
+ return void 0;
19
+ }
20
+ const ariaLabel = normalizeSectionLabel(section.getAttribute("aria-label"));
21
+ if (ariaLabel) {
22
+ return ariaLabel;
23
+ }
24
+ const labelledBy = section.getAttribute("aria-labelledby");
25
+ const labelledByText = normalizeSectionLabel(
26
+ labelledBy?.split(/\s+/).map((id) => section.ownerDocument.getElementById(id)?.textContent).filter(Boolean).join(" ")
27
+ );
28
+ if (labelledByText) {
29
+ return labelledByText;
30
+ }
31
+ const headings = Array.from(
32
+ section.querySelectorAll("h1, h2, h3, h4, h5, h6")
33
+ ).filter((candidate) => {
34
+ if (candidate.checkVisibility?.({ visibilityProperty: true }) === false) {
35
+ return false;
36
+ }
37
+ const semanticSection = candidate.closest(SECTION_SELECTOR);
38
+ return !semanticSection || semanticSection === section || !section.contains(semanticSection);
39
+ });
40
+ const highestHeadingLevel = Math.min(
41
+ ...headings.map((heading2) => Number(heading2.tagName[1]))
42
+ );
43
+ const [heading, competingHeading] = headings.filter(
44
+ (candidate) => Number(candidate.tagName[1]) === highestHeadingLevel
45
+ );
46
+ return competingHeading ? void 0 : normalizeSectionLabel(heading ? getRenderedText(heading) : void 0);
47
+ }
11
48
  function getNearestSelectableElement(element) {
12
49
  const host = skipInlineTextMarkElements(element);
13
50
  return host.closest(`[${SELECT_MODE_SELECTABLE_ATTRIBUTE}]`) ?? host;
@@ -30,12 +67,20 @@ function getSelectableDataFromElement(element) {
30
67
  rect: getElementBoundingRect(selectableElement)
31
68
  };
32
69
  }
33
- function identifyElement(element) {
70
+ function getRenderedText(element) {
71
+ if (element instanceof HTMLElement) {
72
+ return (element.innerText ?? element.textContent)?.trim() ?? null;
73
+ }
74
+ return element.textContent?.trim() ?? null;
75
+ }
76
+ function identifyElement({
77
+ element,
78
+ trimmedText
79
+ }) {
34
80
  const tagName = element.tagName.toLowerCase();
35
81
  if (tagName === "button" || element.getAttribute("role") === "button" || tagName === "input") {
36
- const trimmedText2 = element.textContent?.trim();
37
- if (trimmedText2) {
38
- return `${tagName}: "${trimmedText2.slice(0, 60)}"`;
82
+ if (trimmedText) {
83
+ return `${tagName}: "${trimmedText.slice(0, 60)}"`;
39
84
  }
40
85
  const ariaLabel = element.getAttribute("aria-label");
41
86
  if (ariaLabel) {
@@ -49,9 +94,8 @@ function identifyElement(element) {
49
94
  return tagName;
50
95
  }
51
96
  if (tagName === "a") {
52
- const trimmedText2 = element.textContent?.trim();
53
- if (trimmedText2) {
54
- return `link: "${trimmedText2.slice(0, 60)}"`;
97
+ if (trimmedText) {
98
+ return `link: "${trimmedText.slice(0, 60)}"`;
55
99
  }
56
100
  const href = element.getAttribute("href");
57
101
  if (href) {
@@ -72,8 +116,7 @@ function identifyElement(element) {
72
116
  return "image";
73
117
  }
74
118
  if (/^h[1-6]$/.test(tagName)) {
75
- const trimmedText2 = element.textContent?.trim();
76
- return trimmedText2 ? `heading(${tagName}): "${trimmedText2.slice(0, 60)}"` : tagName;
119
+ return trimmedText ? `heading(${tagName}): "${trimmedText.slice(0, 60)}"` : tagName;
77
120
  }
78
121
  if (["div", "main", "section", "article", "header", "footer", "nav"].includes(
79
122
  tagName
@@ -83,7 +126,6 @@ function identifyElement(element) {
83
126
  return role2;
84
127
  }
85
128
  }
86
- const trimmedText = element.textContent?.trim();
87
129
  if (trimmedText && trimmedText.length < 120) {
88
130
  const role2 = element.getAttribute("role");
89
131
  const label = role2 ?? tagName;
@@ -168,7 +210,8 @@ function getTextMetrics(element) {
168
210
  function buildElementDescriptor({
169
211
  element,
170
212
  clickPosition,
171
- siblingSelectors = []
213
+ siblingSelectors = [],
214
+ section
172
215
  }) {
173
216
  const selectableData = getSelectableDataFromElement(element);
174
217
  if (selectableData) {
@@ -177,12 +220,15 @@ function buildElementDescriptor({
177
220
  const textMetrics = getTextMetrics(element);
178
221
  const fiberChain = element instanceof HTMLElement ? getFiberChain(element) : [];
179
222
  const sourceLocation = fiberChain[0]?.sourceLocation;
223
+ const trimmedText = getRenderedText(element);
224
+ const sectionLabel = findSectionLabel(section);
180
225
  return {
181
226
  kind: "react-element",
182
227
  selector: buildSelector(element),
183
228
  tagName: element.tagName.toLowerCase(),
184
- elementType: identifyElement(element),
185
- textContent: element.textContent?.trim()?.slice(0, 200) ?? null,
229
+ elementType: identifyElement({ element, trimmedText }),
230
+ textContent: trimmedText?.slice(0, 200) ?? null,
231
+ ...sectionLabel ? { sectionLabel } : {},
186
232
  rect: getElementBoundingRect(element),
187
233
  textMetrics,
188
234
  outerHTML: element.outerHTML.slice(0, 4e3),
@@ -192,7 +238,8 @@ function buildElementDescriptor({
192
238
  clickPosition,
193
239
  sourceLocation,
194
240
  fiberChain,
195
- siblingSelectors
241
+ siblingSelectors,
242
+ ...section ? { section: { selector: buildSelector(section) } } : {}
196
243
  };
197
244
  }
198
245
  export {
@@ -91,7 +91,9 @@ function applyPayload({
91
91
  payload,
92
92
  token
93
93
  }) {
94
- if (payload.kind === "image") return;
94
+ if (payload.kind === "image") {
95
+ return;
96
+ }
95
97
  if (payload.kind === "css") {
96
98
  addToken(element, token);
97
99
  return;
@@ -201,7 +203,9 @@ function reapplyOverride({
201
203
  override,
202
204
  skipElement
203
205
  }) {
204
- if (override.payload.kind === "image") return;
206
+ if (override.payload.kind === "image") {
207
+ return;
208
+ }
205
209
  for (const target of override.targets) {
206
210
  const element = resolveTarget(target);
207
211
  if (!element || skipElement && element.contains(skipElement)) {
@@ -220,10 +224,14 @@ function reapplyOverride({
220
224
  function syncImagePreviews() {
221
225
  const configurations = /* @__PURE__ */ new Map();
222
226
  for (const override of getOverrideList()) {
223
- if (override.payload.kind !== "image") continue;
227
+ if (override.payload.kind !== "image") {
228
+ continue;
229
+ }
224
230
  for (const target of override.targets) {
225
231
  const element = resolveTarget(target);
226
- if (!(element instanceof HTMLImageElement)) continue;
232
+ if (!(element instanceof HTMLImageElement)) {
233
+ continue;
234
+ }
227
235
  const previous = configurations.get(element);
228
236
  configurations.set(element, {
229
237
  image: override.payload.image,
@@ -232,7 +240,9 @@ function syncImagePreviews() {
232
240
  }
233
241
  }
234
242
  for (const [element, preview] of imagePreviews) {
235
- if (configurations.has(element)) continue;
243
+ if (configurations.has(element)) {
244
+ continue;
245
+ }
236
246
  preview.dispose();
237
247
  imagePreviews.delete(element);
238
248
  }
@@ -253,7 +263,9 @@ function syncImagePreviews() {
253
263
  }
254
264
  setOverrideList(
255
265
  getOverrideList().filter((entry) => {
256
- if (entry.payload.kind !== "image") return true;
266
+ if (entry.payload.kind !== "image") {
267
+ return true;
268
+ }
257
269
  entry.targets = entry.targets.filter((target) => {
258
270
  const element = resolveTarget(target);
259
271
  return !element || !acknowledged.has(element);
@@ -16,7 +16,7 @@ import type { DisplayMode, SelectModeFormatCommand, SelectModeOptimisticUpdatePa
16
16
  * the initial `setDisplayMode` — without this handshake the parent would
17
17
  * race the iframe's mount and the first message could be lost.
18
18
  */
19
- export declare function useParentMessaging({ applyOptimisticUpdate, markCommitted, resolveSessionOverrides, onEnterEditMode, onExitEditMode, onFormatCommand, revertOverride, setDisplayMode, setSelectionType, }: {
19
+ export declare function useParentMessaging({ applyOptimisticUpdate, markCommitted, resolveSessionOverrides, onExpandToSection, onSetSectionPreview, onEnterEditMode, onExitEditMode, onFormatCommand, revertOverride, setDisplayMode, setSelectionType, }: {
20
20
  applyOptimisticUpdate?: (input: {
21
21
  editId: string;
22
22
  payload: SelectModeOptimisticUpdatePayload;
@@ -25,6 +25,12 @@ export declare function useParentMessaging({ applyOptimisticUpdate, markCommitte
25
25
  }) => void;
26
26
  markCommitted?: (editId: string, sessionId: string) => void;
27
27
  resolveSessionOverrides?: (sessionId: string, outcome: "persisted" | "failed") => void;
28
+ onExpandToSection?: (input: {
29
+ targetSelector: string;
30
+ }) => void;
31
+ onSetSectionPreview?: (input: {
32
+ targetSelector: string | null;
33
+ }) => void;
28
34
  onEnterEditMode?: () => void;
29
35
  onExitEditMode?: () => void;
30
36
  onFormatCommand?: (command: SelectModeFormatCommand) => void;
@@ -27,6 +27,8 @@ function useParentMessaging({
27
27
  applyOptimisticUpdate,
28
28
  markCommitted,
29
29
  resolveSessionOverrides,
30
+ onExpandToSection,
31
+ onSetSectionPreview,
30
32
  onEnterEditMode,
31
33
  onExitEditMode,
32
34
  onFormatCommand,
@@ -37,6 +39,8 @@ function useParentMessaging({
37
39
  useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));
38
40
  useEffect(() => {
39
41
  setSelectModeParentHandlers({
42
+ expandToSection: (input) => onExpandToSection?.(input),
43
+ setSectionPreview: (input) => onSetSectionPreview?.(input),
40
44
  setDisplayMode: ({ displayMode }) => {
41
45
  setDisplayMode(displayMode);
42
46
  },
@@ -83,6 +87,8 @@ function useParentMessaging({
83
87
  applyOptimisticUpdate,
84
88
  markCommitted,
85
89
  resolveSessionOverrides,
90
+ onExpandToSection,
91
+ onSetSectionPreview,
86
92
  onEnterEditMode,
87
93
  onExitEditMode,
88
94
  onFormatCommand,
@@ -1,6 +1,10 @@
1
1
  import { useEffect } from "react";
2
2
  import { getNearestSelectableElement } from "../descriptor/elementDescriptor";
3
3
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
4
+ import {
5
+ CONTAINER_TAGS,
6
+ findContainingSection
7
+ } from "../interactions/sectionGeometry";
4
8
  import { getElementBoundingRect } from "../interactions/selectionGeometry";
5
9
  import { useDisplayMode, useSelectModeStore } from "../selectModeStore";
6
10
  function useSelectionHover() {
@@ -11,16 +15,30 @@ function useSelectionHover() {
11
15
  return;
12
16
  }
13
17
  const postHoveredElement = (element) => {
14
- const { lastPostedHoverRect, actions } = store.getState();
18
+ const { lastPostedHover, actions } = store.getState();
19
+ const lastPostedHoverRect = lastPostedHover?.rect;
15
20
  const rect = element ? getElementBoundingRect(element) : null;
16
- const isUnchanged = lastPostedHoverRect !== void 0 && (rect === lastPostedHoverRect || rect !== null && lastPostedHoverRect !== null && rect.x === lastPostedHoverRect.x && rect.y === lastPostedHoverRect.y && rect.width === lastPostedHoverRect.width && rect.height === lastPostedHoverRect.height);
21
+ const isUnchanged = lastPostedHover?.element === element && lastPostedHoverRect !== void 0 && (rect === lastPostedHoverRect || rect !== null && lastPostedHoverRect !== null && rect.x === lastPostedHoverRect.x && rect.y === lastPostedHoverRect.y && rect.width === lastPostedHoverRect.width && rect.height === lastPostedHoverRect.height);
17
22
  if (isUnchanged) {
18
23
  return;
19
24
  }
20
- actions.setLastPostedHoverRect(rect);
21
- getWebsiteBuilderRpc()?.hoverElement({ rect });
25
+ const hoverRect = rect && element ? {
26
+ ...rect,
27
+ isSection: element === store.getState().sectionPreviewElement || CONTAINER_TAGS.has(element.tagName) && findContainingSection(element) === element
28
+ } : null;
29
+ actions.setLastPostedHover({ element, rect: hoverRect });
30
+ getWebsiteBuilderRpc()?.hoverElement({ rect: hoverRect });
22
31
  };
23
32
  const syncCurrentHover = (pointerClientPosition = store.getState().lastPointerClientPosition) => {
33
+ const previewSection = store.getState().sectionPreviewElement;
34
+ if (previewSection) {
35
+ if (previewSection.isConnected) {
36
+ postHoveredElement(previewSection);
37
+ return;
38
+ }
39
+ store.getState().actions.setSectionPreview({ targetSelector: null });
40
+ return;
41
+ }
24
42
  let hoveredElement = null;
25
43
  if (pointerClientPosition) {
26
44
  const isPointerWithinViewport = pointerClientPosition.x >= 0 && pointerClientPosition.y >= 0 && pointerClientPosition.x <= window.innerWidth && pointerClientPosition.y <= window.innerHeight;
@@ -59,14 +77,6 @@ function useSelectionHover() {
59
77
  }
60
78
  syncCurrentHover(position);
61
79
  };
62
- const handlePointerOver = (event) => {
63
- const position = { x: event.clientX, y: event.clientY };
64
- store.getState().actions.setLastPointerClientPosition(position);
65
- if (store.getState().interactionSession.type === "drag") {
66
- return;
67
- }
68
- syncCurrentHover(position);
69
- };
70
80
  const handlePointerOut = (event) => {
71
81
  if (store.getState().interactionSession.type === "drag") {
72
82
  return;
@@ -81,11 +91,20 @@ function useSelectionHover() {
81
91
  cancelAnimationFrame(hoverSyncAnimationFrameId);
82
92
  actions.setHoverSyncAnimationFrameId(null);
83
93
  }
84
- postHoveredElement(null);
94
+ if (store.getState().sectionPreviewElement) {
95
+ syncCurrentHover(null);
96
+ } else {
97
+ postHoveredElement(null);
98
+ }
85
99
  };
100
+ const stopPreviewObservation = store.subscribe((state, previous) => {
101
+ if (state.sectionPreviewElement !== previous.sectionPreviewElement) {
102
+ syncCurrentHover();
103
+ }
104
+ });
86
105
  syncCurrentHover();
87
106
  document.addEventListener("pointermove", handlePointerMove, true);
88
- document.addEventListener("pointerover", handlePointerOver, true);
107
+ document.addEventListener("pointerover", handlePointerMove, true);
89
108
  document.addEventListener("pointerout", handlePointerOut, true);
90
109
  document.addEventListener("scroll", scheduleHoverSync, true);
91
110
  document.addEventListener("wheel", scheduleHoverSync, {
@@ -94,13 +113,17 @@ function useSelectionHover() {
94
113
  });
95
114
  window.addEventListener("resize", scheduleHoverSync);
96
115
  return () => {
116
+ stopPreviewObservation();
117
+ store.getState().actions.setSectionPreview({ targetSelector: null });
118
+ postHoveredElement(null);
119
+ store.getState().actions.setLastPostedHover(void 0);
97
120
  const { hoverSyncAnimationFrameId } = store.getState();
98
121
  if (hoverSyncAnimationFrameId !== null) {
99
122
  cancelAnimationFrame(hoverSyncAnimationFrameId);
100
123
  store.getState().actions.setHoverSyncAnimationFrameId(null);
101
124
  }
102
125
  document.removeEventListener("pointermove", handlePointerMove, true);
103
- document.removeEventListener("pointerover", handlePointerOver, true);
126
+ document.removeEventListener("pointerover", handlePointerMove, true);
104
127
  document.removeEventListener("pointerout", handlePointerOut, true);
105
128
  document.removeEventListener("scroll", scheduleHoverSync, true);
106
129
  document.removeEventListener("wheel", scheduleHoverSync, {
@@ -1,10 +1,10 @@
1
1
  import { useEffect } from "react";
2
2
  import {
3
3
  buildElementDescriptor,
4
- buildSelector,
5
4
  getNearestSelectableElement
6
5
  } from "../descriptor/elementDescriptor";
7
6
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
7
+ import { findContainingSection } from "../interactions/sectionGeometry";
8
8
  import {
9
9
  collectDragCandidates,
10
10
  getCandidatesInSelection,
@@ -27,7 +27,6 @@ import {
27
27
  } from "../selectModeSession";
28
28
  import { useDisplayMode, useSelectModeStore } from "../selectModeStore";
29
29
  import { symbolicateSelectedElement } from "../symbolicate";
30
- import { getRepetitionSiblingElements } from "./useSiblingResolution";
31
30
  const DOUBLE_CLICK_THRESHOLD_MS = 500;
32
31
  function getSelectedElementsFromSession(session) {
33
32
  if (session.type === "element") {
@@ -72,8 +71,8 @@ function useSelectionInteractions() {
72
71
  actions().hideSelectionFrames();
73
72
  };
74
73
  const showAreaSelectionFrames = actions().showAreaSelectionFrames;
75
- const showElementSelectionFrames = actions().showElementSelectionFrames;
76
74
  const editingPointerIds = store().editingPointerIds;
75
+ let committedDataAtPointerDown = null;
77
76
  const clearCommittedSelection = ({
78
77
  rect
79
78
  }) => {
@@ -186,6 +185,8 @@ function useSelectionInteractions() {
186
185
  event.preventDefault();
187
186
  event.stopPropagation();
188
187
  const scrollOffset = getDocumentScrollOffset();
188
+ const session = interactionSessionRef.current;
189
+ committedDataAtPointerDown = session.type === "element" ? session.data : null;
189
190
  actions().setCommittedSelectionAtPointerDown(
190
191
  getSelectedElementsFromSession(interactionSessionRef.current)
191
192
  );
@@ -242,6 +243,7 @@ function useSelectionInteractions() {
242
243
  element,
243
244
  data: buildElementDescriptor({
244
245
  element,
246
+ section: findContainingSection(element),
245
247
  clickPosition: getElementAnchor({
246
248
  element,
247
249
  selectionRect: anchorRect
@@ -277,7 +279,6 @@ function useSelectionInteractions() {
277
279
  selectionElement,
278
280
  pointerEvent
279
281
  }) => {
280
- const targetRect = selectionElement.getBoundingClientRect();
281
282
  const clickPosition = getElementAnchor({
282
283
  element: selectionElement,
283
284
  selectionRect: getPointRect({
@@ -285,12 +286,6 @@ function useSelectionInteractions() {
285
286
  y: pointerEvent.clientY
286
287
  })
287
288
  });
288
- const scrollOffset = getDocumentScrollOffset();
289
- const siblingElements = selectionElement instanceof HTMLElement ? getRepetitionSiblingElements({ element: selectionElement }) : [];
290
- showElementSelectionFrames({
291
- elements: [selectionElement, ...siblingElements],
292
- scrollOffset
293
- });
294
289
  const now = pointerEvent.timeStamp;
295
290
  const previousClick = store().lastClick;
296
291
  if (previousClick && previousClick.target === selectionElement && now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS && isTextEditableElement(selectionElement)) {
@@ -301,41 +296,20 @@ function useSelectionInteractions() {
301
296
  });
302
297
  return;
303
298
  }
304
- const data = buildElementDescriptor({
299
+ const data = actions().commitElementSelection({
305
300
  element: selectionElement,
306
301
  clickPosition,
307
- siblingSelectors: siblingElements.map(buildSelector)
302
+ isPendingClickOnSelection: (descriptor) => {
303
+ const session = interactionSessionRef.current;
304
+ const committedSelectionAtPointerDown = store().committedSelectionAtPointerDown;
305
+ return session.type === "drag" && !session.hasExceededThreshold && committedDataAtPointerDown === descriptor && committedSelectionAtPointerDown.length === 1 && committedSelectionAtPointerDown[0] === selectionElement;
306
+ }
308
307
  });
309
308
  actions().setLastClick({
310
309
  descriptorPromise: Promise.resolve(data),
311
310
  target: selectionElement,
312
311
  time: now
313
312
  });
314
- if (!selectionElement.isConnected) {
315
- return;
316
- }
317
- interactionSessionRef.current = {
318
- type: "element",
319
- element: selectionElement,
320
- siblingElements,
321
- clickPosition: {
322
- x: clickPosition.x - targetRect.left,
323
- y: clickPosition.y - targetRect.top
324
- },
325
- data
326
- };
327
- void maybeSymbolicate(data).then((symbolicated) => {
328
- const session = interactionSessionRef.current;
329
- const committedSelectionAtPointerDown = store().committedSelectionAtPointerDown;
330
- const isPendingClickOnSelection = session.type === "drag" && !session.hasExceededThreshold && committedSelectionAtPointerDown.length === 1 && committedSelectionAtPointerDown[0] === selectionElement;
331
- if (!isPendingClickOnSelection && !isSelectionStillCurrent({
332
- session: interactionSessionRef.current,
333
- selection: { type: "element", element: selectionElement }
334
- })) {
335
- return;
336
- }
337
- getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
338
- });
339
313
  };
340
314
  const commitClickSelection = ({
341
315
  targetElement,
@@ -1,4 +1,5 @@
1
1
  import { useEffect } from "react";
2
+ import { isSelectedSection } from "../../_vendor/schemas/selectMode.mjs";
2
3
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
3
4
  import {
4
5
  getDocumentScrollOffset,
@@ -37,7 +38,8 @@ function useSelectionScrollSync() {
37
38
  interactionSession.element,
38
39
  ...interactionSession.siblingElements
39
40
  ],
40
- scrollOffset
41
+ scrollOffset,
42
+ isSection: isSelectedSection(interactionSession.data)
41
43
  });
42
44
  getWebsiteBuilderRpc()?.selectedElementRectUpdate({
43
45
  rect: boundingRect,
@@ -57,8 +59,10 @@ function useSelectionScrollSync() {
57
59
  );
58
60
  };
59
61
  document.addEventListener("scroll", handleScroll, true);
62
+ window.addEventListener("resize", handleScroll);
60
63
  return () => {
61
64
  document.removeEventListener("scroll", handleScroll, true);
65
+ window.removeEventListener("resize", handleScroll);
62
66
  const { scrollSyncAnimationFrameId, actions } = store.getState();
63
67
  if (scrollSyncAnimationFrameId !== null) {
64
68
  cancelAnimationFrame(scrollSyncAnimationFrameId);
@@ -13,9 +13,13 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
13
13
  element: import("zod").ZodObject<{
14
14
  kind: import("zod").ZodLiteral<"react-element">;
15
15
  selector: import("zod").ZodString;
16
+ section: import("zod").ZodOptional<import("zod").ZodObject<{
17
+ selector: import("zod").ZodString;
18
+ }, import("zod/v4/core").$strip>>;
16
19
  tagName: import("zod").ZodString;
17
20
  elementType: import("zod").ZodString;
18
21
  textContent: import("zod").ZodNullable<import("zod").ZodString>;
22
+ sectionLabel: import("zod").ZodOptional<import("zod").ZodString>;
19
23
  rect: import("zod").ZodObject<{
20
24
  x: import("zod").ZodNumber;
21
25
  y: import("zod").ZodNumber;
@@ -104,9 +108,13 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
104
108
  data: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
105
109
  kind: import("zod").ZodLiteral<"react-element">;
106
110
  selector: import("zod").ZodString;
111
+ section: import("zod").ZodOptional<import("zod").ZodObject<{
112
+ selector: import("zod").ZodString;
113
+ }, import("zod/v4/core").$strip>>;
107
114
  tagName: import("zod").ZodString;
108
115
  elementType: import("zod").ZodString;
109
116
  textContent: import("zod").ZodNullable<import("zod").ZodString>;
117
+ sectionLabel: import("zod").ZodOptional<import("zod").ZodString>;
110
118
  rect: import("zod").ZodObject<{
111
119
  x: import("zod").ZodNumber;
112
120
  y: import("zod").ZodNumber;
@@ -202,6 +210,7 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
202
210
  bottom: import("zod").ZodNumber;
203
211
  width: import("zod").ZodNumber;
204
212
  height: import("zod").ZodNumber;
213
+ isSection: import("zod").ZodOptional<import("zod").ZodBoolean>;
205
214
  }, import("zod/v4/core").$strip>>;
206
215
  }, import("zod/v4/core").$strip>;
207
216
  outputSchema: import("zod").ZodVoid;
@@ -283,9 +292,13 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
283
292
  items: import("zod").ZodArray<import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
284
293
  kind: import("zod").ZodLiteral<"react-element">;
285
294
  selector: import("zod").ZodString;
295
+ section: import("zod").ZodOptional<import("zod").ZodObject<{
296
+ selector: import("zod").ZodString;
297
+ }, import("zod/v4/core").$strip>>;
286
298
  tagName: import("zod").ZodString;
287
299
  elementType: import("zod").ZodString;
288
300
  textContent: import("zod").ZodNullable<import("zod").ZodString>;
301
+ sectionLabel: import("zod").ZodOptional<import("zod").ZodString>;
289
302
  rect: import("zod").ZodObject<{
290
303
  x: import("zod").ZodNumber;
291
304
  y: import("zod").ZodNumber;
@@ -25,6 +25,8 @@ function createSelectModeIframeRpcChannel() {
25
25
  outgoingMethods: previewIframeToWebsiteBuilderMethodConfig,
26
26
  incomingMethods: websiteBuilderToPreviewIframeMethodConfig,
27
27
  handlers: {
28
+ expandToSection: (input) => messagingHandlers?.expandToSection(input),
29
+ setSectionPreview: (input) => messagingHandlers?.setSectionPreview(input),
28
30
  setDisplayMode: (input) => {
29
31
  messagingHandlers?.setDisplayMode(input);
30
32
  },