@replohq/sandbox-runtime 0.2.0 → 1.0.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 (51) hide show
  1. package/_vendor/schemas/componentPreview.d.ts +68 -0
  2. package/_vendor/schemas/componentPreview.mjs +97 -0
  3. package/_vendor/schemas/generated/componentPreview.d.ts +30 -0
  4. package/_vendor/schemas/generated/componentPreview.mjs +1 -0
  5. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +8 -0
  6. package/_vendor/schemas/loaderKeys.d.ts +57 -0
  7. package/_vendor/schemas/selectModeIframeRpc.d.ts +21 -1
  8. package/_vendor/schemas/selectModeIframeRpc.mjs +11 -3
  9. package/buildMetadata.d.ts +9 -0
  10. package/buildMetadata.js +7 -0
  11. package/buildMetadata.js.map +7 -0
  12. package/next/component-preview-href.d.ts +5 -0
  13. package/next/component-preview-href.js +14 -0
  14. package/next/component-preview-href.js.map +7 -0
  15. package/next/component-preview-navigation.d.ts +1 -0
  16. package/next/component-preview-navigation.js +66 -0
  17. package/next/component-preview-navigation.js.map +7 -0
  18. package/next/component-preview.d.ts +4 -1
  19. package/next/component-preview.js +49 -14
  20. package/next/component-preview.js.map +2 -2
  21. package/package.json +12 -5
  22. package/select-mode/EditorSelection.js +8 -2
  23. package/select-mode/EditorSelection.js.map +2 -2
  24. package/select-mode/fiber.d.ts +12 -17
  25. package/select-mode/fiber.js +228 -64
  26. package/select-mode/fiber.js.map +2 -2
  27. package/select-mode/hooks/useEditModeStateMachine.js +2 -0
  28. package/select-mode/hooks/useEditModeStateMachine.js.map +2 -2
  29. package/select-mode/hooks/useOptimisticOverrides.js +2 -2
  30. package/select-mode/hooks/useOptimisticOverrides.js.map +2 -2
  31. package/select-mode/hooks/useParentMessaging.js +21 -1
  32. package/select-mode/hooks/useParentMessaging.js.map +2 -2
  33. package/select-mode/hooks/useSelectModeController.js +2 -0
  34. package/select-mode/hooks/useSelectModeController.js.map +2 -2
  35. package/select-mode/hooks/useSelectionHover.d.ts +4 -5
  36. package/select-mode/hooks/useSelectionHover.js +21 -3
  37. package/select-mode/hooks/useSelectionHover.js.map +2 -2
  38. package/select-mode/hooks/useSelectionInteractions.js +12 -11
  39. package/select-mode/hooks/useSelectionInteractions.js.map +2 -2
  40. package/select-mode/hooks/useSelectionScrollSync.d.ts +3 -4
  41. package/select-mode/hooks/useSelectionScrollSync.js +4 -9
  42. package/select-mode/hooks/useSelectionScrollSync.js.map +2 -2
  43. package/select-mode/hooks/useSiblingResolution.d.ts +2 -2
  44. package/select-mode/hooks/useSiblingResolution.js +10 -47
  45. package/select-mode/hooks/useSiblingResolution.js.map +2 -2
  46. package/select-mode/iframeRpcChannel.d.ts +7 -1
  47. package/select-mode/selectModeSession.d.ts +1 -0
  48. package/select-mode/selectModeSession.js.map +2 -2
  49. package/select-mode/selectionFrame.d.ts +13 -17
  50. package/select-mode/selectionFrame.js +18 -5
  51. package/select-mode/selectionFrame.js.map +2 -2
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectionHover.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type { DisplayMode } from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport { getNearestSelectableElement } from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { getElementBoundingRect } from \"../interactions/selectionGeometry\";\n\n/**\n * Tracks whichever element is under the user's pointer while in `selection`\n * mode and reports its bounding rect to the parent so the parent can render\n * the hover overlay.\n *\n * Two behaviors worth knowing about:\n * - During an active drag we suppress hover updates; the drag overlay is\n * the only highlight we want visible.\n * - On scroll/wheel/resize we re-run the hover lookup from the last known\n * pointer position (we use rAF to coalesce). Without this, scrolling\n * over an already-hovered element wouldn't update the highlight to the\n * new element actually under the cursor.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionHover({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\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 hoverSyncAnimationFrameId: number | null = null;\n let lastPointerClientPosition: { x: number; y: number } | null = null;\n\n // NOTE (Kevin O'Connell, 2026-04-20): `null` is the deliberate \"no element\n // under pointer / pointer left viewport\" signal \u2014 it produces a\n // clear-hover notification to the parent, not an error.\n const postHoveredElement = (element: Element | null) => {\n // The hover overlay only needs the element rect \u2014 serializing the full\n // styled outerHTML on every pointer move would be wasteful and never\n // consumed by the parent.\n getWebsiteBuilderRpc()?.hoverElement({\n rect: element ? getElementBoundingRect(element) : null,\n });\n };\n\n const syncCurrentHover = (\n pointerClientPosition = lastPointerClientPosition,\n ) => {\n let hoveredElement: Element | null = null;\n\n if (pointerClientPosition) {\n const isPointerWithinViewport =\n pointerClientPosition.x >= 0 &&\n pointerClientPosition.y >= 0 &&\n pointerClientPosition.x <= window.innerWidth &&\n pointerClientPosition.y <= window.innerHeight;\n\n if (isPointerWithinViewport) {\n hoveredElement = document.elementFromPoint(\n pointerClientPosition.x,\n pointerClientPosition.y,\n );\n }\n }\n\n if (!hoveredElement) {\n const hoveredElementStack = document.querySelectorAll(\":hover\");\n hoveredElement =\n hoveredElementStack[hoveredElementStack.length - 1] ?? null;\n }\n\n postHoveredElement(\n hoveredElement ? getNearestSelectableElement(hoveredElement) : null,\n );\n };\n\n const scheduleHoverSync = () => {\n if (\n interactionSessionRef.current.type === \"drag\" ||\n hoverSyncAnimationFrameId !== null\n ) {\n return;\n }\n\n hoverSyncAnimationFrameId = requestAnimationFrame(() => {\n hoverSyncAnimationFrameId = null;\n syncCurrentHover();\n });\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n lastPointerClientPosition = {\n x: event.clientX,\n y: event.clientY,\n };\n\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n syncCurrentHover(lastPointerClientPosition);\n };\n\n // Catches the \"cursor is already inside the iframe when select mode\n // toggles on\" case \u2014 pointermove only fires on motion, so a stationary\n // cursor would otherwise wait forever for the first hover sync.\n const handlePointerOver = (event: PointerEvent) => {\n lastPointerClientPosition = {\n x: event.clientX,\n y: event.clientY,\n };\n\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n syncCurrentHover(lastPointerClientPosition);\n };\n\n const handlePointerOut = (event: PointerEvent) => {\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n const nextRelatedTarget = event.relatedTarget;\n if (\n nextRelatedTarget instanceof Node &&\n document.contains(nextRelatedTarget)\n ) {\n return;\n }\n\n lastPointerClientPosition = null;\n postHoveredElement(null);\n };\n\n const handleViewportChange = () => {\n scheduleHoverSync();\n };\n\n syncCurrentHover();\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerover\", handlePointerOver, true);\n document.addEventListener(\"pointerout\", handlePointerOut, true);\n document.addEventListener(\"scroll\", handleViewportChange, true);\n document.addEventListener(\"wheel\", handleViewportChange, {\n capture: true,\n passive: true,\n });\n window.addEventListener(\"resize\", handleViewportChange);\n\n return () => {\n if (hoverSyncAnimationFrameId !== null) {\n cancelAnimationFrame(hoverSyncAnimationFrameId);\n }\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerover\", handlePointerOver, true);\n document.removeEventListener(\"pointerout\", handlePointerOut, true);\n document.removeEventListener(\"scroll\", handleViewportChange, true);\n document.removeEventListener(\"wheel\", handleViewportChange, {\n capture: true,\n });\n window.removeEventListener(\"resize\", handleViewportChange);\n };\n }, [displayMode, interactionSessionRef]);\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectModeBoundingRect,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport { getNearestSelectableElement } from \"../descriptor/elementDescriptor\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { getElementBoundingRect } from \"../interactions/selectionGeometry\";\n\n// The parent clears its hover overlay without a round-trip when a selection\n// is dismissed (Escape, prompt submit). The rect-dedup cache below must\n// forget its last posted rect at that moment, or hovering the same element\n// again would produce an \"unchanged\" rect that never re-posts and the\n// overlay would stay missing. `resetInteractionSession` calls this on the\n// parent-authoritative clear path.\nlet activeHoverDedupReset: (() => void) | null = null;\n\nexport function resetHoverRectDedup() {\n activeHoverDedupReset?.();\n}\n\n/**\n * Tracks whichever element is under the user's pointer while in `selection`\n * mode and reports its bounding rect to the parent so the parent can render\n * the hover overlay.\n *\n * Two behaviors worth knowing about:\n * - During an active drag we suppress hover updates; the drag overlay is\n * the only highlight we want visible.\n * - Unchanged rectangles do not cross the iframe bridge. Viewport updates\n * are coalesced because scroll and wheel can fire in the same frame.\n *\n * @author Noah Gilmore 2026-07-26\n */\nexport function useSelectionHover({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\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 hoverSyncAnimationFrameId: number | null = null;\n let lastPointerClientPosition: { x: number; y: number } | null = null;\n let lastPostedRect: SelectModeBoundingRect | null | undefined;\n activeHoverDedupReset = () => {\n lastPostedRect = undefined;\n };\n\n // NOTE (Kevin O'Connell, 2026-04-20): `null` is the deliberate \"no element\n // under pointer / pointer left viewport\" signal \u2014 it produces a\n // clear-hover notification to the parent, not an error.\n const postHoveredElement = (element: Element | null) => {\n const rect = element ? getElementBoundingRect(element) : null;\n const isUnchanged =\n lastPostedRect !== undefined &&\n (rect === lastPostedRect ||\n (rect !== null &&\n lastPostedRect !== null &&\n rect.x === lastPostedRect.x &&\n rect.y === lastPostedRect.y &&\n rect.width === lastPostedRect.width &&\n rect.height === lastPostedRect.height));\n if (isUnchanged) {\n return;\n }\n lastPostedRect = rect;\n // The hover overlay only needs the element rect \u2014 serializing the full\n // styled outerHTML on every pointer move would be wasteful and never\n // consumed by the parent.\n getWebsiteBuilderRpc()?.hoverElement({ rect });\n };\n\n const syncCurrentHover = (\n pointerClientPosition = lastPointerClientPosition,\n ) => {\n let hoveredElement: Element | null = null;\n\n if (pointerClientPosition) {\n const isPointerWithinViewport =\n pointerClientPosition.x >= 0 &&\n pointerClientPosition.y >= 0 &&\n pointerClientPosition.x <= window.innerWidth &&\n pointerClientPosition.y <= window.innerHeight;\n\n if (isPointerWithinViewport) {\n hoveredElement = document.elementFromPoint(\n pointerClientPosition.x,\n pointerClientPosition.y,\n );\n }\n }\n\n if (!hoveredElement) {\n const hoveredElementStack = document.querySelectorAll(\":hover\");\n hoveredElement =\n hoveredElementStack[hoveredElementStack.length - 1] ?? null;\n }\n\n postHoveredElement(\n hoveredElement ? getNearestSelectableElement(hoveredElement) : null,\n );\n };\n\n const scheduleHoverSync = () => {\n if (\n interactionSessionRef.current.type === \"drag\" ||\n hoverSyncAnimationFrameId !== null\n ) {\n return;\n }\n\n hoverSyncAnimationFrameId = requestAnimationFrame(() => {\n hoverSyncAnimationFrameId = null;\n syncCurrentHover();\n });\n };\n\n const handlePointerMove = (event: PointerEvent) => {\n lastPointerClientPosition = {\n x: event.clientX,\n y: event.clientY,\n };\n\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n syncCurrentHover(lastPointerClientPosition);\n };\n\n // Catches the \"cursor is already inside the iframe when select mode\n // toggles on\" case \u2014 pointermove only fires on motion, so a stationary\n // cursor would otherwise wait forever for the first hover sync.\n const handlePointerOver = (event: PointerEvent) => {\n lastPointerClientPosition = {\n x: event.clientX,\n y: event.clientY,\n };\n\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n syncCurrentHover(lastPointerClientPosition);\n };\n\n const handlePointerOut = (event: PointerEvent) => {\n if (interactionSessionRef.current.type === \"drag\") {\n return;\n }\n\n const nextRelatedTarget = event.relatedTarget;\n if (\n nextRelatedTarget instanceof Node &&\n document.contains(nextRelatedTarget)\n ) {\n return;\n }\n\n lastPointerClientPosition = null;\n if (hoverSyncAnimationFrameId !== null) {\n cancelAnimationFrame(hoverSyncAnimationFrameId);\n hoverSyncAnimationFrameId = null;\n }\n postHoveredElement(null);\n };\n\n const handleViewportChange = () => {\n scheduleHoverSync();\n };\n\n syncCurrentHover();\n document.addEventListener(\"pointermove\", handlePointerMove, true);\n document.addEventListener(\"pointerover\", handlePointerOver, true);\n document.addEventListener(\"pointerout\", handlePointerOut, true);\n document.addEventListener(\"scroll\", handleViewportChange, true);\n document.addEventListener(\"wheel\", handleViewportChange, {\n capture: true,\n passive: true,\n });\n window.addEventListener(\"resize\", handleViewportChange);\n\n return () => {\n activeHoverDedupReset = null;\n if (hoverSyncAnimationFrameId !== null) {\n cancelAnimationFrame(hoverSyncAnimationFrameId);\n }\n document.removeEventListener(\"pointermove\", handlePointerMove, true);\n document.removeEventListener(\"pointerover\", handlePointerOver, true);\n document.removeEventListener(\"pointerout\", handlePointerOut, true);\n document.removeEventListener(\"scroll\", handleViewportChange, true);\n document.removeEventListener(\"wheel\", handleViewportChange, {\n capture: true,\n });\n window.removeEventListener(\"resize\", handleViewportChange);\n };\n }, [displayMode, interactionSessionRef]);\n}\n"],
5
+ "mappings": "AAOA,SAAS,iBAAiB;AAE1B,SAAS,mCAAmC;AAC5C,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AAQvC,IAAI,wBAA6C;AAE1C,SAAS,sBAAsB;AACpC,0BAAwB;AAC1B;AAeO,SAAS,kBAAkB;AAAA,EAChC;AAAA,EACA;AACF,GAGG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,QAAI,4BAA2C;AAC/C,QAAI,4BAA6D;AACjE,QAAI;AACJ,4BAAwB,MAAM;AAC5B,uBAAiB;AAAA,IACnB;AAKA,UAAM,qBAAqB,CAAC,YAA4B;AACtD,YAAM,OAAO,UAAU,uBAAuB,OAAO,IAAI;AACzD,YAAM,cACJ,mBAAmB,WAClB,SAAS,kBACP,SAAS,QACR,mBAAmB,QACnB,KAAK,MAAM,eAAe,KAC1B,KAAK,MAAM,eAAe,KAC1B,KAAK,UAAU,eAAe,SAC9B,KAAK,WAAW,eAAe;AACrC,UAAI,aAAa;AACf;AAAA,MACF;AACA,uBAAiB;AAIjB,2BAAqB,GAAG,aAAa,EAAE,KAAK,CAAC;AAAA,IAC/C;AAEA,UAAM,mBAAmB,CACvB,wBAAwB,8BACrB;AACH,UAAI,iBAAiC;AAErC,UAAI,uBAAuB;AACzB,cAAM,0BACJ,sBAAsB,KAAK,KAC3B,sBAAsB,KAAK,KAC3B,sBAAsB,KAAK,OAAO,cAClC,sBAAsB,KAAK,OAAO;AAEpC,YAAI,yBAAyB;AAC3B,2BAAiB,SAAS;AAAA,YACxB,sBAAsB;AAAA,YACtB,sBAAsB;AAAA,UACxB;AAAA,QACF;AAAA,MACF;AAEA,UAAI,CAAC,gBAAgB;AACnB,cAAM,sBAAsB,SAAS,iBAAiB,QAAQ;AAC9D,yBACE,oBAAoB,oBAAoB,SAAS,CAAC,KAAK;AAAA,MAC3D;AAEA;AAAA,QACE,iBAAiB,4BAA4B,cAAc,IAAI;AAAA,MACjE;AAAA,IACF;AAEA,UAAM,oBAAoB,MAAM;AAC9B,UACE,sBAAsB,QAAQ,SAAS,UACvC,8BAA8B,MAC9B;AACA;AAAA,MACF;AAEA,kCAA4B,sBAAsB,MAAM;AACtD,oCAA4B;AAC5B,yBAAiB;AAAA,MACnB,CAAC;AAAA,IACH;AAEA,UAAM,oBAAoB,CAAC,UAAwB;AACjD,kCAA4B;AAAA,QAC1B,GAAG,MAAM;AAAA,QACT,GAAG,MAAM;AAAA,MACX;AAEA,UAAI,sBAAsB,QAAQ,SAAS,QAAQ;AACjD;AAAA,MACF;AAEA,uBAAiB,yBAAyB;AAAA,IAC5C;AAKA,UAAM,oBAAoB,CAAC,UAAwB;AACjD,kCAA4B;AAAA,QAC1B,GAAG,MAAM;AAAA,QACT,GAAG,MAAM;AAAA,MACX;AAEA,UAAI,sBAAsB,QAAQ,SAAS,QAAQ;AACjD;AAAA,MACF;AAEA,uBAAiB,yBAAyB;AAAA,IAC5C;AAEA,UAAM,mBAAmB,CAAC,UAAwB;AAChD,UAAI,sBAAsB,QAAQ,SAAS,QAAQ;AACjD;AAAA,MACF;AAEA,YAAM,oBAAoB,MAAM;AAChC,UACE,6BAA6B,QAC7B,SAAS,SAAS,iBAAiB,GACnC;AACA;AAAA,MACF;AAEA,kCAA4B;AAC5B,UAAI,8BAA8B,MAAM;AACtC,6BAAqB,yBAAyB;AAC9C,oCAA4B;AAAA,MAC9B;AACA,yBAAmB,IAAI;AAAA,IACzB;AAEA,UAAM,uBAAuB,MAAM;AACjC,wBAAkB;AAAA,IACpB;AAEA,qBAAiB;AACjB,aAAS,iBAAiB,eAAe,mBAAmB,IAAI;AAChE,aAAS,iBAAiB,eAAe,mBAAmB,IAAI;AAChE,aAAS,iBAAiB,cAAc,kBAAkB,IAAI;AAC9D,aAAS,iBAAiB,UAAU,sBAAsB,IAAI;AAC9D,aAAS,iBAAiB,SAAS,sBAAsB;AAAA,MACvD,SAAS;AAAA,MACT,SAAS;AAAA,IACX,CAAC;AACD,WAAO,iBAAiB,UAAU,oBAAoB;AAEtD,WAAO,MAAM;AACX,8BAAwB;AACxB,UAAI,8BAA8B,MAAM;AACtC,6BAAqB,yBAAyB;AAAA,MAChD;AACA,eAAS,oBAAoB,eAAe,mBAAmB,IAAI;AACnE,eAAS,oBAAoB,eAAe,mBAAmB,IAAI;AACnE,eAAS,oBAAoB,cAAc,kBAAkB,IAAI;AACjE,eAAS,oBAAoB,UAAU,sBAAsB,IAAI;AACjE,eAAS,oBAAoB,SAAS,sBAAsB;AAAA,QAC1D,SAAS;AAAA,MACX,CAAC;AACD,aAAO,oBAAoB,UAAU,oBAAoB;AAAA,IAC3D;AAAA,EACF,GAAG,CAAC,aAAa,qBAAqB,CAAC;AACzC;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  import { useEffect } from "react";
2
2
  import {
3
3
  buildElementDescriptor,
4
+ buildSelector,
4
5
  getNearestSelectableElement
5
6
  } from "../descriptor/elementDescriptor";
6
7
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
@@ -14,11 +15,14 @@ import {
14
15
  getViewportRectFromPageRect,
15
16
  getViewportSelectionRect
16
17
  } from "../interactions/selectionGeometry";
17
- import { showSelectionFrame, showSelectionFrames } from "../selectionFrame";
18
+ import {
19
+ showElementSelectionFrames,
20
+ showSelectionFrames
21
+ } from "../selectionFrame";
18
22
  import { IDLE_SESSION } from "../selectModeSession";
19
23
  import { symbolicateSelectedElement } from "../symbolicate";
20
24
  import { isTextEditableElement } from "./useEditModeStateMachine";
21
- import { getSourceLocationSiblingSelectors } from "./useSiblingResolution";
25
+ import { getRepetitionSiblingElements } from "./useSiblingResolution";
22
26
  const DOUBLE_CLICK_THRESHOLD_MS = 500;
23
27
  async function maybeSymbolicate(data) {
24
28
  return data.kind === "react-element" ? symbolicateSelectedElement(data) : data;
@@ -183,14 +187,10 @@ function useSelectionInteractions({
183
187
  y: pointerEvent.clientY
184
188
  };
185
189
  const scrollOffset = getDocumentScrollOffset();
186
- showSelectionFrame({
187
- variant: "element",
188
- pageRect: {
189
- x: targetRect.left + scrollOffset.left,
190
- y: targetRect.top + scrollOffset.top,
191
- width: targetRect.width,
192
- height: targetRect.height
193
- }
190
+ const siblingElements = selectionElement instanceof HTMLElement ? getRepetitionSiblingElements({ element: selectionElement }) : [];
191
+ showElementSelectionFrames({
192
+ elements: [selectionElement, ...siblingElements],
193
+ scrollOffset
194
194
  });
195
195
  const now = pointerEvent.timeStamp;
196
196
  const previousClick = lastClick;
@@ -205,7 +205,7 @@ function useSelectionInteractions({
205
205
  const data = buildElementDescriptor({
206
206
  element: selectionElement,
207
207
  clickPosition,
208
- siblingSelectors: selectionElement instanceof HTMLElement ? getSourceLocationSiblingSelectors({ element: selectionElement }) : []
208
+ siblingSelectors: siblingElements.map(buildSelector)
209
209
  });
210
210
  lastClick = {
211
211
  descriptorPromise: Promise.resolve(data),
@@ -218,6 +218,7 @@ function useSelectionInteractions({
218
218
  interactionSessionRef.current = {
219
219
  type: "element",
220
220
  element: selectionElement,
221
+ siblingElements,
221
222
  clickPosition: {
222
223
  x: pointerEvent.clientX - targetRect.left,
223
224
  y: pointerEvent.clientY - targetRect.top
@@ -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 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 { showSelectionFrame, showSelectionFrames } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { isTextEditableElement } from \"./useEditModeStateMachine\";\nimport { getSourceLocationSiblingSelectors } 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 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 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 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 frame 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 showSelectionFrame({\n variant: \"element\",\n pageRect: {\n x: targetRect.left + scrollOffset.left,\n y: targetRect.top + scrollOffset.top,\n width: targetRect.width,\n height: targetRect.height,\n },\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:\n selectionElement instanceof HTMLElement\n ? getSourceLocationSiblingSelectors({ element: selectionElement })\n : [],\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 clickPosition: {\n x: pointerEvent.clientX - targetRect.left,\n y: pointerEvent.clientY - targetRect.top,\n },\n data,\n };\n void maybeSymbolicate(data).then((symbolicated) => {\n getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\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 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 = () => {\n resetDragState();\n };\n\n const handleWindowBlur = () => {\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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",
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 { 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 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 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 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 getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });\n });\n };\n\n const handlePointerUp = (event: PointerEvent) => {\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 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 = () => {\n resetDragState();\n };\n\n const handleWindowBlur = () => {\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
  }
@@ -13,14 +13,13 @@ import type { SelectModeInteractionSession } from "../selectModeSession";
13
13
  * — the rest of the descriptor (selector, outerHTML, computed styles,
14
14
  * text metrics, source location) was captured at selection-commit time
15
15
  * and doesn't change while the user is just scrolling. Rebuilding the
16
- * full descriptor here used to call `serializeStyledElement` (recursive
17
- * `getComputedStyle` walk) on every scroll frame; this keeps the hot
18
- * path to a single `getBoundingClientRect` read.
16
+ * full descriptor here would call `serializeStyledElement` recursively.
17
+ * The in-iframe frames only remeasure the selected repetition group.
19
18
  * - For an `area` session we keep the original *page* rect (which doesn't
20
19
  * move) and convert it to viewport coordinates using the latest scroll
21
20
  * offset before sending.
22
21
  *
23
- * @author Kevin O'Connell 2026-04-17
22
+ * @author Noah Gilmore 2026-07-26
24
23
  */
25
24
  export declare function useSelectionScrollSync({ displayMode, interactionSessionRef, }: {
26
25
  displayMode: DisplayMode;
@@ -5,7 +5,7 @@ import {
5
5
  getElementBoundingRect,
6
6
  getViewportRectFromPageRect
7
7
  } from "../interactions/selectionGeometry";
8
- import { showSelectionFrame } from "../selectionFrame";
8
+ import { showElementSelectionFrames } from "../selectionFrame";
9
9
  function useSelectionScrollSync({
10
10
  displayMode,
11
11
  interactionSessionRef
@@ -30,14 +30,9 @@ function useSelectionScrollSync({
30
30
  y: sessionClickPosition !== null ? boundingRect.top + sessionClickPosition.y : boundingRect.bottom
31
31
  };
32
32
  const scrollOffset = getDocumentScrollOffset();
33
- showSelectionFrame({
34
- variant: "element",
35
- pageRect: {
36
- x: boundingRect.left + scrollOffset.left,
37
- y: boundingRect.top + scrollOffset.top,
38
- width: boundingRect.width,
39
- height: boundingRect.height
40
- }
33
+ showElementSelectionFrames({
34
+ elements: [session.element, ...session.siblingElements],
35
+ scrollOffset
41
36
  });
42
37
  getWebsiteBuilderRpc()?.selectedElementRectUpdate({
43
38
  rect: boundingRect,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectionScrollSync.ts"],
4
- "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type { DisplayMode } from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n getDocumentScrollOffset,\n getElementBoundingRect,\n getViewportRectFromPageRect,\n} from \"../interactions/selectionGeometry\";\nimport { showSelectionFrame } from \"../selectionFrame\";\n\n/**\n * After a selection (element or area) has been resolved, the parent's\n * highlight overlay is positioned in viewport coordinates. When the user\n * scrolls the iframe document the highlight would otherwise lag behind the\n * underlying element. This hook re-sends an updated rect on every scroll\n * (rAF-coalesced) so the parent overlay tracks the element/area through\n * scrolls without us having to mirror scroll state into the parent.\n *\n * - For an `element` session we send only `{ boundingRect, clickPosition }`\n * \u2014 the rest of the descriptor (selector, outerHTML, computed styles,\n * text metrics, source location) was captured at selection-commit time\n * and doesn't change while the user is just scrolling. Rebuilding the\n * full descriptor here used to call `serializeStyledElement` (recursive\n * `getComputedStyle` walk) on every scroll frame; this keeps the hot\n * path to a single `getBoundingClientRect` read.\n * - For an `area` session we keep the original *page* rect (which doesn't\n * move) and convert it to viewport coordinates using the latest scroll\n * offset before sending.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useSelectionScrollSync({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\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 animationFrameId: number | null = null;\n\n const handleScroll = () => {\n if (animationFrameId !== null) {\n return;\n }\n\n animationFrameId = requestAnimationFrame(() => {\n animationFrameId = null;\n\n const session = interactionSessionRef.current;\n if (session.type === \"element\") {\n const boundingRect = getElementBoundingRect(session.element);\n const sessionClickPosition = session.clickPosition;\n const clickPosition = {\n x:\n sessionClickPosition !== null\n ? boundingRect.left + sessionClickPosition.x\n : boundingRect.right,\n y:\n sessionClickPosition !== null\n ? boundingRect.top + sessionClickPosition.y\n : boundingRect.bottom,\n };\n // Page coordinates of the in-iframe frame are stable across\n // pure scroll, but layout shifts during scroll (sticky\n // headers expanding, lazy images settling, etc.) can move the\n // selected element. Re-applying every rAF tick keeps the\n // frame glued; same-value style writes are cheap and don't\n // repaint.\n const scrollOffset = getDocumentScrollOffset();\n showSelectionFrame({\n variant: \"element\",\n pageRect: {\n x: boundingRect.left + scrollOffset.left,\n y: boundingRect.top + scrollOffset.top,\n width: boundingRect.width,\n height: boundingRect.height,\n },\n });\n getWebsiteBuilderRpc()?.selectedElementRectUpdate({\n rect: boundingRect,\n clickPosition,\n });\n return;\n }\n\n if (session.type === \"area\") {\n getWebsiteBuilderRpc()?.selectedAreaRectUpdate({\n rect: getViewportRectFromPageRect({\n rect: session.pageRect,\n scrollOffset: getDocumentScrollOffset(),\n }),\n });\n }\n });\n };\n\n document.addEventListener(\"scroll\", handleScroll, true);\n return () => {\n document.removeEventListener(\"scroll\", handleScroll, true);\n if (animationFrameId !== null) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [displayMode, interactionSessionRef]);\n}\n"],
5
- "mappings": "AAIA,SAAS,iBAAiB;AAE1B,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,0BAA0B;AAuB5B,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AACF,GAGG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,QAAI,mBAAkC;AAEtC,UAAM,eAAe,MAAM;AACzB,UAAI,qBAAqB,MAAM;AAC7B;AAAA,MACF;AAEA,yBAAmB,sBAAsB,MAAM;AAC7C,2BAAmB;AAEnB,cAAM,UAAU,sBAAsB;AACtC,YAAI,QAAQ,SAAS,WAAW;AAC9B,gBAAM,eAAe,uBAAuB,QAAQ,OAAO;AAC3D,gBAAM,uBAAuB,QAAQ;AACrC,gBAAM,gBAAgB;AAAA,YACpB,GACE,yBAAyB,OACrB,aAAa,OAAO,qBAAqB,IACzC,aAAa;AAAA,YACnB,GACE,yBAAyB,OACrB,aAAa,MAAM,qBAAqB,IACxC,aAAa;AAAA,UACrB;AAOA,gBAAM,eAAe,wBAAwB;AAC7C,6BAAmB;AAAA,YACjB,SAAS;AAAA,YACT,UAAU;AAAA,cACR,GAAG,aAAa,OAAO,aAAa;AAAA,cACpC,GAAG,aAAa,MAAM,aAAa;AAAA,cACnC,OAAO,aAAa;AAAA,cACpB,QAAQ,aAAa;AAAA,YACvB;AAAA,UACF,CAAC;AACD,+BAAqB,GAAG,0BAA0B;AAAA,YAChD,MAAM;AAAA,YACN;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAEA,YAAI,QAAQ,SAAS,QAAQ;AAC3B,+BAAqB,GAAG,uBAAuB;AAAA,YAC7C,MAAM,4BAA4B;AAAA,cAChC,MAAM,QAAQ;AAAA,cACd,cAAc,wBAAwB;AAAA,YACxC,CAAC;AAAA,UACH,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH;AAEA,aAAS,iBAAiB,UAAU,cAAc,IAAI;AACtD,WAAO,MAAM;AACX,eAAS,oBAAoB,UAAU,cAAc,IAAI;AACzD,UAAI,qBAAqB,MAAM;AAC7B,6BAAqB,gBAAgB;AAAA,MACvC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,aAAa,qBAAqB,CAAC;AACzC;",
4
+ "sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type { DisplayMode } from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useEffect } from \"react\";\n\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport {\n getDocumentScrollOffset,\n getElementBoundingRect,\n getViewportRectFromPageRect,\n} from \"../interactions/selectionGeometry\";\nimport { showElementSelectionFrames } from \"../selectionFrame\";\n\n/**\n * After a selection (element or area) has been resolved, the parent's\n * highlight overlay is positioned in viewport coordinates. When the user\n * scrolls the iframe document the highlight would otherwise lag behind the\n * underlying element. This hook re-sends an updated rect on every scroll\n * (rAF-coalesced) so the parent overlay tracks the element/area through\n * scrolls without us having to mirror scroll state into the parent.\n *\n * - For an `element` session we send only `{ boundingRect, clickPosition }`\n * \u2014 the rest of the descriptor (selector, outerHTML, computed styles,\n * text metrics, source location) was captured at selection-commit time\n * and doesn't change while the user is just scrolling. Rebuilding the\n * full descriptor here would call `serializeStyledElement` recursively.\n * The in-iframe frames only remeasure the selected repetition group.\n * - For an `area` session we keep the original *page* rect (which doesn't\n * move) and convert it to viewport coordinates using the latest scroll\n * offset before sending.\n *\n * @author Noah Gilmore 2026-07-26\n */\nexport function useSelectionScrollSync({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\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 animationFrameId: number | null = null;\n\n const handleScroll = () => {\n if (animationFrameId !== null) {\n return;\n }\n\n animationFrameId = requestAnimationFrame(() => {\n animationFrameId = null;\n\n const session = interactionSessionRef.current;\n if (session.type === \"element\") {\n const boundingRect = getElementBoundingRect(session.element);\n const sessionClickPosition = session.clickPosition;\n const clickPosition = {\n x:\n sessionClickPosition !== null\n ? boundingRect.left + sessionClickPosition.x\n : boundingRect.right,\n y:\n sessionClickPosition !== null\n ? boundingRect.top + sessionClickPosition.y\n : boundingRect.bottom,\n };\n // Page coordinates of the in-iframe frame are stable across\n // pure scroll, but layout shifts during scroll (sticky\n // headers expanding, lazy images settling, etc.) can move the\n // selected element. Re-applying every rAF tick keeps the\n // frame glued; same-value style writes are cheap and don't\n // repaint.\n const scrollOffset = getDocumentScrollOffset();\n showElementSelectionFrames({\n elements: [session.element, ...session.siblingElements],\n scrollOffset,\n });\n getWebsiteBuilderRpc()?.selectedElementRectUpdate({\n rect: boundingRect,\n clickPosition,\n });\n return;\n }\n\n if (session.type === \"area\") {\n getWebsiteBuilderRpc()?.selectedAreaRectUpdate({\n rect: getViewportRectFromPageRect({\n rect: session.pageRect,\n scrollOffset: getDocumentScrollOffset(),\n }),\n });\n }\n });\n };\n\n document.addEventListener(\"scroll\", handleScroll, true);\n return () => {\n document.removeEventListener(\"scroll\", handleScroll, true);\n if (animationFrameId !== null) {\n cancelAnimationFrame(animationFrameId);\n }\n };\n }, [displayMode, interactionSessionRef]);\n}\n"],
5
+ "mappings": "AAIA,SAAS,iBAAiB;AAE1B,SAAS,4BAA4B;AACrC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,kCAAkC;AAsBpC,SAAS,uBAAuB;AAAA,EACrC;AAAA,EACA;AACF,GAGG;AAED,YAAU,MAAM;AACd,QAAI,gBAAgB,aAAa;AAC/B;AAAA,IACF;AAEA,QAAI,mBAAkC;AAEtC,UAAM,eAAe,MAAM;AACzB,UAAI,qBAAqB,MAAM;AAC7B;AAAA,MACF;AAEA,yBAAmB,sBAAsB,MAAM;AAC7C,2BAAmB;AAEnB,cAAM,UAAU,sBAAsB;AACtC,YAAI,QAAQ,SAAS,WAAW;AAC9B,gBAAM,eAAe,uBAAuB,QAAQ,OAAO;AAC3D,gBAAM,uBAAuB,QAAQ;AACrC,gBAAM,gBAAgB;AAAA,YACpB,GACE,yBAAyB,OACrB,aAAa,OAAO,qBAAqB,IACzC,aAAa;AAAA,YACnB,GACE,yBAAyB,OACrB,aAAa,MAAM,qBAAqB,IACxC,aAAa;AAAA,UACrB;AAOA,gBAAM,eAAe,wBAAwB;AAC7C,qCAA2B;AAAA,YACzB,UAAU,CAAC,QAAQ,SAAS,GAAG,QAAQ,eAAe;AAAA,YACtD;AAAA,UACF,CAAC;AACD,+BAAqB,GAAG,0BAA0B;AAAA,YAChD,MAAM;AAAA,YACN;AAAA,UACF,CAAC;AACD;AAAA,QACF;AAEA,YAAI,QAAQ,SAAS,QAAQ;AAC3B,+BAAqB,GAAG,uBAAuB;AAAA,YAC7C,MAAM,4BAA4B;AAAA,cAChC,MAAM,QAAQ;AAAA,cACd,cAAc,wBAAwB;AAAA,YACxC,CAAC;AAAA,UACH,CAAC;AAAA,QACH;AAAA,MACF,CAAC;AAAA,IACH;AAEA,aAAS,iBAAiB,UAAU,cAAc,IAAI;AACtD,WAAO,MAAM;AACX,eAAS,oBAAoB,UAAU,cAAc,IAAI;AACzD,UAAI,qBAAqB,MAAM;AAC7B,6BAAqB,gBAAgB;AAAA,MACvC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,aAAa,qBAAqB,CAAC;AACzC;",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
- export declare function getSourceLocationSiblingElements({ element, }: {
1
+ export declare function getRepetitionSiblingElements({ element, }: {
2
2
  element: Element;
3
3
  }): HTMLElement[];
4
- export declare function getSourceLocationSiblingSelectors({ element, }: {
4
+ export declare function getRepetitionSiblingSelectors({ element, }: {
5
5
  element: Element;
6
6
  }): string[];
@@ -1,59 +1,22 @@
1
1
  import { buildSelector } from "../descriptor/elementDescriptor";
2
- import { getHostSourceIdentity } from "../fiber";
3
- const sourceIdentityCache = /* @__PURE__ */ new WeakMap();
4
- function getSourceIdentityKey(element) {
5
- const cached = sourceIdentityCache.get(element);
6
- if (cached !== void 0) {
7
- return cached;
8
- }
9
- const identity = getHostSourceIdentity(element);
10
- if (!identity) {
11
- sourceIdentityCache.set(element, null);
12
- return null;
13
- }
14
- const key = [
15
- identity.filePath,
16
- identity.lineNumber,
17
- identity.columnNumber ?? "",
18
- identity.tagName
19
- ].join(":");
20
- sourceIdentityCache.set(element, key);
21
- return key;
22
- }
23
- function getSourceLocationSiblingElements({
2
+ import { getRepetitionHostNodes } from "../fiber";
3
+ function getRepetitionSiblingElements({
24
4
  element
25
5
  }) {
26
- if (!(element instanceof HTMLElement) || !document.body) {
6
+ if (!(element instanceof HTMLElement)) {
27
7
  return [];
28
8
  }
29
- const selectedLocationKey = getSourceIdentityKey(element);
30
- if (!selectedLocationKey) {
31
- return [];
32
- }
33
- const siblings = [];
34
- const treeWalker = document.createTreeWalker(
35
- document.body,
36
- NodeFilter.SHOW_ELEMENT
37
- );
38
- let currentNode = treeWalker.nextNode();
39
- while (currentNode) {
40
- if (currentNode instanceof HTMLElement && currentNode !== element) {
41
- const currentLocationKey = getSourceIdentityKey(currentNode);
42
- if (currentLocationKey === selectedLocationKey) {
43
- siblings.push(currentNode);
44
- }
45
- }
46
- currentNode = treeWalker.nextNode();
47
- }
48
- return siblings;
9
+ return getRepetitionHostNodes(element).filter((node) => {
10
+ return node !== element && (node.checkVisibility?.() ?? true);
11
+ });
49
12
  }
50
- function getSourceLocationSiblingSelectors({
13
+ function getRepetitionSiblingSelectors({
51
14
  element
52
15
  }) {
53
- return getSourceLocationSiblingElements({ element }).map(buildSelector);
16
+ return getRepetitionSiblingElements({ element }).map(buildSelector);
54
17
  }
55
18
  export {
56
- getSourceLocationSiblingElements,
57
- getSourceLocationSiblingSelectors
19
+ getRepetitionSiblingElements,
20
+ getRepetitionSiblingSelectors
58
21
  };
59
22
  //# sourceMappingURL=useSiblingResolution.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSiblingResolution.ts"],
4
- "sourcesContent": ["import { buildSelector } from \"../descriptor/elementDescriptor\";\nimport { getHostSourceIdentity } from \"../fiber\";\n\nconst sourceIdentityCache = new WeakMap<HTMLElement, string | null>();\n\nfunction getSourceIdentityKey(element: HTMLElement): string | null {\n const cached = sourceIdentityCache.get(element);\n if (cached !== undefined) {\n return cached;\n }\n const identity = getHostSourceIdentity(element);\n if (!identity) {\n sourceIdentityCache.set(element, null);\n return null;\n }\n const key = [\n identity.filePath,\n identity.lineNumber,\n identity.columnNumber ?? \"\",\n identity.tagName,\n ].join(\":\");\n sourceIdentityCache.set(element, key);\n return key;\n}\n\nexport function getSourceLocationSiblingElements({\n element,\n}: {\n element: Element;\n}): HTMLElement[] {\n if (!(element instanceof HTMLElement) || !document.body) {\n return [];\n }\n\n const selectedLocationKey = getSourceIdentityKey(element);\n if (!selectedLocationKey) {\n return [];\n }\n\n const siblings: HTMLElement[] = [];\n const treeWalker = document.createTreeWalker(\n document.body,\n NodeFilter.SHOW_ELEMENT,\n );\n\n let currentNode = treeWalker.nextNode();\n while (currentNode) {\n if (currentNode instanceof HTMLElement && currentNode !== element) {\n const currentLocationKey = getSourceIdentityKey(currentNode);\n if (currentLocationKey === selectedLocationKey) {\n siblings.push(currentNode);\n }\n }\n currentNode = treeWalker.nextNode();\n }\n\n return siblings;\n}\n\nexport function getSourceLocationSiblingSelectors({\n element,\n}: {\n element: Element;\n}): string[] {\n return getSourceLocationSiblingElements({ element }).map(buildSelector);\n}\n"],
5
- "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,6BAA6B;AAEtC,MAAM,sBAAsB,oBAAI,QAAoC;AAEpE,SAAS,qBAAqB,SAAqC;AACjE,QAAM,SAAS,oBAAoB,IAAI,OAAO;AAC9C,MAAI,WAAW,QAAW;AACxB,WAAO;AAAA,EACT;AACA,QAAM,WAAW,sBAAsB,OAAO;AAC9C,MAAI,CAAC,UAAU;AACb,wBAAoB,IAAI,SAAS,IAAI;AACrC,WAAO;AAAA,EACT;AACA,QAAM,MAAM;AAAA,IACV,SAAS;AAAA,IACT,SAAS;AAAA,IACT,SAAS,gBAAgB;AAAA,IACzB,SAAS;AAAA,EACX,EAAE,KAAK,GAAG;AACV,sBAAoB,IAAI,SAAS,GAAG;AACpC,SAAO;AACT;AAEO,SAAS,iCAAiC;AAAA,EAC/C;AACF,GAEkB;AAChB,MAAI,EAAE,mBAAmB,gBAAgB,CAAC,SAAS,MAAM;AACvD,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,sBAAsB,qBAAqB,OAAO;AACxD,MAAI,CAAC,qBAAqB;AACxB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,WAA0B,CAAC;AACjC,QAAM,aAAa,SAAS;AAAA,IAC1B,SAAS;AAAA,IACT,WAAW;AAAA,EACb;AAEA,MAAI,cAAc,WAAW,SAAS;AACtC,SAAO,aAAa;AAClB,QAAI,uBAAuB,eAAe,gBAAgB,SAAS;AACjE,YAAM,qBAAqB,qBAAqB,WAAW;AAC3D,UAAI,uBAAuB,qBAAqB;AAC9C,iBAAS,KAAK,WAAW;AAAA,MAC3B;AAAA,IACF;AACA,kBAAc,WAAW,SAAS;AAAA,EACpC;AAEA,SAAO;AACT;AAEO,SAAS,kCAAkC;AAAA,EAChD;AACF,GAEa;AACX,SAAO,iCAAiC,EAAE,QAAQ,CAAC,EAAE,IAAI,aAAa;AACxE;",
4
+ "sourcesContent": ["import { buildSelector } from \"../descriptor/elementDescriptor\";\nimport { getRepetitionHostNodes } from \"../fiber\";\n\nexport function getRepetitionSiblingElements({\n element,\n}: {\n element: Element;\n}): HTMLElement[] {\n if (!(element instanceof HTMLElement)) {\n return [];\n }\n\n // Hidden members \u2014 a timed-out Suspense sibling's offscreen subtree, a\n // conditionally `display: none`d item \u2014 would get zero-size selection\n // frames and dead selectors, so only visible members count as siblings.\n return getRepetitionHostNodes(element).filter((node) => {\n return node !== element && (node.checkVisibility?.() ?? true);\n });\n}\n\nexport function getRepetitionSiblingSelectors({\n element,\n}: {\n element: Element;\n}): string[] {\n return getRepetitionSiblingElements({ element }).map(buildSelector);\n}\n"],
5
+ "mappings": "AAAA,SAAS,qBAAqB;AAC9B,SAAS,8BAA8B;AAEhC,SAAS,6BAA6B;AAAA,EAC3C;AACF,GAEkB;AAChB,MAAI,EAAE,mBAAmB,cAAc;AACrC,WAAO,CAAC;AAAA,EACV;AAKA,SAAO,uBAAuB,OAAO,EAAE,OAAO,CAAC,SAAS;AACtD,WAAO,SAAS,YAAY,KAAK,kBAAkB,KAAK;AAAA,EAC1D,CAAC;AACH;AAEO,SAAS,8BAA8B;AAAA,EAC5C;AACF,GAEa;AACX,SAAO,6BAA6B,EAAE,QAAQ,CAAC,EAAE,IAAI,aAAa;AACpE;",
6
6
  "names": []
7
7
  }
@@ -73,7 +73,13 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
73
73
  timeoutMs: number;
74
74
  };
75
75
  iframeReady: {
76
- inputSchema: import("zod").ZodUndefined;
76
+ inputSchema: import("zod").ZodOptional<import("zod").ZodObject<{
77
+ packageName: import("zod").ZodString;
78
+ version: import("zod").ZodString;
79
+ branch: import("zod").ZodNullable<import("zod").ZodString>;
80
+ commit: import("zod").ZodNullable<import("zod").ZodString>;
81
+ builtAt: import("zod").ZodNullable<import("zod").ZodString>;
82
+ }, import("zod/v4/core").$strip>>;
77
83
  outputSchema: import("zod").ZodVoid;
78
84
  };
79
85
  elementSelected: {
@@ -11,6 +11,7 @@ export type SelectModeInteractionSession = {
11
11
  } | {
12
12
  type: Extract<SelectModeSelectionType, "element">;
13
13
  element: Element;
14
+ siblingElements: HTMLElement[];
14
15
  clickPosition: SelectModeClickPosition | null;
15
16
  data: SelectableData;
16
17
  } | {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../select-mode/selectModeSession.ts"],
4
- "sourcesContent": ["import type {\n SelectableData,\n SelectModeBoundingRect,\n SelectModeClickPosition,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type {\n DragPosition,\n ScrollOffset,\n} from \"./interactions/selectionGeometry\";\n\n// NOTE (Kevin O'Connell, 2026-04-22, REPL-26014): SDK-internal liveness\n// state for the active drag/select interaction. Looks like a near-\n// duplicate of agent-ui's `SelectModeSelection` (now in\n// `schemas/selectMode.ts`), but deliberately not lifted into\n// `packages/schemas` for two reasons:\n//\n// 1. The `element` variant carries `element: Element` \u2014 a live DOM\n// ref. Zod has no way to model that; `z.custom<Element>()` would\n// regress to `any` in the generated `.ts` (see the `ZodCustom`\n// branch in `packages/schemas/zod/generateTypeDefinition.ts`),\n// which would lose the strong typing the rest of the SDK relies\n// on when it reads `session.element.getBoundingClientRect()`,\n// etc.\n// 2. There is no second consumer. This type is used only inside\n// `packages/sandbox-runtime/select-mode/`; the parent side\n// (agent-ui) consumes the serialized `SelectModeSelection`\n// instead. Lifting it would move a single-package-internal type\n// into a shared package without any cross-package reader.\n//\n// What we *do* share with the schema is the discriminator vocabulary:\n// the literals come from `SelectModeSelectionType` (generated from\n// `selectModeSelectionTypeSchema` in `schemas/selectMode.ts`), so\n// adding or renaming a selection variant is a TS error here, in\n// `SelectModeSelection`, and in `SelectModeEditSelectionMetadata`.\nexport type SelectModeInteractionSession =\n | { type: Extract<SelectModeSelectionType, \"none\"> }\n | {\n type: Extract<SelectModeSelectionType, \"drag\">;\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n hasExceededThreshold: boolean;\n }\n | {\n type: Extract<SelectModeSelectionType, \"element\">;\n element: Element;\n clickPosition: SelectModeClickPosition | null;\n data: SelectableData;\n }\n | {\n type: Extract<SelectModeSelectionType, \"area\">;\n pageRect: SelectModeBoundingRect;\n items: SelectableData[];\n };\n\nexport const IDLE_SESSION: SelectModeInteractionSession = {\n type: \"none\",\n};\n"],
5
- "mappings": "AAwDO,MAAM,eAA6C;AAAA,EACxD,MAAM;AACR;",
4
+ "sourcesContent": ["import type {\n SelectableData,\n SelectModeBoundingRect,\n SelectModeClickPosition,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type {\n DragPosition,\n ScrollOffset,\n} from \"./interactions/selectionGeometry\";\n\n// NOTE (Kevin O'Connell, 2026-04-22, REPL-26014): SDK-internal liveness\n// state for the active drag/select interaction. Looks like a near-\n// duplicate of agent-ui's `SelectModeSelection` (now in\n// `schemas/selectMode.ts`), but deliberately not lifted into\n// `packages/schemas` for two reasons:\n//\n// 1. The `element` variant carries `element: Element` \u2014 a live DOM\n// ref. Zod has no way to model that; `z.custom<Element>()` would\n// regress to `any` in the generated `.ts` (see the `ZodCustom`\n// branch in `packages/schemas/zod/generateTypeDefinition.ts`),\n// which would lose the strong typing the rest of the SDK relies\n// on when it reads `session.element.getBoundingClientRect()`,\n// etc.\n// 2. There is no second consumer. This type is used only inside\n// `packages/sandbox-runtime/select-mode/`; the parent side\n// (agent-ui) consumes the serialized `SelectModeSelection`\n// instead. Lifting it would move a single-package-internal type\n// into a shared package without any cross-package reader.\n//\n// What we *do* share with the schema is the discriminator vocabulary:\n// the literals come from `SelectModeSelectionType` (generated from\n// `selectModeSelectionTypeSchema` in `schemas/selectMode.ts`), so\n// adding or renaming a selection variant is a TS error here, in\n// `SelectModeSelection`, and in `SelectModeEditSelectionMetadata`.\nexport type SelectModeInteractionSession =\n | { type: Extract<SelectModeSelectionType, \"none\"> }\n | {\n type: Extract<SelectModeSelectionType, \"drag\">;\n dragStart: DragPosition;\n dragCurrent: DragPosition;\n scrollOffset: ScrollOffset;\n hasExceededThreshold: boolean;\n }\n | {\n type: Extract<SelectModeSelectionType, \"element\">;\n element: Element;\n // Live members of the element's repetition group, resolved via fiber\n // traversal at commit time. Kept as DOM refs so scroll sync can\n // re-measure them directly instead of re-resolving CSS selectors\n // against a possibly-mutated document on every frame.\n siblingElements: HTMLElement[];\n clickPosition: SelectModeClickPosition | null;\n data: SelectableData;\n }\n | {\n type: Extract<SelectModeSelectionType, \"area\">;\n pageRect: SelectModeBoundingRect;\n items: SelectableData[];\n };\n\nexport const IDLE_SESSION: SelectModeInteractionSession = {\n type: \"none\",\n};\n"],
5
+ "mappings": "AA6DO,MAAM,eAA6C;AAAA,EACxD,MAAM;AACR;",
6
6
  "names": []
7
7
  }
@@ -16,17 +16,15 @@
16
16
  * 2. No bridge serialization: the only cost when selection commits is
17
17
  * a handful of style writes on the frame nodes.
18
18
  *
19
- * A single click resolves to one frame; an area drag resolves to one
20
- * frame per captured element, so the renderer keeps a pool of nodes
21
- * under a shared container and reconciles it on each call (reusing
22
- * existing nodes when the count is stable, which keeps the single-frame
23
- * scroll-sync hot path a couple of style writes rather than a DOM churn).
19
+ * A click resolves to one frame per repeated element; an area drag resolves
20
+ * to one frame per captured element. The renderer keeps a pool of nodes under
21
+ * a shared container and reconciles it on each call.
24
22
  *
25
23
  * The canopy iframe loads a customer page and doesn't include lattice /
26
24
  * tailwind, so styling is intentionally vanilla inline CSS rather than
27
25
  * Tailwind classes.
28
26
  *
29
- * @author Kevin O'Connell 2026-05-26
27
+ * @author Noah Gilmore 2026-07-26
30
28
  */
31
29
  type SelectionFrameVariant = "element" | "area";
32
30
  type SelectionFramePageRect = {
@@ -57,22 +55,20 @@ export declare function getOrCreateFramesLayerContainer({ id, zIndex, }: {
57
55
  * Position selection frames at the given *page* coordinates, one node per
58
56
  * spec. Page coords don't shift during scroll, so the caller is free to
59
57
  * call this once at commit time and let the browser handle the rest of the
60
- * scroll lifetime. Reconciles the node pool against `frames.length` so a
61
- * repeated single-frame call (scroll sync) just repositions one node.
58
+ * scroll lifetime. Reconciles the node pool against `frames.length` so group
59
+ * updates reuse the existing nodes when the repetition count stays stable.
62
60
  *
63
- * @author Kevin O'Connell 2026-05-26
61
+ * @author Noah Gilmore 2026-07-26
64
62
  */
65
63
  export declare function showSelectionFrames({ frames, }: {
66
64
  frames: SelectionFrameSpec[];
67
65
  }): void;
68
- /**
69
- * Convenience wrapper for the single-frame (element click) case.
70
- *
71
- * @author Kevin O'Connell 2026-05-26
72
- */
73
- export declare function showSelectionFrame({ pageRect, variant, }: {
74
- pageRect: SelectionFramePageRect;
75
- variant: SelectionFrameVariant;
66
+ export declare function showElementSelectionFrames({ elements, scrollOffset, }: {
67
+ elements: Element[];
68
+ scrollOffset: {
69
+ left: number;
70
+ top: number;
71
+ };
76
72
  }): void;
77
73
  /**
78
74
  * @author Kevin O'Connell 2026-05-26
@@ -107,11 +107,24 @@ function showSelectionFrames({
107
107
  frame.style.height = `${spec.pageRect.height}px`;
108
108
  });
109
109
  }
110
- function showSelectionFrame({
111
- pageRect,
112
- variant
110
+ function showElementSelectionFrames({
111
+ elements,
112
+ scrollOffset
113
113
  }) {
114
- showSelectionFrames({ frames: [{ pageRect, variant }] });
114
+ showSelectionFrames({
115
+ frames: elements.map((element) => {
116
+ const rect = element.getBoundingClientRect();
117
+ return {
118
+ variant: "element",
119
+ pageRect: {
120
+ x: rect.left + scrollOffset.left,
121
+ y: rect.top + scrollOffset.top,
122
+ width: rect.width,
123
+ height: rect.height
124
+ }
125
+ };
126
+ })
127
+ });
115
128
  }
116
129
  function hideSelectionFrame() {
117
130
  if (typeof document === "undefined") {
@@ -126,7 +139,7 @@ export {
126
139
  getOrCreateFramesLayerContainer,
127
140
  hideSelectionFrame,
128
141
  setSelectionFrameEditing,
129
- showSelectionFrame,
142
+ showElementSelectionFrames,
130
143
  showSelectionFrames
131
144
  };
132
145
  //# sourceMappingURL=selectionFrame.js.map