@replohq/sandbox-runtime 1.6.8 → 1.7.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 (29) hide show
  1. package/_vendor/replo-utils/lib/viewportSnapshot.d.ts +28 -7
  2. package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
  3. package/_vendor/replo-utils/react/use-viewport-snapshot-responder.d.ts +2 -3
  4. package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +18 -2
  5. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +1 -0
  6. package/_vendor/schemas/selectModeIframeRpc.d.ts +2 -0
  7. package/_vendor/schemas/selectModeIframeRpc.mjs +11 -6
  8. package/buildMetadata.js +3 -3
  9. package/{chunk-RXYLMSNM.mjs → chunk-3YA3QXUQ.mjs} +27 -9
  10. package/package.json +4 -4
  11. package/select-mode/hooks/useEditModeStateMachine.js +14 -22
  12. package/select-mode/hooks/useEditModeStateMachine.js.map +2 -2
  13. package/select-mode/hooks/useParentMessaging.d.ts +4 -1
  14. package/select-mode/hooks/useParentMessaging.js +2 -2
  15. package/select-mode/hooks/useParentMessaging.js.map +2 -2
  16. package/select-mode/hooks/useSelectModeController.d.ts +4 -1
  17. package/select-mode/hooks/useSelectModeController.js +27 -2
  18. package/select-mode/hooks/useSelectModeController.js.map +2 -2
  19. package/select-mode/hooks/useSelectionInteractions.js +153 -53
  20. package/select-mode/hooks/useSelectionInteractions.js.map +2 -2
  21. package/select-mode/interactions/selectionGeometry.d.ts +25 -0
  22. package/select-mode/interactions/selectionGeometry.js +83 -1
  23. package/select-mode/interactions/selectionGeometry.js.map +2 -2
  24. package/select-mode/selectModeSession.d.ts +9 -1
  25. package/select-mode/selectModeSession.js +15 -1
  26. package/select-mode/selectModeSession.js.map +2 -2
  27. package/select-mode/selectionFrame.d.ts +7 -0
  28. package/select-mode/selectionFrame.js +20 -0
  29. package/select-mode/selectionFrame.js.map +2 -2
@@ -12,19 +12,40 @@ import {
12
12
  getDragPositionFromClient,
13
13
  getElementAnchor,
14
14
  getPageRectFromViewportRect,
15
+ getPointRect,
16
+ getSelectionAfterAdditiveClick,
15
17
  getViewportRectFromPageRect,
16
- getViewportSelectionRect
18
+ getViewportSelectionRect,
19
+ isAdditiveSelectionClick,
20
+ unionElementPageRects
17
21
  } from "../interactions/selectionGeometry";
18
22
  import {
19
- showElementSelectionFrames,
20
- showSelectionFrames
23
+ hideSelectionFrame,
24
+ showAreaSelectionFrames,
25
+ showElementSelectionFrames
21
26
  } from "../selectionFrame";
22
27
  import { isSelectionStillCurrent } from "../selectionPublish";
23
- import { IDLE_SESSION } from "../selectModeSession";
28
+ import {
29
+ getConnectedSelectedElement,
30
+ IDLE_SESSION
31
+ } from "../selectModeSession";
24
32
  import { symbolicateSelectedElement } from "../symbolicate";
25
33
  import { isTextEditableElement } from "./useEditModeStateMachine";
26
34
  import { getRepetitionSiblingElements } from "./useSiblingResolution";
27
35
  const DOUBLE_CLICK_THRESHOLD_MS = 500;
36
+ function getSelectedElementsFromSession(session) {
37
+ if (session.type === "element") {
38
+ const liveElement = getConnectedSelectedElement(session);
39
+ return liveElement ? [liveElement] : [];
40
+ }
41
+ if (session.type === "area") {
42
+ return session.items.flatMap((item) => {
43
+ const liveElement = getConnectedSelectedElement(item);
44
+ return liveElement ? [liveElement] : [];
45
+ });
46
+ }
47
+ return [];
48
+ }
28
49
  async function maybeSymbolicate(data) {
29
50
  return data.kind === "react-element" ? symbolicateSelectedElement(data) : data;
30
51
  }
@@ -42,6 +63,14 @@ function useSelectionInteractions({
42
63
  const editingPointerIds = /* @__PURE__ */ new Set();
43
64
  let lastClick = null;
44
65
  let dragCandidates = [];
66
+ let committedSelectionAtPointerDown = [];
67
+ const clearCommittedSelection = ({
68
+ rect
69
+ }) => {
70
+ hideSelectionFrame();
71
+ interactionSessionRef.current = IDLE_SESSION;
72
+ getWebsiteBuilderRpc()?.areaSelected({ rect, items: [] });
73
+ };
45
74
  const stopDragAnimation = () => {
46
75
  if (dragAnimationFrameId === null) {
47
76
  return;
@@ -133,6 +162,9 @@ function useSelectionInteractions({
133
162
  dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);
134
163
  };
135
164
  const handlePointerDown = (event) => {
165
+ if (event.button !== 0) {
166
+ return;
167
+ }
136
168
  editingPointerIds.delete(event.pointerId);
137
169
  if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
138
170
  editingPointerIds.add(event.pointerId);
@@ -141,6 +173,9 @@ function useSelectionInteractions({
141
173
  event.preventDefault();
142
174
  event.stopPropagation();
143
175
  const scrollOffset = getDocumentScrollOffset();
176
+ committedSelectionAtPointerDown = getSelectedElementsFromSession(
177
+ interactionSessionRef.current
178
+ );
144
179
  const dragStart = getDragPositionFromClient({
145
180
  clientX: event.clientX,
146
181
  clientY: event.clientY,
@@ -180,16 +215,59 @@ function useSelectionInteractions({
180
215
  }
181
216
  dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);
182
217
  };
218
+ const commitAreaSelection = ({
219
+ selectedElements,
220
+ pageRect,
221
+ anchorRect,
222
+ scrollOffset
223
+ }) => {
224
+ const items = selectedElements.map((element) => ({
225
+ element,
226
+ data: buildElementDescriptor({
227
+ element,
228
+ clickPosition: getElementAnchor({
229
+ element,
230
+ selectionRect: anchorRect
231
+ })
232
+ })
233
+ }));
234
+ interactionSessionRef.current = {
235
+ type: "area",
236
+ pageRect,
237
+ items
238
+ };
239
+ showAreaSelectionFrames({ elements: selectedElements, scrollOffset });
240
+ const rect = getViewportRectFromPageRect({
241
+ rect: pageRect,
242
+ scrollOffset
243
+ });
244
+ void Promise.all(items.map((item) => maybeSymbolicate(item.data))).then(
245
+ (symbolicatedItems) => {
246
+ if (!isSelectionStillCurrent({
247
+ session: interactionSessionRef.current,
248
+ selection: { type: "area", pageRect }
249
+ })) {
250
+ return;
251
+ }
252
+ getWebsiteBuilderRpc()?.areaSelected({
253
+ rect,
254
+ items: symbolicatedItems
255
+ });
256
+ }
257
+ );
258
+ };
183
259
  const commitElementSelection = ({
184
- targetElement,
260
+ selectionElement,
185
261
  pointerEvent
186
262
  }) => {
187
- const selectionElement = getNearestSelectableElement(targetElement);
188
263
  const targetRect = selectionElement.getBoundingClientRect();
189
- const clickPosition = {
190
- x: pointerEvent.clientX,
191
- y: pointerEvent.clientY
192
- };
264
+ const clickPosition = getElementAnchor({
265
+ element: selectionElement,
266
+ selectionRect: getPointRect({
267
+ x: pointerEvent.clientX,
268
+ y: pointerEvent.clientY
269
+ })
270
+ });
193
271
  const scrollOffset = getDocumentScrollOffset();
194
272
  const siblingElements = selectionElement instanceof HTMLElement ? getRepetitionSiblingElements({ element: selectionElement }) : [];
195
273
  showElementSelectionFrames({
@@ -224,8 +302,8 @@ function useSelectionInteractions({
224
302
  element: selectionElement,
225
303
  siblingElements,
226
304
  clickPosition: {
227
- x: pointerEvent.clientX - targetRect.left,
228
- y: pointerEvent.clientY - targetRect.top
305
+ x: clickPosition.x - targetRect.left,
306
+ y: clickPosition.y - targetRect.top
229
307
  },
230
308
  data
231
309
  };
@@ -239,7 +317,58 @@ function useSelectionInteractions({
239
317
  getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
240
318
  });
241
319
  };
320
+ const commitClickSelection = ({
321
+ targetElement,
322
+ pointerEvent,
323
+ candidates
324
+ }) => {
325
+ const selectionElement = getNearestSelectableElement(targetElement);
326
+ if (!isAdditiveSelectionClick(pointerEvent)) {
327
+ commitElementSelection({ selectionElement, pointerEvent });
328
+ return;
329
+ }
330
+ lastClick = null;
331
+ const pointerRect = getPointRect({
332
+ x: pointerEvent.clientX,
333
+ y: pointerEvent.clientY
334
+ });
335
+ const nextElements = getSelectionAfterAdditiveClick({
336
+ candidates,
337
+ currentSelectedElements: committedSelectionAtPointerDown,
338
+ targetElement: selectionElement
339
+ });
340
+ const [firstElement] = nextElements;
341
+ if (!firstElement) {
342
+ clearCommittedSelection({ rect: pointerRect });
343
+ return;
344
+ }
345
+ if (nextElements.length === 1) {
346
+ commitElementSelection({
347
+ selectionElement: firstElement,
348
+ pointerEvent
349
+ });
350
+ lastClick = null;
351
+ return;
352
+ }
353
+ const scrollOffset = getDocumentScrollOffset();
354
+ const pageRect = unionElementPageRects({
355
+ elements: nextElements,
356
+ scrollOffset
357
+ });
358
+ if (!pageRect) {
359
+ return;
360
+ }
361
+ commitAreaSelection({
362
+ selectedElements: nextElements,
363
+ pageRect,
364
+ anchorRect: pointerRect,
365
+ scrollOffset
366
+ });
367
+ };
242
368
  const handlePointerUp = (event) => {
369
+ if (event.button !== 0) {
370
+ return;
371
+ }
243
372
  if (editingPointerIds.delete(event.pointerId)) {
244
373
  return;
245
374
  }
@@ -258,9 +387,10 @@ function useSelectionInteractions({
258
387
  if (!(targetElement instanceof Element)) {
259
388
  return;
260
389
  }
261
- commitElementSelection({
390
+ commitClickSelection({
262
391
  targetElement,
263
- pointerEvent: event
392
+ pointerEvent: event,
393
+ candidates
264
394
  });
265
395
  return;
266
396
  }
@@ -275,9 +405,10 @@ function useSelectionInteractions({
275
405
  if (!(targetElement instanceof Element)) {
276
406
  return;
277
407
  }
278
- commitElementSelection({
408
+ commitClickSelection({
279
409
  targetElement,
280
- pointerEvent: event
410
+ pointerEvent: event,
411
+ candidates
281
412
  });
282
413
  return;
283
414
  }
@@ -289,45 +420,14 @@ function useSelectionInteractions({
289
420
  candidates,
290
421
  selectionPageRect: pageRect
291
422
  });
292
- const items = selectedCandidates.map((candidate) => {
293
- return buildElementDescriptor({
294
- element: candidate.element,
295
- clickPosition: getElementAnchor({
296
- element: candidate.element,
297
- selectionRect
298
- })
299
- });
300
- });
301
- interactionSessionRef.current = {
302
- type: "area",
423
+ commitAreaSelection({
424
+ selectedElements: selectedCandidates.map(
425
+ (candidate) => candidate.element
426
+ ),
303
427
  pageRect,
304
- items
305
- };
306
- showSelectionFrames({
307
- frames: selectedCandidates.map((candidate) => ({
308
- variant: "area",
309
- pageRect: {
310
- x: candidate.pageRect.x,
311
- y: candidate.pageRect.y,
312
- width: candidate.pageRect.width,
313
- height: candidate.pageRect.height
314
- }
315
- }))
428
+ anchorRect: selectionRect,
429
+ scrollOffset
316
430
  });
317
- void Promise.all(items.map(maybeSymbolicate)).then(
318
- (symbolicatedItems) => {
319
- if (!isSelectionStillCurrent({
320
- session: interactionSessionRef.current,
321
- selection: { type: "area", pageRect }
322
- })) {
323
- return;
324
- }
325
- getWebsiteBuilderRpc()?.areaSelected({
326
- rect: selectionRect,
327
- items: symbolicatedItems
328
- });
329
- }
330
- );
331
431
  };
332
432
  const handleClick = (event) => {
333
433
  if (editingElementRef?.current && event.target instanceof Node && editingElementRef.current.contains(event.target)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectionInteractions.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n} from \"../interactions/selectionGeometry\";\nimport {\n showElementSelectionFrames,\n showSelectionFrames,\n} from \"../selectionFrame\";\nimport { isSelectionStillCurrent } from \"../selectionPublish\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n const editingPointerIds = new Set<number>();\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n editingPointerIds.add(event.pointerId);\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitElementSelection = ({\n targetElement,\n pointerEvent,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n const targetRect = selectionElement.getBoundingClientRect();\n const clickPosition: SelectModeClickPosition = {\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n };\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: pointerEvent.clientX - targetRect.left,\n y: pointerEvent.clientY - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"element\", element: selectionElement },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (editingPointerIds.delete(event.pointerId)) {\n return;\n }\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitElementSelection({\n targetElement,\n pointerEvent: event,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n const items = selectedCandidates.map((candidate) => {\n return buildElementDescriptor({\n element: candidate.element,\n clickPosition: getElementAnchor({\n element: candidate.element,\n selectionRect,\n }),\n });\n });\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showSelectionFrames({\n frames: selectedCandidates.map((candidate) => ({\n variant: \"area\" as const,\n pageRect: {\n x: candidate.pageRect.x,\n y: candidate.pageRect.y,\n width: candidate.pageRect.width,\n height: candidate.pageRect.height,\n },\n })),\n });\n\n void Promise.all(items.map(maybeSymbolicate)).then(\n (symbolicatedItems) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"area\", pageRect },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.areaSelected({\n rect: selectionRect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n editingPointerIds.clear();\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
5
- "mappings": "AASA,SAAS,iBAAiB;AAE1B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,OACK;AACP,SAAS,+BAA+B;AACxC,SAAS,oBAAoB;AAC7B,SAAS,kCAAkC;AAC3C,SAAS,6BAA6B;AACtC,SAAS,oCAAoC;AAE7C,MAAM,4BAA4B;AAKlC,eAAe,iBAAiB,MAA+C;AAC7E,SAAO,KAAK,SAAS,kBACjB,2BAA2B,IAAI,IAC/B;AACN;AA2BO,SAAS,yBAAyB;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAQG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,QAAI,uBAAsC;AAC1C,UAAM,oBAAoB,oBAAI,IAAY;AAC1C,QAAI,YAIO;AAGX,QAAI,iBAAkC,CAAC;AAEvC,UAAM,oBAAoB,MAAM;AAC9B,UAAI,yBAAyB,MAAM;AACjC;AAAA,MACF;AAEA,2BAAqB,oBAAoB;AACzC,6BAAuB;AAAA,IACzB;AAQA,UAAM,iBAAiB,CAAC;AAAA,MACtB,0BAA0B;AAAA,IAC5B,IAA2C,CAAC,MAAM;AAChD,YAAM,iBAAiB,sBAAsB;AAE7C,wBAAkB;AAElB,UAAI,eAAe,SAAS,QAAQ;AAClC,yBAAiB,CAAC;AAClB;AAAA,MACF;AAEA,UAAI,2BAA2B,eAAe,sBAAsB;AAClE,6BAAqB,GAAG,gBAAgB;AAAA,UACtC,UAAU;AAAA,UACV,gBAAgB,CAAC;AAAA,QACnB,CAAC;AAAA,MACH;AAEA,uBAAiB,CAAC;AAClB,4BAAsB,UAAU;AAAA,IAClC;AAEA,UAAM,sBAAsB,MAAM;AAChC,YAAM,UAAU,sBAAsB;AACtC,UAAI,QAAQ,SAAS,QAAQ;AAC3B;AAAA,MACF;AAEA,YAAM,OAAO,yBAAyB;AAAA,QACpC,WAAW,QAAQ;AAAA,QACnB,aAAa,QAAQ;AAAA,QACrB,cAAc,QAAQ;AAAA,MACxB,CAAC;AAED,UACE,KAAK,SAAS,KACd,KAAK,UAAU,KACf,CAAC,QAAQ,sBACT;AACA;AAAA,MACF;AAEA,UAAI,CAAC,QAAQ,sBAAsB;AACjC,8BAAsB,UAAU;AAAA,UAC9B,GAAG;AAAA,UACH,sBAAsB;AAAA,QACxB;AAAA,MACF;AAKA,YAAM,oBAAoB,4BAA4B;AAAA,QACpD;AAAA,QACA,cAAc,QAAQ;AAAA,MACxB,CAAC;AACD,YAAM,iBAAiB,yBAAyB;AAAA,QAC9C,YAAY;AAAA,QACZ;AAAA,MACF,CAAC,EAAE,IAAI,CAAC,eAAe;AAAA,QACrB,IAAI,UAAU;AAAA,QACd,MAAM,4BAA4B;AAAA,UAChC,MAAM,UAAU;AAAA,UAChB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH,EAAE;AAEF,2BAAqB,GAAG,gBAAgB;AAAA,QACtC,UAAU;AAAA,QACV;AAAA,MACF,CAAC;AAAA,IACH;AAEA,UAAM,yBAAyB,MAAM;AACnC,YAAM,UAAU,sBAAsB;AACtC,UAAI,QAAQ,SAAS,QAAQ;AAC3B,+BAAuB;AACvB;AAAA,MACF;AAEA,YAAM,mBAAmB,wBAAwB;AACjD,YAAM,kBAAkB,0BAA0B;AAAA,QAChD,SAAS,QAAQ,YAAY;AAAA,QAC7B,SAAS,QAAQ,YAAY;AAAA,QAC7B,cAAc;AAAA,MAChB,CAAC;AAED,UACE,gBAAgB,UAAU,QAAQ,YAAY,SAC9C,gBAAgB,UAAU,QAAQ,YAAY,OAC9C;AACA,8BAAsB,UAAU;AAAA,UAC9B,GAAG;AAAA,UACH,aAAa;AAAA,UACb,cAAc;AAAA,QAChB;AACA,4BAAoB;AAAA,MACtB,OAAO;AACL,8BAAsB,UAAU;AAAA,UAC9B,GAAG;AAAA,UACH,cAAc;AAAA,QAChB;AAAA,MACF;AAEA,6BAAuB,sBAAsB,sBAAsB;AAAA,IACrE;AAEA,UAAM,oBAAoB,CAAC,UAAwB;AACjD,wBAAkB,OAAO,MAAM,SAAS;AACxC,UACE,mBAAmB,WACnB,MAAM,kBAAkB,QACxB,kBAAkB,QAAQ,SAAS,MAAM,MAAM,GAC/C;AACA,0BAAkB,IAAI,MAAM,SAAS;AACrC;AAAA,MACF;AACA,YAAM,eAAe;AACrB,YAAM,gBAAgB;AAEtB,YAAM,eAAe,wBAAwB;AAC7C,YAAM,YAAY,0BAA0B;AAAA,QAC1C,SAAS,MAAM;AAAA,QACf,SAAS,MAAM;AAAA,QACf;AAAA,MACF,CAAC;AACD,4BAAsB,UAAU;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA,aAAa;AAAA,QACb;AAAA,QACA,sBAAsB;AAAA,MACxB;AACA,uBAAiB,sBAAsB,YAAY;AAEnD,UAAI,yBAAyB,MAAM;AACjC,+BAAuB,sBAAsB,sBAAsB;AAAA,MACrE;AAAA,IACF;AAEA,UAAM,oBAAoB,CAAC,UAAwB;AACjD,YAAM,UAAU,sBAAsB;AACtC,UAAI,QAAQ,SAAS,QAAQ;AAC3B;AAAA,MACF;AAEA,4BAAsB,UAAU;AAAA,QAC9B,GAAG;AAAA,QACH,aAAa,0BAA0B;AAAA,UACrC,SAAS,MAAM;AAAA,UACf,SAAS,MAAM;AAAA,UACf,cAAc,wBAAwB;AAAA,QACxC,CAAC;AAAA,MACH;AACA,0BAAoB;AAAA,IACtB;AAEA,UAAM,iCAAiC,MAAM;AAC3C,YAAM,UAAU,sBAAsB;AACtC,UAAI,QAAQ,SAAS,UAAU,yBAAyB,MAAM;AAC5D;AAAA,MACF;AAEA,6BAAuB,sBAAsB,sBAAsB;AAAA,IACrE;AAEA,UAAM,yBAAyB,CAAC;AAAA,MAC9B;AAAA,MACA;AAAA,IACF,MAGM;AACJ,YAAM,mBAAmB,4BAA4B,aAAa;AAClE,YAAM,aAAa,iBAAiB,sBAAsB;AAC1D,YAAM,gBAAyC;AAAA,QAC7C,GAAG,aAAa;AAAA,QAChB,GAAG,aAAa;AAAA,MAClB;AAKA,YAAM,eAAe,wBAAwB;AAC7C,YAAM,kBACJ,4BAA4B,cACxB,6BAA6B,EAAE,SAAS,iBAAiB,CAAC,IAC1D,CAAC;AACP,iCAA2B;AAAA,QACzB,UAAU,CAAC,kBAAkB,GAAG,eAAe;AAAA,QAC/C;AAAA,MACF,CAAC;AAED,YAAM,MAAM,aAAa;AACzB,YAAM,gBAAgB;AACtB,UACE,iBACA,cAAc,WAAW,oBACzB,MAAM,cAAc,QAAQ,6BAC5B,sBAAsB,gBAAgB,GACtC;AACA,oBAAY;AACZ,uBAAe;AAAA,UACb,SAAS;AAAA,UACT,mBAAmB,cAAc;AAAA,QACnC,CAAC;AACD;AAAA,MACF;AASA,YAAM,OAAO,uBAAuB;AAAA,QAClC,SAAS;AAAA,QACT;AAAA,QACA,kBAAkB,gBAAgB,IAAI,aAAa;AAAA,MACrD,CAAC;AACD,kBAAY;AAAA,QACV,mBAAmB,QAAQ,QAAQ,IAAI;AAAA,QACvC,QAAQ;AAAA,QACR,MAAM;AAAA,MACR;AAEA,UAAI,CAAC,iBAAiB,aAAa;AACjC;AAAA,MACF;AACA,4BAAsB,UAAU;AAAA,QAC9B,MAAM;AAAA,QACN,SAAS;AAAA,QACT;AAAA,QACA,eAAe;AAAA,UACb,GAAG,aAAa,UAAU,WAAW;AAAA,UACrC,GAAG,aAAa,UAAU,WAAW;AAAA,QACvC;AAAA,QACA;AAAA,MACF;AACA,WAAK,iBAAiB,IAAI,EAAE,KAAK,CAAC,iBAAiB;AACjD,YACE,CAAC,wBAAwB;AAAA,UACvB,SAAS,sBAAsB;AAAA,UAC/B,WAAW,EAAE,MAAM,WAAW,SAAS,iBAAiB;AAAA,QAC1D,CAAC,GACD;AACA;AAAA,QACF;AACA,6BAAqB,GAAG,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAAA,MAChE,CAAC;AAAA,IACH;AAEA,UAAM,kBAAkB,CAAC,UAAwB;AAC/C,UAAI,kBAAkB,OAAO,MAAM,SAAS,GAAG;AAC7C;AAAA,MACF;AAMA,UACE,mBAAmB,WACnB,MAAM,kBAAkB,QACxB,kBAAkB,QAAQ,SAAS,MAAM,MAAM,GAC/C;AACA;AAAA,MACF;AACA,YAAM,WAAW,sBAAsB;AACvC,YAAM,cACJ,SAAS,SAAS,SAAS,SAAS,uBAAuB;AAC7D,YAAM,YAAY,SAAS,SAAS,SAAS,SAAS,YAAY;AAClE,YAAM,cACJ,SAAS,SAAS,SAAS,SAAS,cAAc;AACpD,YAAM,eACJ,SAAS,SAAS,SACd,SAAS,eACT,wBAAwB;AAI9B,YAAM,aAAa;AAMnB,qBAAe,EAAE,yBAAyB,MAAM,CAAC;AAEjD,UAAI,CAAC,aAAa,SAAS,SAAS,QAAQ;AAC1C,cAAM,gBAAgB,MAAM;AAC5B,YAAI,EAAE,yBAAyB,UAAU;AACvC;AAAA,QACF;AAEA,+BAAuB;AAAA,UACrB;AAAA,UACA,cAAc;AAAA,QAChB,CAAC;AACD;AAAA,MACF;AAEA,YAAM,sBAAsB,eAAe;AAC3C,YAAM,gBAAgB,yBAAyB;AAAA,QAC7C;AAAA,QACA,aAAa;AAAA,QACb;AAAA,MACF,CAAC;AAED,UAAI,CAAC,aAAa;AAChB,cAAM,gBAAgB,MAAM;AAC5B,YAAI,EAAE,yBAAyB,UAAU;AACvC;AAAA,QACF;AAEA,+BAAuB;AAAA,UACrB;AAAA,UACA,cAAc;AAAA,QAChB,CAAC;AACD;AAAA,MACF;AAEA,YAAM,WAAW,4BAA4B;AAAA,QAC3C,MAAM;AAAA,QACN;AAAA,MACF,CAAC;AAED,YAAM,qBAAqB,yBAAyB;AAAA,QAClD;AAAA,QACA,mBAAmB;AAAA,MACrB,CAAC;AACD,YAAM,QAAQ,mBAAmB,IAAI,CAAC,cAAc;AAClD,eAAO,uBAAuB;AAAA,UAC5B,SAAS,UAAU;AAAA,UACnB,eAAe,iBAAiB;AAAA,YAC9B,SAAS,UAAU;AAAA,YACnB;AAAA,UACF,CAAC;AAAA,QACH,CAAC;AAAA,MACH,CAAC;AAED,4BAAsB,UAAU;AAAA,QAC9B,MAAM;AAAA,QACN;AAAA,QACA;AAAA,MACF;AAKA,0BAAoB;AAAA,QAClB,QAAQ,mBAAmB,IAAI,CAAC,eAAe;AAAA,UAC7C,SAAS;AAAA,UACT,UAAU;AAAA,YACR,GAAG,UAAU,SAAS;AAAA,YACtB,GAAG,UAAU,SAAS;AAAA,YACtB,OAAO,UAAU,SAAS;AAAA,YAC1B,QAAQ,UAAU,SAAS;AAAA,UAC7B;AAAA,QACF,EAAE;AAAA,MACJ,CAAC;AAED,WAAK,QAAQ,IAAI,MAAM,IAAI,gBAAgB,CAAC,EAAE;AAAA,QAC5C,CAAC,sBAAsB;AACrB,cACE,CAAC,wBAAwB;AAAA,YACvB,SAAS,sBAAsB;AAAA,YAC/B,WAAW,EAAE,MAAM,QAAQ,SAAS;AAAA,UACtC,CAAC,GACD;AACA;AAAA,UACF;AACA,+BAAqB,GAAG,aAAa;AAAA,YACnC,MAAM;AAAA,YACN,OAAO;AAAA,UACT,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF;AAEA,UAAM,cAAc,CAAC,UAAsB;AACzC,UACE,mBAAmB,WACnB,MAAM,kBAAkB,QACxB,kBAAkB,QAAQ,SAAS,MAAM,MAAM,GAC/C;AACA;AAAA,MACF;AACA,YAAM,eAAe;AACrB,YAAM,gBAAgB;AAAA,IACxB;AAEA,UAAM,sBAAsB,CAAC,UAAwB;AACnD,wBAAkB,OAAO,MAAM,SAAS;AACxC,qBAAe;AAAA,IACjB;AAEA,UAAM,mBAAmB,MAAM;AAC7B,wBAAkB,MAAM;AACxB,qBAAe;AAAA,IACjB;AAEA,aAAS,iBAAiB,eAAe,mBAAmB,IAAI;AAChE,aAAS,iBAAiB,eAAe,mBAAmB,IAAI;AAChE,aAAS,iBAAiB,aAAa,iBAAiB,IAAI;AAC5D,aAAS,iBAAiB,iBAAiB,qBAAqB,IAAI;AACpE,aAAS,iBAAiB,SAAS,aAAa,IAAI;AACpD,aAAS,iBAAiB,UAAU,gCAAgC,IAAI;AACxE,WAAO,iBAAiB,QAAQ,gBAAgB;AAChD,WAAO,iBAAiB,UAAU,8BAA8B;AAEhE,WAAO,MAAM;AACX,eAAS,oBAAoB,eAAe,mBAAmB,IAAI;AACnE,eAAS,oBAAoB,eAAe,mBAAmB,IAAI;AACnE,eAAS,oBAAoB,aAAa,iBAAiB,IAAI;AAC/D,eAAS,oBAAoB,iBAAiB,qBAAqB,IAAI;AACvE,eAAS,oBAAoB,SAAS,aAAa,IAAI;AACvD,eAAS;AAAA,QACP;AAAA,QACA;AAAA,QACA;AAAA,MACF;AACA,aAAO,oBAAoB,QAAQ,gBAAgB;AACnD,aAAO,oBAAoB,UAAU,8BAA8B;AACnE,qBAAe;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,aAAa,mBAAmB,uBAAuB,cAAc,CAAC;AAC5E;",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeBoundingRect,\n} from \"schemas/generated/selectMode\";\nimport type { DragCandidate } from \"../interactions/selectionGeometry\";\nimport type {\n SelectModeAreaItem,\n SelectModeInteractionSession,\n} from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport {\n buildElementDescriptor,\n buildSelector,\n getNearestSelectableElement,\n} from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n collectDragCandidates,\n getCandidatesInSelection,\n getDocumentScrollOffset,\n getDragPositionFromClient,\n getElementAnchor,\n getPageRectFromViewportRect,\n getPointRect,\n getSelectionAfterAdditiveClick,\n getViewportRectFromPageRect,\n getViewportSelectionRect,\n isAdditiveSelectionClick,\n unionElementPageRects,\n} from \"../interactions/selectionGeometry\";\nimport {\n hideSelectionFrame,\n showAreaSelectionFrames,\n showElementSelectionFrames,\n} from \"../selectionFrame\";\nimport { isSelectionStillCurrent } from \"../selectionPublish\";\nimport {\n getConnectedSelectedElement,\n IDLE_SESSION,\n} from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\nconst DOUBLE_CLICK_THRESHOLD_MS = 500;\n\nfunction getSelectedElementsFromSession(\n session: SelectModeInteractionSession,\n): Element[] {\n if (session.type === \"element\") {\n const liveElement = getConnectedSelectedElement(session);\n return liveElement ? [liveElement] : [];\n }\n if (session.type === \"area\") {\n return session.items.flatMap((item) => {\n const liveElement = getConnectedSelectedElement(item);\n return liveElement ? [liveElement] : [];\n });\n }\n return [];\n}\n\n// React fibers carry only Turbopack chunk paths; resolving them to the\n// user's source file is async and only meaningful for \"react-element\"\n// kinds. Plan selectables already point at the right thing.\nasync function maybeSymbolicate(data: SelectableData): Promise<SelectableData> {\n return data.kind === \"react-element\"\n ? symbolicateSelectedElement(data)\n : data;\n}\n\n/**\n * Owns the iframe-side pointer interactions that drive selection:\n *\n * - On `pointerdown` we open a `drag` session (storing both client + page\n * coords plus the current scroll offset). We don't yet know if this will\n * become a click or an actual drag.\n * - On `pointermove` we update the drag rect and post `DRAG_RECT_UPDATED`\n * to the parent so the live drag rectangle can render. We only flip\n * `hasExceededThreshold` once the rect grows past 6\u00D76 px so that a tiny\n * accidental drag still resolves as a click.\n * - On `pointerup` we either:\n * - resolve as an `ELEMENT_SELECTED` (single click / sub-threshold drag)\n * - resolve as an `AREA_SELECTED` (drag past threshold), filtering the\n * candidates snapshotted at drag start against the final rectangle to\n * enumerate which descendants the user enclosed.\n * - During an active drag a `requestAnimationFrame` loop keeps the rect in\n * sync with the page scroll so the rectangle visually \"sticks\" to the\n * page rather than the viewport.\n *\n * We capture-listen and `preventDefault`/`stopPropagation` clicks so the\n * underlying page can't navigate or trigger handlers while the user is\n * working in selection mode.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionInteractions({\n displayMode,\n editingElementRef,\n interactionSessionRef,\n onBeginEditing,\n}: {\n displayMode: DisplayMode;\n editingElementRef?: MutableRefObject<HTMLElement | null>;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n onBeginEditing: (args: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => void;\n}) {\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n if (displayMode !== \"selection\") {\n return;\n }\n\n let dragAnimationFrameId: number | null = null;\n const editingPointerIds = new Set<number>();\n let lastClick: {\n descriptorPromise: Promise<SelectableData>;\n target: Element;\n time: number;\n } | null = null;\n // Snapshot of the page's meaningful elements + their page rects, taken\n // once when a drag begins so per-move hit-testing avoids a full-DOM scan.\n let dragCandidates: DragCandidate[] = [];\n // Pointerdown always opens a drag session, so stash the committed\n // selection here. A sub-threshold Shift/Command click reads it back.\n let committedSelectionAtPointerDown: Element[] = [];\n\n const clearCommittedSelection = ({\n rect,\n }: {\n rect: SelectModeBoundingRect;\n }) => {\n hideSelectionFrame();\n interactionSessionRef.current = IDLE_SESSION;\n getWebsiteBuilderRpc()?.areaSelected({ rect, items: [] });\n };\n\n const stopDragAnimation = () => {\n if (dragAnimationFrameId === null) {\n return;\n }\n\n cancelAnimationFrame(dragAnimationFrameId);\n dragAnimationFrameId = null;\n };\n\n // `notifyParentDragCleared` posts a null drag rect so the parent tears\n // down the live drag overlay. The area-commit path suppresses it: the\n // parent maps a null drag rect to `selection = none`, which round-trips\n // back as `setSelectionType(\"none\")` and wipes the in-iframe frames we're\n // about to paint. The follow-up `areaSelected` supersedes the drag overlay\n // on its own, so the null post is pure collateral damage there.\n const resetDragState = ({\n notifyParentDragCleared = true,\n }: { notifyParentDragCleared?: boolean } = {}) => {\n const currentSession = interactionSessionRef.current;\n\n stopDragAnimation();\n\n if (currentSession.type !== \"drag\") {\n dragCandidates = [];\n return;\n }\n\n if (notifyParentDragCleared && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n\n dragCandidates = [];\n interactionSessionRef.current = IDLE_SESSION;\n };\n\n const postCurrentDragRect = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n const rect = getViewportSelectionRect({\n dragStart: session.dragStart,\n dragCurrent: session.dragCurrent,\n scrollOffset: session.scrollOffset,\n });\n\n if (\n rect.width <= 6 &&\n rect.height <= 6 &&\n !session.hasExceededThreshold\n ) {\n return;\n }\n\n if (!session.hasExceededThreshold) {\n interactionSessionRef.current = {\n ...session,\n hasExceededThreshold: true,\n };\n }\n\n // Cheap hit-test against the candidates we snapshotted at drag start,\n // projected back to viewport coords so the parent can paint a live\n // highlight on each element the drag will capture. No DOM scan here.\n const selectionPageRect = getPageRectFromViewportRect({\n rect,\n scrollOffset: session.scrollOffset,\n });\n const selectionRects = getCandidatesInSelection({\n candidates: dragCandidates,\n selectionPageRect,\n }).map((candidate) => ({\n id: candidate.id,\n rect: getViewportRectFromPageRect({\n rect: candidate.pageRect,\n scrollOffset: session.scrollOffset,\n }),\n }));\n\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: rect,\n selectionRects,\n });\n };\n\n const syncDragRectToViewport = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n dragAnimationFrameId = null;\n return;\n }\n\n const nextScrollOffset = getDocumentScrollOffset();\n const nextDragCurrent = getDragPositionFromClient({\n clientX: session.dragCurrent.clientX,\n clientY: session.dragCurrent.clientY,\n scrollOffset: nextScrollOffset,\n });\n\n if (\n nextDragCurrent.pageX !== session.dragCurrent.pageX ||\n nextDragCurrent.pageY !== session.dragCurrent.pageY\n ) {\n interactionSessionRef.current = {\n ...session,\n dragCurrent: nextDragCurrent,\n scrollOffset: nextScrollOffset,\n };\n postCurrentDragRect();\n } else {\n interactionSessionRef.current = {\n ...session,\n scrollOffset: nextScrollOffset,\n };\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0) {\n return;\n }\n editingPointerIds.delete(event.pointerId);\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n editingPointerIds.add(event.pointerId);\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n\n const scrollOffset = getDocumentScrollOffset();\n committedSelectionAtPointerDown = getSelectedElementsFromSession(\n interactionSessionRef.current,\n );\n const dragStart = getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset,\n });\n interactionSessionRef.current = {\n type: \"drag\",\n dragStart,\n dragCurrent: dragStart,\n scrollOffset,\n hasExceededThreshold: false,\n };\n dragCandidates = collectDragCandidates(scrollOffset);\n\n if (dragAnimationFrameId === null) {\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n }\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\") {\n return;\n }\n\n interactionSessionRef.current = {\n ...session,\n dragCurrent: getDragPositionFromClient({\n clientX: event.clientX,\n clientY: event.clientY,\n scrollOffset: getDocumentScrollOffset(),\n }),\n };\n postCurrentDragRect();\n };\n\n const handleViewportChangeDuringDrag = () => {\n const session = interactionSessionRef.current;\n if (session.type !== \"drag\" || dragAnimationFrameId !== null) {\n return;\n }\n\n dragAnimationFrameId = requestAnimationFrame(syncDragRectToViewport);\n };\n\n const commitAreaSelection = ({\n selectedElements,\n pageRect,\n anchorRect,\n scrollOffset,\n }: {\n selectedElements: Element[];\n pageRect: SelectModeBoundingRect;\n // Viewport rect each member's click anchor is clamped toward: the drag\n // rect for drag-select, the pointer for a modifier click.\n anchorRect: SelectModeBoundingRect;\n scrollOffset: ReturnType<typeof getDocumentScrollOffset>;\n }) => {\n const items: SelectModeAreaItem[] = selectedElements.map((element) => ({\n element,\n data: buildElementDescriptor({\n element,\n clickPosition: getElementAnchor({\n element,\n selectionRect: anchorRect,\n }),\n }),\n }));\n\n interactionSessionRef.current = {\n type: \"area\",\n pageRect,\n items,\n };\n\n // Highlight each captured element so the committed area selection\n // \"sticks\" inside the iframe \u2014 matching how a single click stays\n // outlined \u2014 rather than drawing one faint box over the drag region.\n showAreaSelectionFrames({ elements: selectedElements, scrollOffset });\n\n const rect = getViewportRectFromPageRect({\n rect: pageRect,\n scrollOffset,\n });\n void Promise.all(items.map((item) => maybeSymbolicate(item.data))).then(\n (symbolicatedItems) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"area\", pageRect },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.areaSelected({\n rect,\n items: symbolicatedItems,\n });\n },\n );\n };\n\n const commitElementSelection = ({\n selectionElement,\n pointerEvent,\n }: {\n selectionElement: Element;\n pointerEvent: PointerEvent;\n }) => {\n const targetRect = selectionElement.getBoundingClientRect();\n // A modifier click can commit an element the pointer is not over\n // (deselecting down to one), so clamp the anchor into its bounds.\n const clickPosition = getElementAnchor({\n element: selectionElement,\n selectionRect: getPointRect({\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n }),\n });\n\n // Paint the in-iframe selection frames synchronously for instant\n // feedback \u2014 symbolicate is a network roundtrip and we don't want\n // the visual selection to wait on it.\n const scrollOffset = getDocumentScrollOffset();\n const siblingElements =\n selectionElement instanceof HTMLElement\n ? getRepetitionSiblingElements({ element: selectionElement })\n : [];\n showElementSelectionFrames({\n elements: [selectionElement, ...siblingElements],\n scrollOffset,\n });\n\n const now = pointerEvent.timeStamp;\n const previousClick = lastClick;\n if (\n previousClick &&\n previousClick.target === selectionElement &&\n now - previousClick.time <= DOUBLE_CLICK_THRESHOLD_MS &&\n isTextEditableElement(selectionElement)\n ) {\n lastClick = null;\n onBeginEditing({\n element: selectionElement,\n descriptorPromise: previousClick.descriptorPromise,\n });\n return;\n }\n\n // NOTE (Noah, 2026-07-24): Commit the selection immediately rather than\n // deferring it behind the double-click window. A single click always\n // selects, and a double-click is just \"select + edit\", so there's nothing\n // to suppress \u2014 waiting only added a full DOUBLE_CLICK_THRESHOLD_MS of\n // latency to every click and left `interactionSessionRef` non-element in\n // the gap. Double-click is still detected via the `lastClick` timestamp\n // above; the follow-up click reuses this descriptor to open the editor.\n const data = buildElementDescriptor({\n element: selectionElement,\n clickPosition,\n siblingSelectors: siblingElements.map(buildSelector),\n });\n lastClick = {\n descriptorPromise: Promise.resolve(data),\n target: selectionElement,\n time: now,\n };\n\n if (!selectionElement.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element: selectionElement,\n siblingElements,\n clickPosition: {\n x: clickPosition.x - targetRect.left,\n y: clickPosition.y - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n if (\n !isSelectionStillCurrent({\n session: interactionSessionRef.current,\n selection: { type: \"element\", element: selectionElement },\n })\n ) {\n return;\n }\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const commitClickSelection = ({\n targetElement,\n pointerEvent,\n candidates,\n }: {\n targetElement: Element;\n pointerEvent: PointerEvent;\n candidates: DragCandidate[];\n }) => {\n const selectionElement = getNearestSelectableElement(targetElement);\n if (!isAdditiveSelectionClick(pointerEvent)) {\n commitElementSelection({ selectionElement, pointerEvent });\n return;\n }\n\n // A modifier click is never the second half of a double-click.\n lastClick = null;\n const pointerRect = getPointRect({\n x: pointerEvent.clientX,\n y: pointerEvent.clientY,\n });\n const nextElements = getSelectionAfterAdditiveClick({\n candidates,\n currentSelectedElements: committedSelectionAtPointerDown,\n targetElement: selectionElement,\n });\n const [firstElement] = nextElements;\n if (!firstElement) {\n clearCommittedSelection({ rect: pointerRect });\n return;\n }\n if (nextElements.length === 1) {\n commitElementSelection({\n selectionElement: firstElement,\n pointerEvent,\n });\n // The survivor was not clicked, so it must not arm double-click.\n lastClick = null;\n return;\n }\n const scrollOffset = getDocumentScrollOffset();\n const pageRect = unionElementPageRects({\n elements: nextElements,\n scrollOffset,\n });\n if (!pageRect) {\n return;\n }\n commitAreaSelection({\n selectedElements: nextElements,\n pageRect,\n anchorRect: pointerRect,\n scrollOffset,\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\n if (event.button !== 0) {\n return;\n }\n if (editingPointerIds.delete(event.pointerId)) {\n return;\n }\n // Clicks inside the element being edited belong to the inline editor, not\n // select mode. Without this guard the double-click detection below re-runs\n // `onBeginEditing` on the same element (e.g. when repositioning the\n // caret), rebuilding the editor over itself and wiping the content.\n // Mirrors the guards in `handlePointerDown` and `handleClick`.\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n const snapshot = interactionSessionRef.current;\n const wasDragging =\n snapshot.type === \"drag\" ? snapshot.hasExceededThreshold : false;\n const dragStart = snapshot.type === \"drag\" ? snapshot.dragStart : null;\n const dragCurrent =\n snapshot.type === \"drag\" ? snapshot.dragCurrent : null;\n const scrollOffset =\n snapshot.type === \"drag\"\n ? snapshot.scrollOffset\n : getDocumentScrollOffset();\n // `resetDragState` clears `dragCandidates`, so grab them first \u2014 the\n // final committed set is the same cached candidates the live preview\n // used, so preview and result never disagree.\n const candidates = dragCandidates;\n\n // Suppress the null drag-rect post: if this resolves to an area\n // selection it would otherwise flash the parent to `selection = none`\n // and wipe the frames we paint below. Click/empty-drag paths don't\n // depend on it (a sub-threshold drag never posted a rect to clear).\n resetDragState({ notifyParentDragCleared: false });\n\n if (!dragStart || snapshot.type !== \"drag\") {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitClickSelection({\n targetElement,\n pointerEvent: event,\n candidates,\n });\n return;\n }\n\n const resolvedDragCurrent = dragCurrent ?? dragStart;\n const selectionRect = getViewportSelectionRect({\n dragStart,\n dragCurrent: resolvedDragCurrent,\n scrollOffset,\n });\n\n if (!wasDragging) {\n const targetElement = event.target;\n if (!(targetElement instanceof Element)) {\n return;\n }\n\n commitClickSelection({\n targetElement,\n pointerEvent: event,\n candidates,\n });\n return;\n }\n\n const pageRect = getPageRectFromViewportRect({\n rect: selectionRect,\n scrollOffset,\n });\n\n const selectedCandidates = getCandidatesInSelection({\n candidates,\n selectionPageRect: pageRect,\n });\n commitAreaSelection({\n selectedElements: selectedCandidates.map(\n (candidate) => candidate.element,\n ),\n pageRect,\n anchorRect: selectionRect,\n scrollOffset,\n });\n };\n\n const handleClick = (event: MouseEvent) => {\n if (\n editingElementRef?.current &&\n event.target instanceof Node &&\n editingElementRef.current.contains(event.target)\n ) {\n return;\n }\n event.preventDefault();\n event.stopPropagation();\n };\n\n const handlePointerCancel = (event: PointerEvent) => {\n editingPointerIds.delete(event.pointerId);\n resetDragState();\n };\n\n const handleWindowBlur = () => {\n editingPointerIds.clear();\n resetDragState();\n };\n\n document.addEventListener(\"pointerdown\", handlePointerDown, true);\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerup\", handlePointerUp, true);\n document.addEventListener(\"pointercancel\", handlePointerCancel, true);\n document.addEventListener(\"click\", handleClick, true);\n document.addEventListener(\"scroll\", handleViewportChangeDuringDrag, true);\n window.addEventListener(\"blur\", handleWindowBlur);\n window.addEventListener(\"resize\", handleViewportChangeDuringDrag);\n\n return () => {\n document.removeEventListener(\"pointerdown\", handlePointerDown, true);\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerup\", handlePointerUp, true);\n document.removeEventListener(\"pointercancel\", handlePointerCancel, true);\n document.removeEventListener(\"click\", handleClick, true);\n document.removeEventListener(\n \"scroll\",\n handleViewportChangeDuringDrag,\n true,\n );\n window.removeEventListener(\"blur\", handleWindowBlur);\n window.removeEventListener(\"resize\", handleViewportChangeDuringDrag);\n resetDragState();\n };\n }, [displayMode, editingElementRef, interactionSessionRef, onBeginEditing]);\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -86,6 +86,31 @@ export declare function getCandidatesInSelection({ candidates, selectionPageRect
86
86
  candidates: DragCandidate[];
87
87
  selectionPageRect: SelectModeBoundingRect;
88
88
  }): DragCandidate[];
89
+ /**
90
+ * Shift / Command / Ctrl click toggles the clicked element in the current
91
+ * selection (theme-builder canvas rules): a click inside an already-selected
92
+ * ancestor is ignored, selecting a parent drops its selected descendants,
93
+ * and once every candidate child of a candidate parent is selected the
94
+ * parent replaces them — the same collapse drag-select applies.
95
+ */
96
+ export declare function getSelectionAfterAdditiveClick({ candidates, currentSelectedElements, targetElement, }: {
97
+ candidates: DragCandidate[];
98
+ currentSelectedElements: Element[];
99
+ targetElement: Element;
100
+ }): Element[];
101
+ export declare function isAdditiveSelectionClick(event: {
102
+ shiftKey: boolean;
103
+ metaKey: boolean;
104
+ ctrlKey: boolean;
105
+ }): boolean;
106
+ export declare function getPointRect({ x, y, }: {
107
+ x: number;
108
+ y: number;
109
+ }): SelectModeBoundingRect;
110
+ export declare function unionElementPageRects({ elements, scrollOffset, }: {
111
+ elements: Element[];
112
+ scrollOffset: ScrollOffset;
113
+ }): SelectModeBoundingRect | null;
89
114
  /**
90
115
  * Picks a "click position" for an element that fell inside an area
91
116
  * selection. We use the corner of the element nearest the top-left of the
@@ -154,6 +154,84 @@ function getCandidatesInSelection({
154
154
  });
155
155
  return collapseFullyCoveredChildren({ candidates, selected: intersecting });
156
156
  }
157
+ function getSelectionAfterAdditiveClick({
158
+ candidates,
159
+ currentSelectedElements,
160
+ targetElement
161
+ }) {
162
+ if (currentSelectedElements.includes(targetElement)) {
163
+ return currentSelectedElements.filter(
164
+ (element) => element !== targetElement
165
+ );
166
+ }
167
+ if (currentSelectedElements.some((element) => element.contains(targetElement))) {
168
+ return currentSelectedElements;
169
+ }
170
+ const selected = new Set(
171
+ currentSelectedElements.filter(
172
+ (element) => !targetElement.contains(element)
173
+ )
174
+ );
175
+ selected.add(targetElement);
176
+ const candidateElements = new Set(
177
+ candidates.map((candidate) => candidate.element)
178
+ );
179
+ let parent = targetElement.parentElement;
180
+ while (parent && candidateElements.has(parent)) {
181
+ const candidateChildren = Array.from(parent.children).filter((child) => {
182
+ return candidateElements.has(child);
183
+ });
184
+ if (candidateChildren.length === 0 || !candidateChildren.every((child) => selected.has(child))) {
185
+ break;
186
+ }
187
+ for (const child of candidateChildren) {
188
+ selected.delete(child);
189
+ }
190
+ selected.add(parent);
191
+ parent = parent.parentElement;
192
+ }
193
+ return [...selected];
194
+ }
195
+ function isAdditiveSelectionClick(event) {
196
+ return event.shiftKey || event.metaKey || event.ctrlKey;
197
+ }
198
+ function getPointRect({
199
+ x,
200
+ y
201
+ }) {
202
+ return { x, y, width: 0, height: 0, top: y, right: x, bottom: y, left: x };
203
+ }
204
+ function unionElementPageRects({
205
+ elements,
206
+ scrollOffset
207
+ }) {
208
+ const rects = elements.map((element) => {
209
+ return getPageRectFromViewportRect({
210
+ rect: getElementBoundingRect(element),
211
+ scrollOffset
212
+ });
213
+ });
214
+ const firstRect = rects[0];
215
+ if (!firstRect) {
216
+ return null;
217
+ }
218
+ return rects.slice(1).reduce((union, rect) => {
219
+ const left = Math.min(union.left, rect.left);
220
+ const top = Math.min(union.top, rect.top);
221
+ const right = Math.max(union.right, rect.right);
222
+ const bottom = Math.max(union.bottom, rect.bottom);
223
+ return {
224
+ x: left,
225
+ y: top,
226
+ width: right - left,
227
+ height: bottom - top,
228
+ left,
229
+ top,
230
+ right,
231
+ bottom
232
+ };
233
+ }, firstRect);
234
+ }
157
235
  function getElementAnchor({
158
236
  element,
159
237
  selectionRect
@@ -186,7 +264,11 @@ export {
186
264
  getElementAnchor,
187
265
  getElementBoundingRect,
188
266
  getPageRectFromViewportRect,
267
+ getPointRect,
268
+ getSelectionAfterAdditiveClick,
189
269
  getViewportRectFromPageRect,
190
- getViewportSelectionRect
270
+ getViewportSelectionRect,
271
+ isAdditiveSelectionClick,
272
+ unionElementPageRects
191
273
  };
192
274
  //# sourceMappingURL=selectionGeometry.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/interactions/selectionGeometry.ts"],
4
- "sourcesContent": ["import type {\n SelectModeBoundingRect,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\n\nimport { isIntersecting, isRectEnclosing } from \"replo-utils/lib/geometry\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { isInlineTextMarkElement } from \"../inlineTextMarks\";\n\nexport type DragPosition = {\n clientX: number;\n clientY: number;\n pageX: number;\n pageY: number;\n};\n\nexport type ScrollOffset = {\n left: number;\n top: number;\n};\n\nexport type SelectedAreaState = {\n pageRect: SelectModeBoundingRect;\n};\n\n// NOTE (Noah, 2026-04-21): A small fudge factor so elements\n// that visually appear to touch the selection rect (but whose bounding\n// box is a pixel or two off due to borders/sub-pixel rendering) still get\n// included.\nconst SELECTION_TOLERANCE_PX = 4;\n\nfunction normalizeSelectionRect({\n startX,\n startY,\n endX,\n endY,\n}: {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}): SelectModeBoundingRect {\n const left = Math.min(startX, endX);\n const right = Math.max(startX, endX);\n const top = Math.min(startY, endY);\n const bottom = Math.max(startY, endY);\n\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n top,\n right,\n bottom,\n left,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDragPositionFromClient({\n clientX,\n clientY,\n scrollOffset,\n}: {\n clientX: number;\n clientY: number;\n scrollOffset: ScrollOffset;\n}): DragPosition {\n return {\n clientX,\n clientY,\n pageX: clientX + scrollOffset.left,\n pageY: clientY + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDocumentScrollOffset(): ScrollOffset {\n const scrollingElement = document.scrollingElement;\n return {\n left: scrollingElement?.scrollLeft ?? window.scrollX,\n top: scrollingElement?.scrollTop ?? window.scrollY,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportSelectionRect({\n dragStart,\n dragCurrent,\n scrollOffset,\n}: {\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return normalizeSelectionRect({\n startX: dragStart.pageX - scrollOffset.left,\n startY: dragStart.pageY - scrollOffset.top,\n endX: dragCurrent.pageX - scrollOffset.left,\n endY: dragCurrent.pageY - scrollOffset.top,\n });\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getPageRectFromViewportRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x + scrollOffset.left,\n y: rect.y + scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left + scrollOffset.left,\n top: rect.top + scrollOffset.top,\n right: rect.right + scrollOffset.left,\n bottom: rect.bottom + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportRectFromPageRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x - scrollOffset.left,\n y: rect.y - scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left - scrollOffset.left,\n top: rect.top - scrollOffset.top,\n right: rect.right - scrollOffset.left,\n bottom: rect.bottom - scrollOffset.top,\n };\n}\n\nfunction isMeaningfulElement(\n element: Element,\n elementRect: SelectModeBoundingRect,\n): boolean {\n const tagName = element.tagName.toLowerCase();\n if ([\"html\", \"body\", \"script\", \"style\", \"meta\", \"link\"].includes(tagName)) {\n return false;\n }\n\n if (isInlineTextMarkElement(element)) {\n return false;\n }\n\n if (elementRect.width < 4 || elementRect.height < 4) {\n return false;\n }\n\n return true;\n}\n\n/**\n * A drag captures an element when their boxes overlap (within\n * `tolerancePx`) \u2014 but NOT when the drag sits entirely inside the\n * element. The latter case is the user dragging *within* a large\n * container (a section, `<main>`, `<body>`) to lasso its children; the\n * container itself isn't a thing they're trying to select, so we drop it.\n */\nexport function doesRectIntersectSelection({\n elementRect,\n selectionRect,\n tolerancePx,\n}: {\n elementRect: SelectModeBoundingRect;\n selectionRect: SelectModeBoundingRect;\n tolerancePx: number;\n}) {\n return (\n isIntersecting(elementRect, selectionRect, { tolerance: tolerancePx }) &&\n !isRectEnclosing(elementRect, selectionRect)\n );\n}\n\n// A meaningful element plus its page-coordinate rect and a stable id,\n// snapshotted once when a drag begins. Page coordinates are scroll-\n// invariant, so per-move hit-testing never needs to re-measure.\nexport type DragCandidate = {\n id: string;\n element: Element;\n pageRect: SelectModeBoundingRect;\n};\n\n/**\n * Snapshots every meaningful element and its page rect when a drag begins.\n * Each candidate gets a stable id so the parent overlay can key its\n * live highlights without remounting them as the drag moves.\n */\nexport function collectDragCandidates(\n scrollOffset: ScrollOffset,\n): DragCandidate[] {\n const candidates: DragCandidate[] = [];\n for (const element of Array.from(document.querySelectorAll(\"*\"))) {\n const viewportRect = getElementBoundingRect(element);\n if (!isMeaningfulElement(element, viewportRect)) {\n continue;\n }\n candidates.push({\n id: uuidv4(),\n element,\n pageRect: getPageRectFromViewportRect({\n rect: viewportRect,\n scrollOffset,\n }),\n });\n }\n return candidates;\n}\n\n/**\n * Drop a child whose parent is also selected and every one of that\n * parent's *candidate* children is selected: the parent already represents\n * the whole group, so the children are redundant. Returns only the\n * candidates that survive the collapse.\n *\n * Only children that are themselves drag candidates count toward \"fully\n * covered\". A parent's DOM children can include decorative / non-selectable\n * elements (and `.children` excludes text nodes); those can never be\n * selected, so counting them would wedge the check permanently false and\n * the collapse would never fire.\n *\n * Membership is read once up front, so the collapse cascades up the tree\n * in a single pass \u2014 a parent kept this round is itself dropped when *its*\n * parent is likewise fully covered, leaving only the topmost ancestor of\n * any fully-enclosed subtree.\n */\nfunction collapseFullyCoveredChildren({\n candidates,\n selected,\n}: {\n candidates: DragCandidate[];\n selected: DragCandidate[];\n}): DragCandidate[] {\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n const selectedElements = new Set(\n selected.map((candidate) => candidate.element),\n );\n return selected.filter((candidate) => {\n const parent = candidate.element.parentElement;\n if (!parent || !selectedElements.has(parent)) {\n return true;\n }\n const everyCandidateChildSelected = Array.from(parent.children)\n .filter((child) => candidateElements.has(child))\n .every((child) => selectedElements.has(child));\n return !everyCandidateChildSelected;\n });\n}\n\n/**\n * Cheap per-move hit-test against the pre-collected candidates: keep the\n * ones whose page rect overlaps the selection (excluding any that wholly\n * enclose it), then collapse any fully-covered parent/children group down\n * to just the parent (see `collapseFullyCoveredChildren`). Does no DOM\n * queries or re-measures of geometry \u2014 the only DOM reads are cheap\n * parent/child structural lookups.\n *\n * Because the collapse depends on which siblings are currently captured,\n * elements legitimately move in and out of the result as the drag grows or\n * shrinks: a child drops out the moment the drag swallows its last sibling\n * (the parent takes over), and reappears if the drag retreats.\n */\nexport function getCandidatesInSelection({\n candidates,\n selectionPageRect,\n}: {\n candidates: DragCandidate[];\n selectionPageRect: SelectModeBoundingRect;\n}): DragCandidate[] {\n const intersecting = candidates.filter((candidate) => {\n return doesRectIntersectSelection({\n elementRect: candidate.pageRect,\n selectionRect: selectionPageRect,\n tolerancePx: SELECTION_TOLERANCE_PX,\n });\n });\n\n return collapseFullyCoveredChildren({ candidates, selected: intersecting });\n}\n\n/**\n * Picks a \"click position\" for an element that fell inside an area\n * selection. We use the corner of the element nearest the top-left of the\n * selection rect, clamped to the element's own bounds. This anchor is what\n * lets the selected-element overlay continue to track the element when the\n * user scrolls (`useSelectionScrollSync` re-projects relative to it).\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementAnchor({\n element,\n selectionRect,\n}: {\n element: Element;\n selectionRect: SelectModeBoundingRect;\n}): SelectModeClickPosition {\n const rect = element.getBoundingClientRect();\n\n return {\n x: Math.min(Math.max(selectionRect.left, rect.left), rect.right),\n y: Math.min(Math.max(selectionRect.top, rect.top), rect.bottom),\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementBoundingRect(\n element: Element,\n): SelectModeBoundingRect {\n const rect = element.getBoundingClientRect();\n\n return {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n };\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n SelectModeBoundingRect,\n SelectModeClickPosition,\n} from \"schemas/generated/selectMode\";\n\nimport { isIntersecting, isRectEnclosing } from \"replo-utils/lib/geometry\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nimport { isInlineTextMarkElement } from \"../inlineTextMarks\";\n\nexport type DragPosition = {\n clientX: number;\n clientY: number;\n pageX: number;\n pageY: number;\n};\n\nexport type ScrollOffset = {\n left: number;\n top: number;\n};\n\nexport type SelectedAreaState = {\n pageRect: SelectModeBoundingRect;\n};\n\n// NOTE (Noah, 2026-04-21): A small fudge factor so elements\n// that visually appear to touch the selection rect (but whose bounding\n// box is a pixel or two off due to borders/sub-pixel rendering) still get\n// included.\nconst SELECTION_TOLERANCE_PX = 4;\n\nfunction normalizeSelectionRect({\n startX,\n startY,\n endX,\n endY,\n}: {\n startX: number;\n startY: number;\n endX: number;\n endY: number;\n}): SelectModeBoundingRect {\n const left = Math.min(startX, endX);\n const right = Math.max(startX, endX);\n const top = Math.min(startY, endY);\n const bottom = Math.max(startY, endY);\n\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n top,\n right,\n bottom,\n left,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDragPositionFromClient({\n clientX,\n clientY,\n scrollOffset,\n}: {\n clientX: number;\n clientY: number;\n scrollOffset: ScrollOffset;\n}): DragPosition {\n return {\n clientX,\n clientY,\n pageX: clientX + scrollOffset.left,\n pageY: clientY + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getDocumentScrollOffset(): ScrollOffset {\n const scrollingElement = document.scrollingElement;\n return {\n left: scrollingElement?.scrollLeft ?? window.scrollX,\n top: scrollingElement?.scrollTop ?? window.scrollY,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportSelectionRect({\n dragStart,\n dragCurrent,\n scrollOffset,\n}: {\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return normalizeSelectionRect({\n startX: dragStart.pageX - scrollOffset.left,\n startY: dragStart.pageY - scrollOffset.top,\n endX: dragCurrent.pageX - scrollOffset.left,\n endY: dragCurrent.pageY - scrollOffset.top,\n });\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getPageRectFromViewportRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x + scrollOffset.left,\n y: rect.y + scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left + scrollOffset.left,\n top: rect.top + scrollOffset.top,\n right: rect.right + scrollOffset.left,\n bottom: rect.bottom + scrollOffset.top,\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getViewportRectFromPageRect({\n rect,\n scrollOffset,\n}: {\n rect: SelectModeBoundingRect;\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect {\n return {\n x: rect.x - scrollOffset.left,\n y: rect.y - scrollOffset.top,\n width: rect.width,\n height: rect.height,\n left: rect.left - scrollOffset.left,\n top: rect.top - scrollOffset.top,\n right: rect.right - scrollOffset.left,\n bottom: rect.bottom - scrollOffset.top,\n };\n}\n\nfunction isMeaningfulElement(\n element: Element,\n elementRect: SelectModeBoundingRect,\n): boolean {\n const tagName = element.tagName.toLowerCase();\n if ([\"html\", \"body\", \"script\", \"style\", \"meta\", \"link\"].includes(tagName)) {\n return false;\n }\n\n if (isInlineTextMarkElement(element)) {\n return false;\n }\n\n if (elementRect.width < 4 || elementRect.height < 4) {\n return false;\n }\n\n return true;\n}\n\n/**\n * A drag captures an element when their boxes overlap (within\n * `tolerancePx`) \u2014 but NOT when the drag sits entirely inside the\n * element. The latter case is the user dragging *within* a large\n * container (a section, `<main>`, `<body>`) to lasso its children; the\n * container itself isn't a thing they're trying to select, so we drop it.\n */\nexport function doesRectIntersectSelection({\n elementRect,\n selectionRect,\n tolerancePx,\n}: {\n elementRect: SelectModeBoundingRect;\n selectionRect: SelectModeBoundingRect;\n tolerancePx: number;\n}) {\n return (\n isIntersecting(elementRect, selectionRect, { tolerance: tolerancePx }) &&\n !isRectEnclosing(elementRect, selectionRect)\n );\n}\n\n// A meaningful element plus its page-coordinate rect and a stable id,\n// snapshotted once when a drag begins. Page coordinates are scroll-\n// invariant, so per-move hit-testing never needs to re-measure.\nexport type DragCandidate = {\n id: string;\n element: Element;\n pageRect: SelectModeBoundingRect;\n};\n\n/**\n * Snapshots every meaningful element and its page rect when a drag begins.\n * Each candidate gets a stable id so the parent overlay can key its\n * live highlights without remounting them as the drag moves.\n */\nexport function collectDragCandidates(\n scrollOffset: ScrollOffset,\n): DragCandidate[] {\n const candidates: DragCandidate[] = [];\n for (const element of Array.from(document.querySelectorAll(\"*\"))) {\n const viewportRect = getElementBoundingRect(element);\n if (!isMeaningfulElement(element, viewportRect)) {\n continue;\n }\n candidates.push({\n id: uuidv4(),\n element,\n pageRect: getPageRectFromViewportRect({\n rect: viewportRect,\n scrollOffset,\n }),\n });\n }\n return candidates;\n}\n\n/**\n * Drop a child whose parent is also selected and every one of that\n * parent's *candidate* children is selected: the parent already represents\n * the whole group, so the children are redundant. Returns only the\n * candidates that survive the collapse.\n *\n * Only children that are themselves drag candidates count toward \"fully\n * covered\". A parent's DOM children can include decorative / non-selectable\n * elements (and `.children` excludes text nodes); those can never be\n * selected, so counting them would wedge the check permanently false and\n * the collapse would never fire.\n *\n * Membership is read once up front, so the collapse cascades up the tree\n * in a single pass \u2014 a parent kept this round is itself dropped when *its*\n * parent is likewise fully covered, leaving only the topmost ancestor of\n * any fully-enclosed subtree.\n */\nfunction collapseFullyCoveredChildren({\n candidates,\n selected,\n}: {\n candidates: DragCandidate[];\n selected: DragCandidate[];\n}): DragCandidate[] {\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n const selectedElements = new Set(\n selected.map((candidate) => candidate.element),\n );\n return selected.filter((candidate) => {\n const parent = candidate.element.parentElement;\n if (!parent || !selectedElements.has(parent)) {\n return true;\n }\n const everyCandidateChildSelected = Array.from(parent.children)\n .filter((child) => candidateElements.has(child))\n .every((child) => selectedElements.has(child));\n return !everyCandidateChildSelected;\n });\n}\n\n/**\n * Cheap per-move hit-test against the pre-collected candidates: keep the\n * ones whose page rect overlaps the selection (excluding any that wholly\n * enclose it), then collapse any fully-covered parent/children group down\n * to just the parent (see `collapseFullyCoveredChildren`). Does no DOM\n * queries or re-measures of geometry \u2014 the only DOM reads are cheap\n * parent/child structural lookups.\n *\n * Because the collapse depends on which siblings are currently captured,\n * elements legitimately move in and out of the result as the drag grows or\n * shrinks: a child drops out the moment the drag swallows its last sibling\n * (the parent takes over), and reappears if the drag retreats.\n */\nexport function getCandidatesInSelection({\n candidates,\n selectionPageRect,\n}: {\n candidates: DragCandidate[];\n selectionPageRect: SelectModeBoundingRect;\n}): DragCandidate[] {\n const intersecting = candidates.filter((candidate) => {\n return doesRectIntersectSelection({\n elementRect: candidate.pageRect,\n selectionRect: selectionPageRect,\n tolerancePx: SELECTION_TOLERANCE_PX,\n });\n });\n\n return collapseFullyCoveredChildren({ candidates, selected: intersecting });\n}\n\n/**\n * Shift / Command / Ctrl click toggles the clicked element in the current\n * selection (theme-builder canvas rules): a click inside an already-selected\n * ancestor is ignored, selecting a parent drops its selected descendants,\n * and once every candidate child of a candidate parent is selected the\n * parent replaces them \u2014 the same collapse drag-select applies.\n */\nexport function getSelectionAfterAdditiveClick({\n candidates,\n currentSelectedElements,\n targetElement,\n}: {\n candidates: DragCandidate[];\n currentSelectedElements: Element[];\n targetElement: Element;\n}): Element[] {\n if (currentSelectedElements.includes(targetElement)) {\n return currentSelectedElements.filter(\n (element) => element !== targetElement,\n );\n }\n if (\n currentSelectedElements.some((element) => element.contains(targetElement))\n ) {\n return currentSelectedElements;\n }\n\n const selected = new Set(\n currentSelectedElements.filter(\n (element) => !targetElement.contains(element),\n ),\n );\n selected.add(targetElement);\n\n const candidateElements = new Set(\n candidates.map((candidate) => candidate.element),\n );\n let parent = targetElement.parentElement;\n while (parent && candidateElements.has(parent)) {\n const candidateChildren = Array.from(parent.children).filter((child) => {\n return candidateElements.has(child);\n });\n if (\n candidateChildren.length === 0 ||\n !candidateChildren.every((child) => selected.has(child))\n ) {\n break;\n }\n for (const child of candidateChildren) {\n selected.delete(child);\n }\n selected.add(parent);\n parent = parent.parentElement;\n }\n return [...selected];\n}\n\nexport function isAdditiveSelectionClick(event: {\n shiftKey: boolean;\n metaKey: boolean;\n ctrlKey: boolean;\n}): boolean {\n return event.shiftKey || event.metaKey || event.ctrlKey;\n}\n\nexport function getPointRect({\n x,\n y,\n}: {\n x: number;\n y: number;\n}): SelectModeBoundingRect {\n return { x, y, width: 0, height: 0, top: y, right: x, bottom: y, left: x };\n}\n\nexport function unionElementPageRects({\n elements,\n scrollOffset,\n}: {\n elements: Element[];\n scrollOffset: ScrollOffset;\n}): SelectModeBoundingRect | null {\n const rects = elements.map((element) => {\n return getPageRectFromViewportRect({\n rect: getElementBoundingRect(element),\n scrollOffset,\n });\n });\n const firstRect = rects[0];\n if (!firstRect) {\n return null;\n }\n return rects.slice(1).reduce((union, rect) => {\n const left = Math.min(union.left, rect.left);\n const top = Math.min(union.top, rect.top);\n const right = Math.max(union.right, rect.right);\n const bottom = Math.max(union.bottom, rect.bottom);\n return {\n x: left,\n y: top,\n width: right - left,\n height: bottom - top,\n left,\n top,\n right,\n bottom,\n };\n }, firstRect);\n}\n\n/**\n * Picks a \"click position\" for an element that fell inside an area\n * selection. We use the corner of the element nearest the top-left of the\n * selection rect, clamped to the element's own bounds. This anchor is what\n * lets the selected-element overlay continue to track the element when the\n * user scrolls (`useSelectionScrollSync` re-projects relative to it).\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementAnchor({\n element,\n selectionRect,\n}: {\n element: Element;\n selectionRect: SelectModeBoundingRect;\n}): SelectModeClickPosition {\n const rect = element.getBoundingClientRect();\n\n return {\n x: Math.min(Math.max(selectionRect.left, rect.left), rect.right),\n y: Math.min(Math.max(selectionRect.top, rect.top), rect.bottom),\n };\n}\n\n/**\n * @author Kevin O'Connell 2026-04-17\n */\nexport function getElementBoundingRect(\n element: Element,\n): SelectModeBoundingRect {\n const rect = element.getBoundingClientRect();\n\n return {\n x: rect.x,\n y: rect.y,\n width: rect.width,\n height: rect.height,\n top: rect.top,\n right: rect.right,\n bottom: rect.bottom,\n left: rect.left,\n };\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }