@replohq/sandbox-runtime 1.9.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 (33) hide show
  1. package/_vendor/replo-utils/lib/fonts.mjs +18 -6
  2. package/_vendor/schemas/generated/selectMode.d.ts +14 -0
  3. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
  4. package/_vendor/schemas/selectMode.d.ts +24 -0
  5. package/_vendor/schemas/selectMode.mjs +1 -1
  6. package/_vendor/schemas/selectModeIframeRpc.d.ts +49 -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-PJBGUXBG.mjs} +6 -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 +22 -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 +9 -37
  22. package/select-mode/hooks/useSelectionScrollSync.js +5 -1
  23. package/select-mode/iframeRpcChannel.d.ts +10 -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
@@ -0,0 +1,200 @@
1
+ import { SELECT_MODE_SELECTABLE_ATTRIBUTE } from "../../_vendor/schemas/selectMode.mjs";
2
+ const GROUP_TAGS = /* @__PURE__ */ new Set(["DIV", "ARTICLE", "ASIDE", "FORM"]);
3
+ const LANDMARK_TAGS = /* @__PURE__ */ new Set(["HEADER", "NAV", "FOOTER"]);
4
+ const SECTION_TAGS = /* @__PURE__ */ new Set(["SECTION", "ARTICLE", ...LANDMARK_TAGS]);
5
+ const CONTAINER_TAGS = /* @__PURE__ */ new Set([...GROUP_TAGS, ...SECTION_TAGS]);
6
+ function findContainingSection(element) {
7
+ const rects = /* @__PURE__ */ new Map();
8
+ const styles = /* @__PURE__ */ new Map();
9
+ const visibleChildren = /* @__PURE__ */ new Map();
10
+ if (element.closest(`[${SELECT_MODE_SELECTABLE_ATTRIBUTE}]`)) {
11
+ return null;
12
+ }
13
+ if (element.closest('dialog, [role="dialog"], [aria-modal="true"]')) {
14
+ return null;
15
+ }
16
+ for (let parent = element; parent; parent = parent.parentElement) {
17
+ const position = getStyle(parent).position;
18
+ if (position === "fixed" && !LANDMARK_TAGS.has(parent.tagName)) {
19
+ return null;
20
+ }
21
+ }
22
+ const lane = findContentLane();
23
+ if (!lane.contains(element)) {
24
+ return null;
25
+ }
26
+ const outerLaneRect = getRect(lane);
27
+ const laneStyle = getStyle(lane);
28
+ const paddingLeft = parseFloat(laneStyle.paddingLeft || "0");
29
+ const paddingRight = parseFloat(laneStyle.paddingRight || "0");
30
+ const laneRect = {
31
+ left: outerLaneRect.left + paddingLeft,
32
+ right: outerLaneRect.right - paddingRight,
33
+ width: outerLaneRect.width - paddingLeft - paddingRight
34
+ };
35
+ if (laneRect.width <= 0) {
36
+ return null;
37
+ }
38
+ const tolerance = Math.max(4, laneRect.width * 0.04);
39
+ const section = findOutermostSection();
40
+ return section ? trimSectionWrappers(section) : null;
41
+ function findContentLane() {
42
+ let lane2 = element.closest("main") ?? element.ownerDocument.body;
43
+ while (true) {
44
+ const children = getVisibleChildren(lane2);
45
+ const child = children[0];
46
+ if (children.length !== 1 || !child || child.tagName !== "DIV" || hasSectionStyling(child) && (getVisibleChildren(child).filter((descendant) => {
47
+ return CONTAINER_TAGS.has(descendant.tagName);
48
+ }).length < 2 || getRect(child).height < getRect(lane2).height * 0.9)) {
49
+ break;
50
+ }
51
+ lane2 = child;
52
+ }
53
+ return lane2;
54
+ }
55
+ function findOutermostSection() {
56
+ const candidates = [];
57
+ for (let candidate = element; candidate && candidate !== lane; candidate = candidate.parentElement) {
58
+ candidates.push(candidate);
59
+ }
60
+ for (const candidate of candidates.reverse()) {
61
+ if (!CONTAINER_TAGS.has(candidate.tagName) || !spansLane(candidate)) {
62
+ continue;
63
+ }
64
+ const style = getStyle(candidate);
65
+ if (style.display === "none" || style.display === "contents" || style.visibility === "hidden" || style.position === "absolute") {
66
+ continue;
67
+ }
68
+ const rect = getRect(candidate);
69
+ const minimumHeight = LANDMARK_TAGS.has(candidate.tagName) ? 20 : 48;
70
+ if (rect.height < minimumHeight || isMultiSectionWrapper(candidate)) {
71
+ continue;
72
+ }
73
+ if (SECTION_TAGS.has(candidate.tagName)) {
74
+ return candidate;
75
+ }
76
+ let parent = candidate.parentElement;
77
+ while (parent && getStyle(parent).display === "contents") {
78
+ parent = parent.parentElement;
79
+ }
80
+ const hasPeerBand = parent && getVisibleChildren(parent).some((peer) => {
81
+ if (peer === candidate || peer.contains(candidate) || !spansLane(peer)) {
82
+ return false;
83
+ }
84
+ const peerRect = getRect(peer);
85
+ return peerRect.height >= 20 && (peerRect.bottom <= rect.top + 4 || peerRect.top >= rect.bottom - 4);
86
+ });
87
+ if (hasPeerBand) {
88
+ return candidate;
89
+ }
90
+ }
91
+ return null;
92
+ }
93
+ function isMultiSectionWrapper(candidate) {
94
+ if (!GROUP_TAGS.has(candidate.tagName) || hasSectionStyling(candidate) || hasUngroupedText(candidate)) {
95
+ return false;
96
+ }
97
+ const children = getVisibleChildren(candidate);
98
+ const childBands = children.filter((child) => {
99
+ return CONTAINER_TAGS.has(child.tagName) && spansLane(child) && getRect(child).height >= 48;
100
+ });
101
+ const hasLeftoverContent = children.some((child) => {
102
+ return !CONTAINER_TAGS.has(child.tagName) && Boolean(child.textContent?.trim());
103
+ });
104
+ if (childBands.length <= 1 || hasLeftoverContent) {
105
+ return false;
106
+ }
107
+ let minimumBottom = Infinity;
108
+ let maximumTop = -Infinity;
109
+ for (const child of childBands) {
110
+ const childRect = getRect(child);
111
+ minimumBottom = Math.min(minimumBottom, childRect.bottom);
112
+ maximumTop = Math.max(maximumTop, childRect.top);
113
+ }
114
+ return minimumBottom <= maximumTop + 4;
115
+ }
116
+ function trimSectionWrappers(section2) {
117
+ while (!SECTION_TAGS.has(section2.tagName) && !hasSectionStyling(section2)) {
118
+ const children = getVisibleChildren(section2);
119
+ const child = children[0];
120
+ if (children.length !== 1 || !child || !child.contains(element) || !CONTAINER_TAGS.has(child.tagName)) {
121
+ break;
122
+ }
123
+ const outer = getRect(section2);
124
+ const inner = getRect(child);
125
+ if (Math.abs(outer.top - inner.top) > 4 || Math.abs(outer.bottom - inner.bottom) > 4 || !spansLane(child)) {
126
+ break;
127
+ }
128
+ section2 = child;
129
+ }
130
+ return section2;
131
+ }
132
+ function spansLane(candidate) {
133
+ const rect = getRect(candidate);
134
+ return Math.abs(rect.left - laneRect.left) <= tolerance && Math.abs(rect.right - laneRect.right) <= tolerance;
135
+ }
136
+ function getRect(element2) {
137
+ const cached = rects.get(element2);
138
+ if (cached) {
139
+ return cached;
140
+ }
141
+ const rect = element2.getBoundingClientRect();
142
+ rects.set(element2, rect);
143
+ return rect;
144
+ }
145
+ function getStyle(element2) {
146
+ const cached = styles.get(element2);
147
+ if (cached) {
148
+ return cached;
149
+ }
150
+ const style = getComputedStyle(element2);
151
+ styles.set(element2, style);
152
+ return style;
153
+ }
154
+ function getVisibleChildren(element2) {
155
+ const cached = visibleChildren.get(element2);
156
+ if (cached) {
157
+ return cached;
158
+ }
159
+ const children = Array.from(element2.children).flatMap((child) => {
160
+ const style = getStyle(child);
161
+ if (style.display === "none" || style.visibility === "hidden" || ["fixed", "absolute"].includes(style.position) && !LANDMARK_TAGS.has(child.tagName)) {
162
+ return [];
163
+ }
164
+ if (style.display === "contents") {
165
+ return getVisibleChildren(child);
166
+ }
167
+ const rect = getRect(child);
168
+ return rect.width > 0 && rect.height > 0 ? [child] : [];
169
+ });
170
+ visibleChildren.set(element2, children);
171
+ return children;
172
+ }
173
+ function hasUngroupedText(element2) {
174
+ return Array.from(element2.childNodes).some((node) => {
175
+ if (node.nodeType === Node.TEXT_NODE) {
176
+ return Boolean(node.textContent?.trim());
177
+ }
178
+ if (!(node instanceof Element)) {
179
+ return false;
180
+ }
181
+ const style = getStyle(node);
182
+ return style.display === "contents" && style.visibility !== "hidden" && hasUngroupedText(node);
183
+ });
184
+ }
185
+ function hasSectionStyling(element2) {
186
+ const style = getStyle(element2);
187
+ return style.backgroundColor !== "rgba(0, 0, 0, 0)" && style.backgroundColor !== "transparent" && style.backgroundColor !== "" || style.backgroundImage !== "none" && style.backgroundImage !== "" || [
188
+ style.paddingTop,
189
+ style.paddingRight,
190
+ style.paddingBottom,
191
+ style.paddingLeft,
192
+ style.borderTopWidth,
193
+ style.borderBottomWidth
194
+ ].some((value) => parseFloat(value) > 0);
195
+ }
196
+ }
197
+ export {
198
+ CONTAINER_TAGS,
199
+ findContainingSection
200
+ };
@@ -1,4 +1,4 @@
1
- import type { DisplayMode, SelectableData, SelectModeBoundingRect, SelectModeFormatCommand, SelectModeSelectionType, SelectModeTiptapActiveMarks } from "../_vendor/schemas/generated/selectMode";
1
+ import type { DisplayMode, SelectableData, SelectModeClickPosition, SelectModeFormatCommand, SelectModeHoverRect, SelectModeSelectionType, SelectModeTiptapActiveMarks } from "../_vendor/schemas/generated/selectMode";
2
2
  import type { InlineEditFeedback } from "../_vendor/schemas/generated/selectModeIframeRpc";
3
3
  import type { DragCandidate } from "./interactions/selectionGeometry";
4
4
  import type { SelectionFrameSpec } from "./selectionFrame";
@@ -22,6 +22,10 @@ type LastClick = {
22
22
  target: Element;
23
23
  time: number;
24
24
  };
25
+ type PostedHover = {
26
+ element: Element | null;
27
+ rect: SelectModeHoverRect | null;
28
+ };
25
29
  interface SelectModeStoreState {
26
30
  displayMode: DisplayMode;
27
31
  interactionSession: SelectModeInteractionSession;
@@ -31,7 +35,8 @@ interface SelectModeStoreState {
31
35
  x: number;
32
36
  y: number;
33
37
  } | null;
34
- lastPostedHoverRect: SelectModeBoundingRect | null | undefined;
38
+ lastPostedHover: PostedHover | undefined;
39
+ sectionPreviewElement: Element | null;
35
40
  hoverSyncAnimationFrameId: number | null;
36
41
  lastClick: LastClick | null;
37
42
  dragCandidates: DragCandidate[];
@@ -60,8 +65,20 @@ interface SelectModeStoreState {
60
65
  postSelectedElementSnapshot: (options?: {
61
66
  clearMissing?: boolean;
62
67
  }) => boolean;
63
- showElementSelectionFrames: ({ elements, scrollOffset, }: {
68
+ commitElementSelection: ({ element, clickPosition, isPendingClickOnSelection, }: {
69
+ element: Element;
70
+ clickPosition: SelectModeClickPosition;
71
+ isPendingClickOnSelection?: (data: SelectableData) => boolean;
72
+ }) => SelectableData;
73
+ expandToSection: (input: {
74
+ targetSelector: string;
75
+ }) => void;
76
+ setSectionPreview: (input: {
77
+ targetSelector: string | null;
78
+ }) => void;
79
+ showElementSelectionFrames: ({ elements, isSection, scrollOffset, }: {
64
80
  elements: Element[];
81
+ isSection?: boolean;
65
82
  scrollOffset: {
66
83
  left: number;
67
84
  top: number;
@@ -79,8 +96,7 @@ interface SelectModeStoreState {
79
96
  x: number;
80
97
  y: number;
81
98
  } | null) => void;
82
- setLastPostedHoverRect: (rect: SelectModeBoundingRect | null | undefined) => void;
83
- resetHoverRectDedup: () => void;
99
+ setLastPostedHover: (hover: PostedHover | undefined) => void;
84
100
  setHoverSyncAnimationFrameId: (id: number | null) => void;
85
101
  setLastClick: (lastClick: LastClick | null) => void;
86
102
  setDragCandidates: (dragCandidates: DragCandidate[]) => void;
@@ -111,8 +127,20 @@ export declare const useSelectModeStoreActions: () => {
111
127
  postSelectedElementSnapshot: (options?: {
112
128
  clearMissing?: boolean;
113
129
  }) => boolean;
114
- showElementSelectionFrames: ({ elements, scrollOffset, }: {
130
+ commitElementSelection: ({ element, clickPosition, isPendingClickOnSelection, }: {
131
+ element: Element;
132
+ clickPosition: SelectModeClickPosition;
133
+ isPendingClickOnSelection?: (data: SelectableData) => boolean;
134
+ }) => SelectableData;
135
+ expandToSection: (input: {
136
+ targetSelector: string;
137
+ }) => void;
138
+ setSectionPreview: (input: {
139
+ targetSelector: string | null;
140
+ }) => void;
141
+ showElementSelectionFrames: ({ elements, isSection, scrollOffset, }: {
115
142
  elements: Element[];
143
+ isSection?: boolean;
116
144
  scrollOffset: {
117
145
  left: number;
118
146
  top: number;
@@ -130,8 +158,7 @@ export declare const useSelectModeStoreActions: () => {
130
158
  x: number;
131
159
  y: number;
132
160
  } | null) => void;
133
- setLastPostedHoverRect: (rect: SelectModeBoundingRect | null | undefined) => void;
134
- resetHoverRectDedup: () => void;
161
+ setLastPostedHover: (hover: PostedHover | undefined) => void;
135
162
  setHoverSyncAnimationFrameId: (id: number | null) => void;
136
163
  setLastClick: (lastClick: LastClick | null) => void;
137
164
  setDragCandidates: (dragCandidates: DragCandidate[]) => void;
@@ -149,6 +176,7 @@ export declare const useSelectModeStoreActions: () => {
149
176
  };
150
177
  export declare const useDisplayMode: () => DisplayMode;
151
178
  export declare const useSelectionFrames: () => SelectionFrameSpec[];
179
+ export declare const useSectionHoverRect: () => SelectModeHoverRect | null;
152
180
  export declare const useIsSelectionFrameEditing: () => boolean;
153
181
  export declare const useEditingElement: () => HTMLElement | null;
154
182
  export declare const useInteractionSession: () => SelectModeInteractionSession;
@@ -10,9 +10,11 @@ import {
10
10
  requestInlineTextEdit
11
11
  } from "./iframeRpcChannel";
12
12
  import { reportInlineEditFeedback } from "./inlineEditFeedback";
13
+ import { findContainingSection } from "./interactions/sectionGeometry";
13
14
  import { getDocumentScrollOffset } from "./interactions/selectionGeometry";
14
15
  import { isTextEditableElement } from "./isTextEditableElement";
15
16
  import { getSelectionFrameSpecs } from "./selectionFrame";
17
+ import { isSelectionStillCurrent } from "./selectionPublish";
16
18
  import { getConnectedSelectedElement, IDLE_SESSION } from "./selectModeSession";
17
19
  import {
18
20
  isUserSourceLocation,
@@ -42,6 +44,21 @@ function stopInlineEditRequest(request) {
42
44
  }
43
45
  request.stopFeedback();
44
46
  }
47
+ function buildElementSelection({
48
+ element,
49
+ clickPosition
50
+ }) {
51
+ const section = findContainingSection(element);
52
+ const isSection = section === element;
53
+ const siblingElements = isSection ? [] : getRepetitionSiblingElements({ element });
54
+ const data = buildElementDescriptor({
55
+ element,
56
+ clickPosition,
57
+ siblingSelectors: siblingElements.map(buildSelector),
58
+ section
59
+ });
60
+ return { data, siblingElements, isSection };
61
+ }
45
62
  function reportInlineEditingState({
46
63
  activeMarks,
47
64
  isInlineEditing,
@@ -59,7 +76,8 @@ const INITIAL_STATE = {
59
76
  selectionFrames: [],
60
77
  isSelectionFrameEditing: false,
61
78
  lastPointerClientPosition: null,
62
- lastPostedHoverRect: void 0,
79
+ lastPostedHover: void 0,
80
+ sectionPreviewElement: null,
63
81
  hoverSyncAnimationFrameId: null,
64
82
  lastClick: null,
65
83
  dragCandidates: [],
@@ -294,7 +312,8 @@ const selectModeStore = createShallowStore(
294
312
  set({
295
313
  interactionSession: IDLE_SESSION,
296
314
  selectionFrames: [],
297
- lastPostedHoverRect: void 0,
315
+ lastPostedHover: void 0,
316
+ sectionPreviewElement: null,
298
317
  hoverSyncAnimationFrameId: null,
299
318
  dragAnimationFrameId: null,
300
319
  dragCandidates: []
@@ -356,15 +375,19 @@ const selectModeStore = createShallowStore(
356
375
  });
357
376
  },
358
377
  setInteractionSession: (interactionSession) => {
359
- set({ interactionSession });
378
+ set({ interactionSession, sectionPreviewElement: null });
360
379
  },
361
380
  resetInteractionSession,
362
381
  postSelectedElementSnapshot: ({ clearMissing = false } = {}) => {
363
382
  const { interactionSession } = get();
364
- if (interactionSession.type !== "element") return true;
383
+ if (interactionSession.type !== "element") {
384
+ return true;
385
+ }
365
386
  const element = getConnectedSelectedElement(interactionSession);
366
387
  if (!element) {
367
- if (!clearMissing) return false;
388
+ if (!clearMissing) {
389
+ return false;
390
+ }
368
391
  get().actions.setSelectionType({ selectionType: "none" });
369
392
  getWebsiteBuilderRpc()?.areaSelected({
370
393
  rect: interactionSession.data.rect,
@@ -372,27 +395,39 @@ const selectModeStore = createShallowStore(
372
395
  });
373
396
  return true;
374
397
  }
375
- const siblingElements = getRepetitionSiblingElements({ element });
376
- const descriptor = buildElementDescriptor({
398
+ const {
399
+ data: descriptor,
400
+ siblingElements,
401
+ isSection
402
+ } = buildElementSelection({
377
403
  element,
378
- clickPosition: interactionSession.clickPosition ?? (interactionSession.data.kind === "react-element" ? interactionSession.data.clickPosition : { x: 0, y: 0 }),
379
- siblingSelectors: siblingElements.map(buildSelector)
404
+ clickPosition: interactionSession.clickPosition ?? (interactionSession.data.kind === "react-element" ? interactionSession.data.clickPosition : { x: 0, y: 0 })
380
405
  });
381
- if (descriptor.kind !== "react-element") return true;
406
+ if (descriptor.kind !== "react-element") {
407
+ return true;
408
+ }
382
409
  const refreshedSession = {
383
410
  ...interactionSession,
384
411
  element,
385
- siblingElements
412
+ siblingElements,
413
+ data: descriptor
386
414
  };
387
- set({ interactionSession: refreshedSession });
415
+ set({
416
+ interactionSession: refreshedSession,
417
+ sectionPreviewElement: null
418
+ });
388
419
  get().actions.showElementSelectionFrames({
389
420
  elements: [element, ...siblingElements],
421
+ isSection,
390
422
  scrollOffset: getDocumentScrollOffset()
391
423
  });
392
424
  void symbolicateSelectedElement(descriptor).then((symbolicated) => {
393
- if (get().interactionSession !== refreshedSession) return;
394
- if (symbolicated.sourceLocation && !isUserSourceLocation(symbolicated.sourceLocation))
425
+ if (get().interactionSession !== refreshedSession) {
426
+ return;
427
+ }
428
+ if (symbolicated.sourceLocation && !isUserSourceLocation(symbolicated.sourceLocation)) {
395
429
  return;
430
+ }
396
431
  set({
397
432
  interactionSession: { ...refreshedSession, data: symbolicated }
398
433
  });
@@ -400,12 +435,94 @@ const selectModeStore = createShallowStore(
400
435
  });
401
436
  return true;
402
437
  },
403
- showElementSelectionFrames: ({ elements, scrollOffset }) => {
438
+ commitElementSelection: ({
439
+ element,
440
+ clickPosition,
441
+ isPendingClickOnSelection
442
+ }) => {
443
+ set({ sectionPreviewElement: null });
444
+ const { data, siblingElements, isSection } = buildElementSelection({
445
+ element,
446
+ clickPosition
447
+ });
448
+ get().actions.showElementSelectionFrames({
449
+ elements: [element, ...siblingElements],
450
+ isSection,
451
+ scrollOffset: getDocumentScrollOffset()
452
+ });
453
+ if (!element.isConnected) {
454
+ return data;
455
+ }
456
+ const rect = element.getBoundingClientRect();
457
+ set({
458
+ interactionSession: {
459
+ type: "element",
460
+ element,
461
+ siblingElements,
462
+ clickPosition: {
463
+ x: clickPosition.x - rect.left,
464
+ y: clickPosition.y - rect.top
465
+ },
466
+ data
467
+ }
468
+ });
469
+ const descriptorPromise = data.kind === "react-element" ? symbolicateSelectedElement(data) : Promise.resolve(data);
470
+ void descriptorPromise.then((result) => {
471
+ const session = get().interactionSession;
472
+ const isCurrent = session.type === "element" && session.data === data && isSelectionStillCurrent({
473
+ session,
474
+ selection: { type: "element", element }
475
+ });
476
+ if (isCurrent || isPendingClickOnSelection?.(data)) {
477
+ getWebsiteBuilderRpc()?.elementSelected({ data: result });
478
+ }
479
+ });
480
+ return data;
481
+ },
482
+ expandToSection: ({ targetSelector }) => {
483
+ const { editingElement, inlineEditRequest, interactionSession } = get();
484
+ if (interactionSession.type !== "element" || interactionSession.data.kind !== "react-element" || interactionSession.data.selector !== targetSelector || !interactionSession.data.section || editingElement || inlineEditRequest) {
485
+ return;
486
+ }
487
+ const section = interactionSession.element.isConnected ? findContainingSection(interactionSession.element) : null;
488
+ if (!section || section === interactionSession.element) {
489
+ return;
490
+ }
491
+ const rect = section.getBoundingClientRect();
492
+ get().actions.commitElementSelection({
493
+ element: section,
494
+ clickPosition: {
495
+ x: rect.left + rect.width / 2,
496
+ y: rect.top + Math.min(24, rect.height / 2)
497
+ }
498
+ });
499
+ },
500
+ setSectionPreview: ({ targetSelector }) => {
501
+ if (!targetSelector) {
502
+ set({ sectionPreviewElement: null });
503
+ return;
504
+ }
505
+ const {
506
+ displayMode,
507
+ editingElement,
508
+ inlineEditRequest,
509
+ interactionSession
510
+ } = get();
511
+ if (displayMode !== "selection" || interactionSession.type !== "element" || interactionSession.data.kind !== "react-element" || interactionSession.data.selector !== targetSelector || !interactionSession.data.section || editingElement || inlineEditRequest) {
512
+ return;
513
+ }
514
+ const section = interactionSession.element.isConnected ? findContainingSection(interactionSession.element) : null;
515
+ if (!section || section === interactionSession.element) {
516
+ return;
517
+ }
518
+ set({ sectionPreviewElement: section });
519
+ },
520
+ showElementSelectionFrames: ({ elements, isSection, scrollOffset }) => {
404
521
  set({
405
522
  selectionFrames: getSelectionFrameSpecs({
406
523
  elements,
407
524
  scrollOffset,
408
- variant: "element"
525
+ variant: isSection ? "section" : "element"
409
526
  })
410
527
  });
411
528
  },
@@ -424,11 +541,8 @@ const selectModeStore = createShallowStore(
424
541
  setLastPointerClientPosition: (lastPointerClientPosition) => {
425
542
  set({ lastPointerClientPosition });
426
543
  },
427
- setLastPostedHoverRect: (lastPostedHoverRect) => {
428
- set({ lastPostedHoverRect });
429
- },
430
- resetHoverRectDedup: () => {
431
- set({ lastPostedHoverRect: void 0 });
544
+ setLastPostedHover: (lastPostedHover) => {
545
+ set({ lastPostedHover });
432
546
  },
433
547
  setHoverSyncAnimationFrameId: (hoverSyncAnimationFrameId) => {
434
548
  set({ hoverSyncAnimationFrameId });
@@ -557,6 +671,11 @@ const useSelectionFrames = () => {
557
671
  (state) => state.selectionFrames
558
672
  );
559
673
  };
674
+ const useSectionHoverRect = () => {
675
+ return useShallowStore(useSelectModeStore(), (state) => {
676
+ return state.displayMode === "selection" && state.interactionSession.type !== "drag" && state.lastPostedHover?.rect?.isSection ? state.lastPostedHover.rect : null;
677
+ });
678
+ };
560
679
  const useIsSelectionFrameEditing = () => {
561
680
  return useShallowStore(
562
681
  useSelectModeStore(),
@@ -579,6 +698,7 @@ export {
579
698
  useEditingElement,
580
699
  useInteractionSession,
581
700
  useIsSelectionFrameEditing,
701
+ useSectionHoverRect,
582
702
  useSelectModeStore,
583
703
  useSelectModeStoreActions,
584
704
  useSelectionFrames
@@ -1,7 +1,9 @@
1
1
  export declare const SELECTION_FRAMES_CONTAINER_ID = "replo-select-mode-selection-frames";
2
2
  export declare const SELECTION_FRAME_BORDER_HEX = "#c026d3";
3
3
  export declare const SELECTION_FRAME_EDITING_BORDER_HEX = "#e879f9";
4
- export type SelectionFrameVariant = "element" | "area";
4
+ export declare const SECTION_SELECTION_FRAME_BORDER_HEX = "#60a5fa";
5
+ export declare const SECTION_SELECTION_FRAME_BACKGROUND = "rgba(96, 165, 250, 0.06)";
6
+ export type SelectionFrameVariant = "element" | "area" | "section";
5
7
  export type SelectionFramePageRect = {
6
8
  x: number;
7
9
  y: number;
@@ -1,6 +1,8 @@
1
1
  const SELECTION_FRAMES_CONTAINER_ID = "replo-select-mode-selection-frames";
2
2
  const SELECTION_FRAME_BORDER_HEX = "#c026d3";
3
3
  const SELECTION_FRAME_EDITING_BORDER_HEX = "#e879f9";
4
+ const SECTION_SELECTION_FRAME_BORDER_HEX = "#60a5fa";
5
+ const SECTION_SELECTION_FRAME_BACKGROUND = "rgba(96, 165, 250, 0.06)";
4
6
  function getSelectionFrameSpecs({
5
7
  elements,
6
8
  scrollOffset,
@@ -20,6 +22,8 @@ function getSelectionFrameSpecs({
20
22
  });
21
23
  }
22
24
  export {
25
+ SECTION_SELECTION_FRAME_BACKGROUND,
26
+ SECTION_SELECTION_FRAME_BORDER_HEX,
23
27
  SELECTION_FRAMES_CONTAINER_ID,
24
28
  SELECTION_FRAME_BORDER_HEX,
25
29
  SELECTION_FRAME_EDITING_BORDER_HEX,