@replohq/sandbox-runtime 1.6.7 → 1.7.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 (29) hide show
  1. package/NextErrorReporter.js +15 -14
  2. package/NextErrorReporter.js.map +2 -2
  3. package/_vendor/replo-utils/lib/viewportSnapshot.d.ts +28 -7
  4. package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
  5. package/_vendor/replo-utils/react/use-viewport-snapshot-responder.d.ts +2 -3
  6. package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +18 -2
  7. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +1 -0
  8. package/_vendor/schemas/selectModeIframeRpc.d.ts +2 -0
  9. package/_vendor/schemas/selectModeIframeRpc.mjs +11 -6
  10. package/buildMetadata.js +3 -3
  11. package/{chunk-RXYLMSNM.mjs → chunk-3YA3QXUQ.mjs} +27 -9
  12. package/package.json +4 -4
  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
@@ -4,8 +4,10 @@ import {
4
4
  isAllowedEditorMessage,
5
5
  isAllowedEditorOrigin
6
6
  } from "./annotations/bridgeGate";
7
- const RED_CHROME_SELECTOR = '[data-next-badge][data-error="true"], [data-nextjs-dialog]';
8
- const BLOCKING_ERROR_SELECTOR = "[data-nextjs-dialog]";
7
+ const ERRORED_BADGE_SELECTOR = '[data-next-badge][data-error="true"]';
8
+ const COLLECTED_ISSUE_SELECTOR = "[data-nextjs-dialog]";
9
+ const CLOSED_DIALOG_OVERLAY_SELECTOR = '[data-nextjs-dialog-overlay][data-rendered="false"]';
10
+ const RED_CHROME_SELECTOR = `${ERRORED_BADGE_SELECTOR}, ${COLLECTED_ISSUE_SELECTOR}`;
9
11
  const STATUS_LATCH_MS = 500;
10
12
  function findInitialEditorOrigin() {
11
13
  if (window.self === window.top) {
@@ -37,14 +39,14 @@ function NextErrorReporter() {
37
39
  window.parent.postMessage(
38
40
  {
39
41
  type: "replo:preview:status",
40
- status: lastStatus,
41
- route: window.location.pathname
42
+ route: window.location.pathname,
43
+ status: lastStatus
42
44
  },
43
45
  editorOrigin
44
46
  );
45
47
  };
46
48
  const postNow = (status) => {
47
- if (status === lastStatus) {
49
+ if (lastStatus === status) {
48
50
  return;
49
51
  }
50
52
  lastStatus = status;
@@ -69,11 +71,11 @@ function NextErrorReporter() {
69
71
  window.clearTimeout(pendingPost.timeoutId);
70
72
  pendingPost = null;
71
73
  }
72
- if (status === lastStatus) {
74
+ if (lastStatus === status) {
73
75
  return;
74
76
  }
75
77
  if (lastStatus === null && status === "ok") {
76
- postNow("ok");
78
+ postNow(status);
77
79
  return;
78
80
  }
79
81
  const timeoutId = window.setTimeout(() => {
@@ -95,18 +97,17 @@ function NextErrorReporter() {
95
97
  shadowObserver.observe(root, {
96
98
  childList: true,
97
99
  subtree: true,
100
+ characterData: true,
98
101
  attributes: true,
99
- attributeFilter: ["data-error"]
102
+ attributeFilter: ["data-error", "data-rendered"]
100
103
  });
101
104
  observedRoot = root;
102
105
  }
103
- portal.style.display = portal.shadowRoot?.querySelector(
104
- RED_CHROME_SELECTOR
105
- ) ? "none" : "";
106
- const hasBlockingError = Boolean(
107
- portal.shadowRoot?.querySelector(BLOCKING_ERROR_SELECTOR)
106
+ portal.style.display = root?.querySelector(RED_CHROME_SELECTOR) ? "none" : "";
107
+ const dialog = root?.querySelector(COLLECTED_ISSUE_SELECTOR) ?? null;
108
+ post(
109
+ dialog && !dialog.closest(CLOSED_DIALOG_OVERLAY_SELECTOR) ? "error" : "ok"
108
110
  );
109
- post(hasBlockingError ? "error" : "ok");
110
111
  };
111
112
  const lightObserver = new MutationObserver(syncOverlayState);
112
113
  lightObserver.observe(document.documentElement, {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../NextErrorReporter.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n isAllowedEditorMessage,\n isAllowedEditorOrigin,\n} from \"./annotations/bridgeGate\";\n\n/**\n * NOTE (Max, 2026-06-10): This component sends post messages to its embedder (website-builder miniapp)\n * so that it can render a loading / error UI if there's an error on this page.\n *\n * Ideally, we wouldn't need any update to the sandbox runtime to achieve this: the miniapp would just read the\n * iframe's content and detect whenever there's a nextjs error overlay.\n * But the preview runs cross-origin from the editor, so the editor can't read the iframe's\n * DOM (browser security).\n *\n * This component has observers that detect when the nextjs error overlay appears or\n * disappears, and sends a post message with a status:\n * - `status`: \"error\" while Next's dev overlay opens its blocking error dialog\n * (a real page break \u2014 see the selectors below for why the badge's issue\n * count is deliberately NOT used: it also counts recovered loader misses)\n * - `status`: \"ok\" otherwise. Cleared by Fast Refresh on a fix, so it's also the\n * recovery signal (agent fixed the error, so it's the error -> ok transition)\n *\n * It also hides Next's dev error overlay (`<nextjs-portal>`) at the source the\n * instant an error appears, so the red bubble never paints \u2014 the MutationObserver\n * callback runs as a microtask before paint. `data-error` stays queryable while\n * hidden, so detection is unaffected;\n *\n * The overlay lives in `<nextjs-portal>`'s shadow root, which Next can recreate,\n * so we re-attach the shadow observer whenever it hands us a new root. No polling.\n *\n * This must be mounted OUTSIDE the error boundary (see ReploProvider) so it keeps\n * reporting while the boundary is caught (and showing its neutral cover).\n *\n * I'm unsatisfied with this approach: observing for the nextjs-portal is brittle and ugly.\n * Ideally, the ClientErrorBoundary, which hooks into the render cycle, in componentDidCatch, would\n * just report a post message to the miniapp. I prototyped that, the error detection works\n * great (ok -> error transition).\n * But the error -> ok transition doesn't work for Server components (it only does for Client\n * components). Here's a slack thread with me <> Gabe on it:\n * https://replohq.slack.com/archives/C03AACVP08Y/p1781154057832689?thread_ts=1781029224.564489&cid=C03AACVP08Y.\n */\n// NOTE (Max, 2026-08-04): <nextjs-portal> surfaces two very different things,\n// and only one of them means the page is actually broken:\n// - the dev-tools badge (`[data-next-badge][data-error=\"true\"]`) lights for\n// EVERY collected issue, including a server-side loader error that React\n// already recovered from by streaming the LoaderErrorBoundary fallback. That\n// is a graceful, recoverable miss (product unpublished, deleted collection,\n// \u2026) \u2014 the page renders fine \u2014 and it must NOT be reported as a page error.\n// - the error overlay dialog (`[data-nextjs-dialog]`) only mounts when Next\n// auto-opens the overlay for a genuinely blocking error: the app shell's\n// error boundary catching an uncaught render crash, an unrecoverable SSR\n// error (missing root tags), or a build error. Next never auto-opens the\n// dialog for a recovered error. Verified against next@16.1.7 (the version\n// the platform SDK packages and most templates pin) and next@15.3.8: the sole auto-open\n// is AppDevOverlayErrorBoundary's componentDidCatch -> openErrorOverlay,\n// i.e. the shell boundary, which a nested LoaderErrorBoundary never reaches\n// (a caught loader miss only increments the badge count). The badge count\n// opens the dialog only on an explicit user click.\n// So the open dialog \u2014 not the badge's issue count \u2014 is the fatal-vs-recoverable\n// discriminator. Keying off the badge reported recoverable loader misses as\n// full-page errors, blanking a preview that had actually rendered its fallback.\n//\n// We still *hide* the whole portal whenever Next paints red chrome (the errored\n// badge OR the dialog) so no dev overlay bleeds into the preview \u2014 a recovered\n// loader miss shouldn't flash a red dot either. Only the blocking dialog is\n// reported upward as a page error.\nconst RED_CHROME_SELECTOR =\n '[data-next-badge][data-error=\"true\"], [data-nextjs-dialog]';\nconst BLOCKING_ERROR_SELECTOR = \"[data-nextjs-dialog]\";\n\n// HMR portal churn flips the overlay for milliseconds in both directions; only a state that persists this long is real.\nconst STATUS_LATCH_MS = 500;\n\ntype PreviewStatus = \"ok\" | \"error\";\n\n// The embedder's origin as the browser reports it, trusted only if it is an allowed editor. `ancestorOrigins` names the actual parent and survives referrer policies and in-frame navigations, but Firefox lacks it; the referrer covers Firefox, the same way the annotation layer announces itself.\nfunction findInitialEditorOrigin(): string | null {\n // A top-level page has no editor to report to.\n if (window.self === window.top) {\n return null;\n }\n const ancestorOrigin =\n \"ancestorOrigins\" in window.location\n ? window.location.ancestorOrigins.item(0)\n : null;\n return (\n [ancestorOrigin, getOriginOrNull(document.referrer)].find((origin) => {\n return origin !== null && isAllowedEditorOrigin(origin);\n }) ?? null\n );\n}\n\nfunction getOriginOrNull(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\nexport function NextErrorReporter() {\n // eslint-disable-next-line replo/no-use-effect -- Observes Next's dev error overlay (a shadow-DOM node outside React's tree) and relays its state to the cross-origin embedder. No React alternative applies.\n React.useEffect(() => {\n let editorOrigin = findInitialEditorOrigin();\n let lastStatus: PreviewStatus | null = null;\n let shadowObserver: MutationObserver | null = null;\n let observedRoot: ShadowRoot | null = null;\n let pendingPost: { status: PreviewStatus; timeoutId: number } | null = null;\n\n // Status and route are only for the editor, so they never go out with a wildcard target; without a trusted origin the status is kept and delivered once one is learned.\n const deliver = () => {\n if (editorOrigin === null || lastStatus === null) {\n return;\n }\n window.parent.postMessage(\n {\n type: \"replo:preview:status\",\n status: lastStatus,\n route: window.location.pathname,\n },\n editorOrigin,\n );\n };\n\n const postNow = (status: PreviewStatus) => {\n if (status === lastStatus) {\n return;\n }\n lastStatus = status;\n deliver();\n };\n\n // Covers a missing or stale referrer (Firefox after an in-frame navigation): a message from the parent window at an allowed origin proves where the editor lives, the same capture the annotation layer uses for its replies.\n const handleParentMessage = (event: MessageEvent) => {\n if (event.source !== window.parent || !isAllowedEditorMessage(event)) {\n return;\n }\n if (event.origin === editorOrigin) {\n return;\n }\n editorOrigin = event.origin;\n deliver();\n };\n window.addEventListener(\"message\", handleParentMessage);\n\n // Latch every transition (the opposite observation cancels a pending one) \u2014 a false error <-> ok flip makes the editor reload the preview. Only the initial \"ok\" posts immediately, as the readiness signal.\n const post = (status: PreviewStatus) => {\n if (pendingPost) {\n if (pendingPost.status === status) {\n return;\n }\n window.clearTimeout(pendingPost.timeoutId);\n pendingPost = null;\n }\n if (status === lastStatus) {\n return;\n }\n if (lastStatus === null && status === \"ok\") {\n postNow(\"ok\");\n return;\n }\n const timeoutId = window.setTimeout(() => {\n pendingPost = null;\n postNow(status);\n }, STATUS_LATCH_MS);\n pendingPost = { status, timeoutId };\n };\n\n // NOTE (Max, 2026-06-10): Reads the dev overlay's current error state and\n // propagates it \u2014 re-attach the shadow observer to the live root, hide the\n // portal while erroring, and post ok/error up to the editor. Runs on init and\n // on every observed DOM mutation.\n const syncOverlayState = () => {\n const portal = document.getElementsByTagName(\"nextjs-portal\")[0];\n if (!(portal instanceof HTMLElement)) {\n post(\"ok\");\n return;\n }\n // NOTE (Max, 2026-06-10): Re-attach whenever Next hands us a fresh shadow\n // root, else we'd keep watching a detached one after the overlay is\n // recreated.\n const root = portal.shadowRoot;\n if (root && root !== observedRoot) {\n shadowObserver?.disconnect();\n shadowObserver = new MutationObserver(syncOverlayState);\n shadowObserver.observe(root, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: [\"data-error\"],\n });\n observedRoot = root;\n }\n // NOTE (Max, 2026-06-10): Hide the dev overlay at the source so the red\n // bubble never paints \u2014 for any red chrome, including a recovered miss's\n // errored badge.\n portal.style.display = portal.shadowRoot?.querySelector(\n RED_CHROME_SELECTOR,\n )\n ? \"none\"\n : \"\";\n // Only the blocking overlay dialog means the page actually broke; the\n // badge's issue count also covers recovered loader misses (see selectors).\n const hasBlockingError = Boolean(\n portal.shadowRoot?.querySelector(BLOCKING_ERROR_SELECTOR),\n );\n post(hasBlockingError ? \"error\" : \"ok\");\n };\n\n // NOTE (Max, 2026-06-10): Catches the portal being added/removed in the light\n // DOM; the re-attached shadow observer catches in-overlay changes.\n const lightObserver = new MutationObserver(syncOverlayState);\n lightObserver.observe(document.documentElement, {\n childList: true,\n subtree: true,\n });\n syncOverlayState();\n\n return () => {\n window.removeEventListener(\"message\", handleParentMessage);\n lightObserver.disconnect();\n shadowObserver?.disconnect();\n if (pendingPost) {\n window.clearTimeout(pendingPost.timeoutId);\n }\n };\n }, []);\n\n return null;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\n\nimport {\n isAllowedEditorMessage,\n isAllowedEditorOrigin,\n} from \"./annotations/bridgeGate\";\n\n/**\n * NOTE (Max, 2026-06-10): This component sends post messages to its embedder (website-builder miniapp)\n * so that it can render a loading / error UI if there's an error on this page.\n *\n * Ideally, we wouldn't need any update to the sandbox runtime to achieve this: the miniapp would just read the\n * iframe's content and detect whenever there's a nextjs error overlay.\n * But the preview runs cross-origin from the editor, so the editor can't read the iframe's\n * DOM (browser security).\n *\n * This component has observers that detect when the nextjs error overlay appears or\n * disappears, and sends a post message with a status:\n * - `status`: \"error\" while Next's dev overlay opens its blocking error dialog\n * (a real page break \u2014 see the selectors below for why the badge's issue\n * count is deliberately NOT used: it also counts recovered loader misses)\n * - `status`: \"ok\" otherwise. Cleared by Fast Refresh on a fix, so it's also the\n * recovery signal (agent fixed the error, so it's the error -> ok transition)\n *\n * It also hides Next's dev error overlay (`<nextjs-portal>`) at the source the\n * instant an error appears, so the red bubble never paints \u2014 the MutationObserver\n * callback runs as a microtask before paint. `data-error` stays queryable while\n * hidden, so detection is unaffected;\n *\n * The overlay lives in `<nextjs-portal>`'s shadow root, which Next can recreate,\n * so we re-attach the shadow observer whenever it hands us a new root. No polling.\n *\n * This must be mounted OUTSIDE the error boundary (see ReploProvider) so it keeps\n * reporting while the boundary is caught (and showing its neutral cover).\n *\n * I'm unsatisfied with this approach: observing for the nextjs-portal is brittle and ugly.\n * Ideally, the ClientErrorBoundary, which hooks into the render cycle, in componentDidCatch, would\n * just report a post message to the miniapp. I prototyped that, the error detection works\n * great (ok -> error transition).\n * But the error -> ok transition doesn't work for Server components (it only does for Client\n * components). Here's a slack thread with me <> Gabe on it:\n * https://replohq.slack.com/archives/C03AACVP08Y/p1781154057832689?thread_ts=1781029224.564489&cid=C03AACVP08Y.\n */\n// NOTE (Max, 2026-08-04; Nick, 2026-09-11): Two independent things have to be\n// read off <nextjs-portal>: *what* Next collected, and whether it considers that\n// blocking.\n// - `[data-nextjs-dialog]` holds the text of the collected issue and is\n// present for recoverable issues too, so it is the only description of the\n// problem available. It is NOT evidence the page broke.\n// - the surrounding `[data-nextjs-dialog-overlay]` says whether Next opened\n// the overlay, which it auto-does only for a genuinely blocking error: the\n// app shell's error boundary catching an uncaught render crash, an\n// unrecoverable SSR error (missing root tags), or a build error. Verified\n// against next@16.1.7 and next@15.3.8: the sole auto-open is\n// AppDevOverlayErrorBoundary's componentDidCatch -> openErrorOverlay, which\n// a nested LoaderErrorBoundary never reaches.\n// So an *open* overlay is the fatal-vs-recoverable discriminator. The badge's\n// issue count is not: it lights for every collected issue, including a\n// server-side loader error React already recovered from by streaming the\n// LoaderErrorBoundary fallback (product unpublished, deleted collection, \u2026).\n// Keying off the badge reported those as full-page errors, blanking a preview\n// that had rendered its fallback.\n//\n// Reading \"open\" is version-dependent. next@16.3 keeps the dialog mounted for\n// recoverable issues, parked inside a wrapper it marks `data-rendered=\"false\"`\n// and hides with `display: none !important`; opening flips that to `\"true\"`.\n// Earlier versions mount the dialog only when it is open, so \"has no explicitly\n// closed ancestor\" reads correctly on both.\n//\n// We still *hide* the whole portal whenever Next paints red chrome (the errored\n// badge OR the dialog) so no dev overlay bleeds into the preview \u2014 a recovered\n// loader miss shouldn't flash a red dot either.\nconst ERRORED_BADGE_SELECTOR = '[data-next-badge][data-error=\"true\"]';\nconst COLLECTED_ISSUE_SELECTOR = \"[data-nextjs-dialog]\";\nconst CLOSED_DIALOG_OVERLAY_SELECTOR =\n '[data-nextjs-dialog-overlay][data-rendered=\"false\"]';\nconst RED_CHROME_SELECTOR = `${ERRORED_BADGE_SELECTOR}, ${COLLECTED_ISSUE_SELECTOR}`;\n\n// HMR portal churn flips the overlay for milliseconds in both directions; only a state that persists this long is real.\nconst STATUS_LATCH_MS = 500;\n\ntype PreviewStatus = \"ok\" | \"error\";\n\n// The embedder's origin as the browser reports it, trusted only if it is an allowed editor. `ancestorOrigins` names the actual parent and survives referrer policies and in-frame navigations, but Firefox lacks it; the referrer covers Firefox, the same way the annotation layer announces itself.\nfunction findInitialEditorOrigin(): string | null {\n // A top-level page has no editor to report to.\n if (window.self === window.top) {\n return null;\n }\n const ancestorOrigin =\n \"ancestorOrigins\" in window.location\n ? window.location.ancestorOrigins.item(0)\n : null;\n return (\n [ancestorOrigin, getOriginOrNull(document.referrer)].find((origin) => {\n return origin !== null && isAllowedEditorOrigin(origin);\n }) ?? null\n );\n}\n\nfunction getOriginOrNull(url: string): string | null {\n try {\n return new URL(url).origin;\n } catch {\n return null;\n }\n}\n\nexport function NextErrorReporter() {\n // eslint-disable-next-line replo/no-use-effect -- Observes Next's dev error overlay (a shadow-DOM node outside React's tree) and relays its state to the cross-origin embedder. No React alternative applies.\n React.useEffect(() => {\n let editorOrigin = findInitialEditorOrigin();\n let lastStatus: PreviewStatus | null = null;\n let shadowObserver: MutationObserver | null = null;\n let observedRoot: ShadowRoot | null = null;\n let pendingPost: { status: PreviewStatus; timeoutId: number } | null = null;\n\n // Status and route are only for the editor, so they never go out with a wildcard target; without a trusted origin the status is kept and delivered once one is learned.\n const deliver = () => {\n if (editorOrigin === null || lastStatus === null) {\n return;\n }\n window.parent.postMessage(\n {\n type: \"replo:preview:status\",\n route: window.location.pathname,\n status: lastStatus,\n },\n editorOrigin,\n );\n };\n\n const postNow = (status: PreviewStatus) => {\n if (lastStatus === status) {\n return;\n }\n lastStatus = status;\n deliver();\n };\n\n // Covers a missing or stale referrer (Firefox after an in-frame navigation): a message from the parent window at an allowed origin proves where the editor lives, the same capture the annotation layer uses for its replies.\n const handleParentMessage = (event: MessageEvent) => {\n if (event.source !== window.parent || !isAllowedEditorMessage(event)) {\n return;\n }\n if (event.origin === editorOrigin) {\n return;\n }\n editorOrigin = event.origin;\n deliver();\n };\n window.addEventListener(\"message\", handleParentMessage);\n\n // Latch every transition (the opposite observation cancels a pending one) \u2014 a false error <-> ok flip makes the editor reload the preview. Only the initial \"ok\" posts immediately, as the readiness signal.\n const post = (status: PreviewStatus) => {\n if (pendingPost) {\n if (pendingPost.status === status) {\n return;\n }\n window.clearTimeout(pendingPost.timeoutId);\n pendingPost = null;\n }\n if (lastStatus === status) {\n return;\n }\n if (lastStatus === null && status === \"ok\") {\n postNow(status);\n return;\n }\n const timeoutId = window.setTimeout(() => {\n pendingPost = null;\n postNow(status);\n }, STATUS_LATCH_MS);\n pendingPost = { status, timeoutId };\n };\n\n // NOTE (Max, 2026-06-10): Reads the dev overlay's current error state and\n // propagates it \u2014 re-attach the shadow observer to the live root, hide the\n // portal while erroring, and post ok/error up to the editor. Runs on init and\n // on every observed DOM mutation.\n const syncOverlayState = () => {\n const portal = document.getElementsByTagName(\"nextjs-portal\")[0];\n if (!(portal instanceof HTMLElement)) {\n post(\"ok\");\n return;\n }\n // NOTE (Max, 2026-06-10): Re-attach whenever Next hands us a fresh shadow\n // root, else we'd keep watching a detached one after the overlay is\n // recreated.\n const root = portal.shadowRoot;\n if (root && root !== observedRoot) {\n shadowObserver?.disconnect();\n shadowObserver = new MutationObserver(syncOverlayState);\n shadowObserver.observe(root, {\n childList: true,\n subtree: true,\n characterData: true,\n attributes: true,\n attributeFilter: [\"data-error\", \"data-rendered\"],\n });\n observedRoot = root;\n }\n // NOTE (Max, 2026-06-10): Hide the dev overlay at the source so the red\n // bubble never paints \u2014 for any red chrome, including a recovered miss's\n // errored badge.\n portal.style.display = root?.querySelector(RED_CHROME_SELECTOR)\n ? \"none\"\n : \"\";\n const dialog = root?.querySelector(COLLECTED_ISSUE_SELECTOR) ?? null;\n post(\n dialog && !dialog.closest(CLOSED_DIALOG_OVERLAY_SELECTOR)\n ? \"error\"\n : \"ok\",\n );\n };\n\n // NOTE (Max, 2026-06-10): Catches the portal being added/removed in the light\n // DOM; the re-attached shadow observer catches in-overlay changes.\n const lightObserver = new MutationObserver(syncOverlayState);\n lightObserver.observe(document.documentElement, {\n childList: true,\n subtree: true,\n });\n\n syncOverlayState();\n\n return () => {\n window.removeEventListener(\"message\", handleParentMessage);\n lightObserver.disconnect();\n shadowObserver?.disconnect();\n if (pendingPost) {\n window.clearTimeout(pendingPost.timeoutId);\n }\n };\n }, []);\n\n return null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,12 +1,14 @@
1
1
  export declare const VIEWPORT_SNAPSHOT_REQUEST = "replo:viewport-snapshot:request";
2
2
  export declare const VIEWPORT_SNAPSHOT_RESPONSE = "replo:viewport-snapshot:response";
3
+ export declare const VIEWPORT_CONTEXT_REQUEST = "replo:viewport-context:request";
4
+ export declare const VIEWPORT_CONTEXT_RESPONSE = "replo:viewport-context:response";
3
5
  /**
4
6
  * Levels of iframe nesting a capture request fans out to below the requester.
5
7
  * 2 covers the deepest product surface: agent-ui → miniapp → builder preview.
6
8
  */
7
9
  export declare const VIEWPORT_SNAPSHOT_MAX_DEPTH = 2;
8
10
  export interface ViewportSnapshotRequestMessage {
9
- type: typeof VIEWPORT_SNAPSHOT_REQUEST;
11
+ type: typeof VIEWPORT_SNAPSHOT_REQUEST | typeof VIEWPORT_CONTEXT_REQUEST;
10
12
  requestId: string;
11
13
  frameIndex: number;
12
14
  remainingDepth: number;
@@ -21,6 +23,19 @@ export interface ViewportFrameRect {
21
23
  export interface ViewportFrameCapture {
22
24
  snapshot: unknown;
23
25
  childFrames: ViewportChildFrame[];
26
+ context?: ViewportFrameContext;
27
+ }
28
+ export interface ViewportFrameContext {
29
+ viewport: {
30
+ width: number;
31
+ height: number;
32
+ };
33
+ scroll: {
34
+ x: number;
35
+ y: number;
36
+ maximumX: number;
37
+ maximumY: number;
38
+ };
24
39
  }
25
40
  export interface ViewportChildFrame {
26
41
  frameIndex: number;
@@ -28,7 +43,7 @@ export interface ViewportChildFrame {
28
43
  capture: ViewportFrameCapture;
29
44
  }
30
45
  export interface ViewportSnapshotResponseMessage {
31
- type: typeof VIEWPORT_SNAPSHOT_RESPONSE;
46
+ type: typeof VIEWPORT_SNAPSHOT_RESPONSE | typeof VIEWPORT_CONTEXT_RESPONSE;
32
47
  requestId: string;
33
48
  frameIndex: number;
34
49
  capture: ViewportFrameCapture | null;
@@ -62,24 +77,30 @@ export declare function isTrustedViewportSnapshotRequester(origin: string): bool
62
77
  * at the timeout with whatever arrived — a frame running an older runtime
63
78
  * simply never answers and is omitted.
64
79
  */
65
- export declare function requestChildViewportSnapshots({ targetWindow, requestId, timeoutMs, remainingDepth, }: {
80
+ export declare function requestChildViewportSnapshots({ targetWindow, requestId, timeoutMs, remainingDepth, requestType, }: {
66
81
  targetWindow: RelayWindow;
67
82
  requestId: string;
68
83
  timeoutMs: number;
69
84
  remainingDepth?: number;
85
+ requestType?: ViewportSnapshotRequestMessage["type"];
70
86
  }): Promise<{
71
87
  frameIndex: number;
72
88
  capture: ViewportFrameCapture;
73
89
  }[]>;
90
+ export declare function requestChildViewportContexts(input: Parameters<typeof requestChildViewportSnapshots>[0]): Promise<{
91
+ frameIndex: number;
92
+ capture: ViewportFrameCapture;
93
+ }[]>;
74
94
  /**
75
- * Child-side responder: on a trusted parent request, serialize this frame's
76
- * DOM, cascade the request to this frame's own children while depth allows,
77
- * and post the combined capture back to the requesting origin only.
95
+ * Child-side responder: on a trusted parent request, capture this frame's DOM
96
+ * or lightweight viewing context, cascade to child frames, and answer only the
97
+ * requesting origin.
78
98
  */
79
- export declare function startViewportSnapshotResponder({ targetWindow, takeSnapshot, measureFrameRect, isTrustedRequester, cascadeTimeoutMs, }: {
99
+ export declare function startViewportSnapshotResponder({ targetWindow, takeSnapshot, measureFrameRect, measureViewportContext, isTrustedRequester, cascadeTimeoutMs, }: {
80
100
  targetWindow: RelayWindow;
81
101
  takeSnapshot: () => unknown;
82
102
  measureFrameRect: (frameIndex: number) => ViewportFrameRect | null;
103
+ measureViewportContext?: () => ViewportFrameContext;
83
104
  isTrustedRequester?: (origin: string) => boolean;
84
105
  cascadeTimeoutMs?: number;
85
106
  }): () => void;
@@ -1 +1 @@
1
- export { VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportSnapshots, startViewportSnapshotResponder } from '../../../chunk-RXYLMSNM.mjs';
1
+ export { VIEWPORT_CONTEXT_REQUEST, VIEWPORT_CONTEXT_RESPONSE, VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportContexts, requestChildViewportSnapshots, startViewportSnapshotResponder } from '../../../chunk-3YA3QXUQ.mjs';
@@ -2,8 +2,7 @@ import type { ViewportFrameRect } from "../lib/viewportSnapshot";
2
2
  /** Relay frameIndex → on-screen rect; window.frames order is iframe document order. */
3
3
  export declare function measureViewportFrameRect(frameIndex: number): ViewportFrameRect | null;
4
4
  /**
5
- * Answers a trusted parent frame's turn-end viewport-capture requests with an
6
- * rrweb serialization of this frame's DOM, cascading into this frame's own
7
- * iframes. Passive until asked; see docs/agent-session-viewport-filmstrip-prd.md.
5
+ * Answers trusted parent requests with either an rrweb DOM capture or compact
6
+ * viewport measurements, cascading into this frame's own iframes.
8
7
  */
9
8
  export declare function useViewportSnapshotResponder(): void;
@@ -1,4 +1,4 @@
1
- import { startViewportSnapshotResponder } from '../../../chunk-RXYLMSNM.mjs';
1
+ import { startViewportSnapshotResponder } from '../../../chunk-3YA3QXUQ.mjs';
2
2
  import { useEffect } from 'react';
3
3
 
4
4
  // ../../node_modules/.pnpm/rrweb-snapshot@2.1.1/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js
@@ -5244,6 +5244,21 @@ function measureViewportFrameRect(frameIndex) {
5244
5244
  const { x, y, width, height } = element.getBoundingClientRect();
5245
5245
  return { x, y, width, height };
5246
5246
  }
5247
+ function measureViewportFrameContext() {
5248
+ const viewport = { width: window.innerWidth, height: window.innerHeight };
5249
+ const scrollingElement = document.scrollingElement ?? document.documentElement;
5250
+ const maximumX = Math.max(0, scrollingElement.scrollWidth - viewport.width);
5251
+ const maximumY = Math.max(0, scrollingElement.scrollHeight - viewport.height);
5252
+ return {
5253
+ viewport,
5254
+ scroll: {
5255
+ x: Math.min(maximumX, Math.max(0, Math.round(window.scrollX))),
5256
+ y: Math.min(maximumY, Math.max(0, Math.round(window.scrollY))),
5257
+ maximumX,
5258
+ maximumY
5259
+ }
5260
+ };
5261
+ }
5247
5262
  function useViewportSnapshotResponder() {
5248
5263
  useEffect(() => {
5249
5264
  if (typeof window === "undefined") {
@@ -5254,7 +5269,8 @@ function useViewportSnapshotResponder() {
5254
5269
  takeSnapshot: () => {
5255
5270
  return snapshot(document, { maskAllInputs: true });
5256
5271
  },
5257
- measureFrameRect: measureViewportFrameRect
5272
+ measureFrameRect: measureViewportFrameRect,
5273
+ measureViewportContext: measureViewportFrameContext
5258
5274
  });
5259
5275
  }, []);
5260
5276
  }
@@ -22,6 +22,7 @@ export type SelectModeParentMessage = {
22
22
  method: "setSelectionType";
23
23
  data: {
24
24
  selectionType: SelectModeSelectionType;
25
+ selectors?: string[];
25
26
  };
26
27
  } | {
27
28
  method: "applyOptimisticUpdate";
@@ -216,6 +216,7 @@ export declare const websiteBuilderToCanopyMethodConfig: {
216
216
  drag: "drag";
217
217
  none: "none";
218
218
  }>;
219
+ selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
219
220
  }, z.core.$strip>;
220
221
  outputSchema: z.ZodVoid;
221
222
  };
@@ -714,6 +715,7 @@ export declare const selectModeParentMessageSchema: z.ZodDiscriminatedUnion<[z.Z
714
715
  drag: "drag";
715
716
  none: "none";
716
717
  }>;
718
+ selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
717
719
  }, z.core.$strip>;
718
720
  }, z.core.$strip>, z.ZodObject<{
719
721
  method: z.ZodLiteral<"applyOptimisticUpdate">;
@@ -33,13 +33,18 @@ var websiteBuilderToCanopyMethodConfig = {
33
33
  inputSchema: z.object({ displayMode: displayModeSchema }),
34
34
  outputSchema: z.void()
35
35
  },
36
- // Declarative push of the parent's current selection type into the
37
- // canopy iframe. Transitioning to `none` is canopy's signal to hide
38
- // its in-iframe selection frame and drop any in-progress drag —
39
- // there's no separate "clear" message because selection state always
40
- // flows through agentUiState.
36
+ // Declarative push of the parent's current selection into the canopy
37
+ // iframe. Transitioning to `none` is canopy's signal to hide its
38
+ // in-iframe selection frame and drop any in-progress drag — there's no
39
+ // separate "clear" message because selection state always flows through
40
+ // agentUiState. For an `area` selection the parent may also send the
41
+ // selectors of the members it still holds, so an item removed in the
42
+ // editor is dropped from the in-iframe selection too.
41
43
  setSelectionType: {
42
- inputSchema: z.object({ selectionType: selectModeSelectionTypeSchema }),
44
+ inputSchema: z.object({
45
+ selectionType: selectModeSelectionTypeSchema,
46
+ selectors: z.array(z.string()).optional()
47
+ }),
43
48
  outputSchema: z.void()
44
49
  },
45
50
  applyOptimisticUpdate: {
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
4
4
  "branch": "HEAD",
5
- "commit": "6f35acf4a9b5e632c79b8914aa2b66350bc80485",
6
- "builtAt": "2026-09-14T19:23:28.579Z"
5
+ "commit": "d7576c3808fcbadef1e0cb29b3ea3ed492485a05",
6
+ "builtAt": "2026-09-14T23:38:15.448Z"
7
7
  };
@@ -1,6 +1,8 @@
1
1
  // ../replo-utils/lib/viewportSnapshot.ts
2
2
  var VIEWPORT_SNAPSHOT_REQUEST = "replo:viewport-snapshot:request";
3
3
  var VIEWPORT_SNAPSHOT_RESPONSE = "replo:viewport-snapshot:response";
4
+ var VIEWPORT_CONTEXT_REQUEST = "replo:viewport-context:request";
5
+ var VIEWPORT_CONTEXT_RESPONSE = "replo:viewport-context:response";
4
6
  var VIEWPORT_SNAPSHOT_MAX_DEPTH = 2;
5
7
  function isTrustedViewportSnapshotRequester(origin) {
6
8
  let hostname;
@@ -21,16 +23,17 @@ function canReceiveMessages(value) {
21
23
  return typeof value === "object" && value !== null && "postMessage" in value && typeof value.postMessage === "function";
22
24
  }
23
25
  function isResponseMessage(data) {
24
- return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_RESPONSE && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number";
26
+ return typeof data === "object" && data !== null && "type" in data && (data.type === VIEWPORT_SNAPSHOT_RESPONSE || data.type === VIEWPORT_CONTEXT_RESPONSE) && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number";
25
27
  }
26
28
  function isRequestMessage(data) {
27
- return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_REQUEST && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number" && "remainingDepth" in data && typeof data.remainingDepth === "number";
29
+ return typeof data === "object" && data !== null && "type" in data && (data.type === VIEWPORT_SNAPSHOT_REQUEST || data.type === VIEWPORT_CONTEXT_REQUEST) && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number" && "remainingDepth" in data && typeof data.remainingDepth === "number";
28
30
  }
29
31
  function requestChildViewportSnapshots({
30
32
  targetWindow,
31
33
  requestId,
32
34
  timeoutMs,
33
- remainingDepth = VIEWPORT_SNAPSHOT_MAX_DEPTH
35
+ remainingDepth = VIEWPORT_SNAPSHOT_MAX_DEPTH,
36
+ requestType = VIEWPORT_SNAPSHOT_REQUEST
34
37
  }) {
35
38
  const frameCount = targetWindow.frames.length;
36
39
  if (frameCount === 0 || remainingDepth <= 0) {
@@ -64,7 +67,7 @@ function requestChildViewportSnapshots({
64
67
  const timeoutId = targetWindow.setTimeout(settle, timeoutMs);
65
68
  for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
66
69
  const message = {
67
- type: VIEWPORT_SNAPSHOT_REQUEST,
70
+ type: requestType,
68
71
  requestId,
69
72
  frameIndex,
70
73
  remainingDepth
@@ -73,10 +76,17 @@ function requestChildViewportSnapshots({
73
76
  }
74
77
  });
75
78
  }
79
+ function requestChildViewportContexts(input) {
80
+ return requestChildViewportSnapshots({
81
+ ...input,
82
+ requestType: VIEWPORT_CONTEXT_REQUEST
83
+ });
84
+ }
76
85
  function startViewportSnapshotResponder({
77
86
  targetWindow,
78
87
  takeSnapshot,
79
88
  measureFrameRect,
89
+ measureViewportContext,
80
90
  isTrustedRequester = isTrustedViewportSnapshotRequester,
81
91
  cascadeTimeoutMs = 1500
82
92
  }) {
@@ -89,11 +99,12 @@ function startViewportSnapshotResponder({
89
99
  return;
90
100
  }
91
101
  const { requestId, frameIndex, remainingDepth } = event.data;
102
+ const captureDom = event.data.type === VIEWPORT_SNAPSHOT_REQUEST;
92
103
  const requester = event.source;
93
104
  const requesterOrigin = event.origin ?? "*";
94
105
  const respond = (capture) => {
95
106
  const response = {
96
- type: VIEWPORT_SNAPSHOT_RESPONSE,
107
+ type: captureDom ? VIEWPORT_SNAPSHOT_RESPONSE : VIEWPORT_CONTEXT_RESPONSE,
97
108
  requestId,
98
109
  frameIndex,
99
110
  capture
@@ -101,11 +112,16 @@ function startViewportSnapshotResponder({
101
112
  requester.postMessage(response, requesterOrigin);
102
113
  };
103
114
  let snapshot = null;
115
+ let context;
104
116
  try {
105
- snapshot = takeSnapshot();
117
+ if (!captureDom) {
118
+ context = measureViewportContext?.();
119
+ } else {
120
+ snapshot = takeSnapshot();
121
+ }
106
122
  } catch {
107
123
  }
108
- if (snapshot === null) {
124
+ if (captureDom ? snapshot === null : !context) {
109
125
  respond(null);
110
126
  return;
111
127
  }
@@ -114,10 +130,12 @@ function startViewportSnapshotResponder({
114
130
  // Derived per level so a child's own fan-out can't collide with ours.
115
131
  requestId: `${requestId}:${frameIndex}`,
116
132
  timeoutMs: cascadeTimeoutMs,
117
- remainingDepth: remainingDepth - 1
133
+ remainingDepth: remainingDepth - 1,
134
+ requestType: event.data.type
118
135
  }).then((childCaptures) => {
119
136
  respond({
120
137
  snapshot,
138
+ ...context ? { context } : {},
121
139
  childFrames: childCaptures.flatMap((child) => {
122
140
  const rect = measureFrameRect(child.frameIndex);
123
141
  return rect ? [{ frameIndex: child.frameIndex, rect, capture: child.capture }] : [];
@@ -131,4 +149,4 @@ function startViewportSnapshotResponder({
131
149
  };
132
150
  }
133
151
 
134
- export { VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportSnapshots, startViewportSnapshotResponder };
152
+ export { VIEWPORT_CONTEXT_REQUEST, VIEWPORT_CONTEXT_RESPONSE, VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportContexts, requestChildViewportSnapshots, startViewportSnapshotResponder };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.6.7",
3
+ "version": "1.7.0",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.6.7",
6
+ "version": "1.7.0",
7
7
  "branch": "HEAD",
8
- "commit": "6f35acf4a9b5e632c79b8914aa2b66350bc80485",
9
- "builtAt": "2026-09-14T19:23:28.579Z"
8
+ "commit": "d7576c3808fcbadef1e0cb29b3ea3ed492485a05",
9
+ "builtAt": "2026-09-14T23:38:15.448Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -32,5 +32,8 @@ export declare function useParentMessaging({ applyOptimisticUpdate, markCommitte
32
32
  onFormatCommand?: (command: SelectModeFormatCommand) => void;
33
33
  revertOverride?: (editId: string) => void;
34
34
  setDisplayMode: (displayMode: DisplayMode) => void;
35
- setSelectionType: (selectionType: SelectModeSelectionType) => void;
35
+ setSelectionType: (input: {
36
+ selectionType: SelectModeSelectionType;
37
+ selectors?: string[];
38
+ }) => void;
36
39
  }): void;
@@ -40,8 +40,8 @@ function useParentMessaging({
40
40
  setDisplayMode: ({ displayMode }) => {
41
41
  setDisplayMode(displayMode);
42
42
  },
43
- setSelectionType: ({ selectionType }) => {
44
- setSelectionType(selectionType);
43
+ setSelectionType: (input) => {
44
+ setSelectionType(input);
45
45
  },
46
46
  applyOptimisticUpdate: (message) => {
47
47
  applyOptimisticUpdate?.({
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useParentMessaging.ts"],
4
- "sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\nimport { iframeRuntimeBuildMetadataSchema } from \"schemas/selectModeIframeRpc\";\nimport { z } from \"zod\";\n\nimport { BUILD_METADATA } from \"../../buildMetadata\";\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\nconst SDK_BUILD_METADATA_SPECIFIER = \"@replohq/sdk/lib/buildMetadata\";\n\nconst sdkBuildMetadataModuleSchema = z.object({\n BUILD_METADATA: iframeRuntimeBuildMetadataSchema,\n});\n\nasync function announceSdkBuildMetadata() {\n try {\n const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(\n await import(SDK_BUILD_METADATA_SPECIFIER),\n );\n if (sdkBuildMetadata.success) {\n getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);\n }\n } catch {\n // Sites that haven't installed @replohq/sdk still handshake with sandbox-runtime alone.\n }\n}\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (selectionType: SelectModeSelectionType) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: ({ selectionType }) => {\n setSelectionType(selectionType);\n },\n applyOptimisticUpdate: (message) => {\n applyOptimisticUpdate?.({\n editId: message.editId,\n payload: message.payload,\n targetSelector: message.targetSelector,\n targetMode: message.targetMode,\n });\n },\n markCommitted: ({ editId, sessionId }) => {\n markCommitted?.(editId, sessionId);\n },\n resolveSessionOverrides: ({ sessionId, outcome }) => {\n resolveSessionOverrides?.(sessionId, outcome);\n },\n revertOverride: ({ editId }) => {\n revertOverride?.(editId);\n },\n enterEditMode: () => {\n onEnterEditMode?.();\n },\n exitEditMode: () => {\n onExitEditMode?.();\n },\n formatCommand: ({ command }) => {\n onFormatCommand?.(command);\n },\n });\n const rpc = getWebsiteBuilderRpc();\n rpc?.iframeReady(BUILD_METADATA);\n void announceSdkBuildMetadata();\n\n return () => {\n setSelectModeParentHandlers(null);\n };\n }, [\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n ]);\n}\n"],
5
- "mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,+BAA+B;AAErC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB;AAClB,CAAC;AAED,eAAe,2BAA2B;AACxC,MAAI;AACF,UAAM,mBAAmB,6BAA6B;AAAA,MACpD,MAAM,OAAO;AAAA,IACf;AACA,QAAI,iBAAiB,SAAS;AAC5B,2BAAqB,GAAG,YAAY,iBAAiB,KAAK,cAAc;AAAA,IAC1E;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAqBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAkBG;AACD,sBAAoB,QAAQ,MAAM,8BAA8B,GAAG,CAAC,CAAC,CAAC;AAGtE,YAAU,MAAM;AACd,gCAA4B;AAAA,MAC1B,gBAAgB,CAAC,EAAE,YAAY,MAAM;AACnC,uBAAe,WAAW;AAAA,MAC5B;AAAA,MACA,kBAAkB,CAAC,EAAE,cAAc,MAAM;AACvC,yBAAiB,aAAa;AAAA,MAChC;AAAA,MACA,uBAAuB,CAAC,YAAY;AAClC,gCAAwB;AAAA,UACtB,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,gBAAgB,QAAQ;AAAA,UACxB,YAAY,QAAQ;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,UAAU,MAAM;AACxC,wBAAgB,QAAQ,SAAS;AAAA,MACnC;AAAA,MACA,yBAAyB,CAAC,EAAE,WAAW,QAAQ,MAAM;AACnD,kCAA0B,WAAW,OAAO;AAAA,MAC9C;AAAA,MACA,gBAAgB,CAAC,EAAE,OAAO,MAAM;AAC9B,yBAAiB,MAAM;AAAA,MACzB;AAAA,MACA,eAAe,MAAM;AACnB,0BAAkB;AAAA,MACpB;AAAA,MACA,cAAc,MAAM;AAClB,yBAAiB;AAAA,MACnB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,0BAAkB,OAAO;AAAA,MAC3B;AAAA,IACF,CAAC;AACD,UAAM,MAAM,qBAAqB;AACjC,SAAK,YAAY,cAAc;AAC/B,SAAK,yBAAyB;AAE9B,WAAO,MAAM;AACX,kCAA4B,IAAI;AAAA,IAClC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
4
+ "sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\nimport { iframeRuntimeBuildMetadataSchema } from \"schemas/selectModeIframeRpc\";\nimport { z } from \"zod\";\n\nimport { BUILD_METADATA } from \"../../buildMetadata\";\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\nconst SDK_BUILD_METADATA_SPECIFIER = \"@replohq/sdk/lib/buildMetadata\";\n\nconst sdkBuildMetadataModuleSchema = z.object({\n BUILD_METADATA: iframeRuntimeBuildMetadataSchema,\n});\n\nasync function announceSdkBuildMetadata() {\n try {\n const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(\n await import(SDK_BUILD_METADATA_SPECIFIER),\n );\n if (sdkBuildMetadata.success) {\n getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);\n }\n } catch {\n // Sites that haven't installed @replohq/sdk still handshake with sandbox-runtime alone.\n }\n}\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (input: {\n selectionType: SelectModeSelectionType;\n selectors?: string[];\n }) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: (input) => {\n setSelectionType(input);\n },\n applyOptimisticUpdate: (message) => {\n applyOptimisticUpdate?.({\n editId: message.editId,\n payload: message.payload,\n targetSelector: message.targetSelector,\n targetMode: message.targetMode,\n });\n },\n markCommitted: ({ editId, sessionId }) => {\n markCommitted?.(editId, sessionId);\n },\n resolveSessionOverrides: ({ sessionId, outcome }) => {\n resolveSessionOverrides?.(sessionId, outcome);\n },\n revertOverride: ({ editId }) => {\n revertOverride?.(editId);\n },\n enterEditMode: () => {\n onEnterEditMode?.();\n },\n exitEditMode: () => {\n onExitEditMode?.();\n },\n formatCommand: ({ command }) => {\n onFormatCommand?.(command);\n },\n });\n const rpc = getWebsiteBuilderRpc();\n rpc?.iframeReady(BUILD_METADATA);\n void announceSdkBuildMetadata();\n\n return () => {\n setSelectModeParentHandlers(null);\n };\n }, [\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n ]);\n}\n"],
5
+ "mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,+BAA+B;AAErC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB;AAClB,CAAC;AAED,eAAe,2BAA2B;AACxC,MAAI;AACF,UAAM,mBAAmB,6BAA6B;AAAA,MACpD,MAAM,OAAO;AAAA,IACf;AACA,QAAI,iBAAiB,SAAS;AAC5B,2BAAqB,GAAG,YAAY,iBAAiB,KAAK,cAAc;AAAA,IAC1E;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAqBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAqBG;AACD,sBAAoB,QAAQ,MAAM,8BAA8B,GAAG,CAAC,CAAC,CAAC;AAGtE,YAAU,MAAM;AACd,gCAA4B;AAAA,MAC1B,gBAAgB,CAAC,EAAE,YAAY,MAAM;AACnC,uBAAe,WAAW;AAAA,MAC5B;AAAA,MACA,kBAAkB,CAAC,UAAU;AAC3B,yBAAiB,KAAK;AAAA,MACxB;AAAA,MACA,uBAAuB,CAAC,YAAY;AAClC,gCAAwB;AAAA,UACtB,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,gBAAgB,QAAQ;AAAA,UACxB,YAAY,QAAQ;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,UAAU,MAAM;AACxC,wBAAgB,QAAQ,SAAS;AAAA,MACnC;AAAA,MACA,yBAAyB,CAAC,EAAE,WAAW,QAAQ,MAAM;AACnD,kCAA0B,WAAW,OAAO;AAAA,MAC9C;AAAA,MACA,gBAAgB,CAAC,EAAE,OAAO,MAAM;AAC9B,yBAAiB,MAAM;AAAA,MACzB;AAAA,MACA,eAAe,MAAM;AACnB,0BAAkB;AAAA,MACpB;AAAA,MACA,cAAc,MAAM;AAClB,yBAAiB;AAAA,MACnB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,0BAAkB,OAAO;AAAA,MAC3B;AAAA,IACF,CAAC;AACD,UAAM,MAAM,qBAAqB;AACjC,SAAK,YAAY,cAAc;AAC/B,SAAK,yBAAyB;AAE9B,WAAO,MAAM;AACX,kCAA4B,IAAI;AAAA,IAClC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -4,5 +4,8 @@ export declare function useSelectModeController(): {
4
4
  displayMode: DisplayMode;
5
5
  interactionSessionRef: import("react").RefObject<SelectModeInteractionSession>;
6
6
  setDisplayMode: (nextDisplayMode: DisplayMode) => void;
7
- setSelectionType: (selectionType: SelectModeSelectionType) => void;
7
+ setSelectionType: ({ selectionType, selectors, }: {
8
+ selectionType: SelectModeSelectionType;
9
+ selectors?: string[];
10
+ }) => void;
8
11
  };
@@ -1,7 +1,8 @@
1
1
  import { useCallback, useRef } from "react";
2
2
  import { displayModeStore, useDisplayMode } from "../displayModeStore";
3
3
  import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
4
- import { hideSelectionFrame } from "../selectionFrame";
4
+ import { getDocumentScrollOffset } from "../interactions/selectionGeometry";
5
+ import { hideSelectionFrame, showAreaSelectionFrames } from "../selectionFrame";
5
6
  import { IDLE_SESSION } from "../selectModeSession";
6
7
  import { resetHoverRectDedup } from "./useSelectionHover";
7
8
  function resetInteractionSession({
@@ -22,10 +23,34 @@ function useSelectModeController() {
22
23
  const displayMode = useDisplayMode();
23
24
  const interactionSessionRef = useRef(IDLE_SESSION);
24
25
  const setSelectionType = useCallback(
25
- (selectionType) => {
26
+ ({
27
+ selectionType,
28
+ selectors
29
+ }) => {
26
30
  if (selectionType === "none") {
27
31
  resetInteractionSession({ interactionSessionRef });
32
+ return;
28
33
  }
34
+ const session = interactionSessionRef.current;
35
+ if (selectionType !== "area" || !selectors || session.type !== "area") {
36
+ return;
37
+ }
38
+ const retainedSelectors = new Set(selectors);
39
+ const items = session.items.filter((item) => {
40
+ return item.data.kind !== "react-element" || retainedSelectors.has(item.data.selector);
41
+ });
42
+ if (items.length === session.items.length) {
43
+ return;
44
+ }
45
+ if (items.length === 0) {
46
+ resetInteractionSession({ interactionSessionRef });
47
+ return;
48
+ }
49
+ interactionSessionRef.current = { ...session, items };
50
+ showAreaSelectionFrames({
51
+ elements: items.map((item) => item.element),
52
+ scrollOffset: getDocumentScrollOffset()
53
+ });
29
54
  },
30
55
  []
31
56
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../select-mode/hooks/useSelectModeController.ts"],
4
- "sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { hideSelectionFrame } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { resetHoverRectDedup } from \"./useSelectionHover\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n // The parent wiped its hover overlay as part of this clear; the hover\n // hook's rect dedup must forget its last post or an identical rect would\n // never re-render the overlay.\n resetHoverRectDedup();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n (selectionType: SelectModeSelectionType) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n }\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
5
- "mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,0BAA0B;AACnC,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAQpC,SAAS,wBAAwB;AAAA,EAC/B;AACF,GAEG;AACD,QAAM,iBAAiB,sBAAsB;AAC7C,MAAI,eAAe,SAAS,UAAU,eAAe,sBAAsB;AACzE,yBAAqB,GAAG,gBAAgB;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,CAAC;AAAA,IACnB,CAAC;AAAA,EACH;AACA,qBAAmB;AAInB,sBAAoB;AACpB,wBAAsB,UAAU;AAClC;AAEO,SAAS,0BAA0B;AAGxC,QAAM,cAAc,eAAe;AACnC,QAAM,wBACJ,OAAqC,YAAY;AAQnD,QAAM,mBAAmB;AAAA,IACvB,CAAC,kBAA2C;AAC1C,UAAI,kBAAkB,QAAQ;AAC5B,gCAAwB,EAAE,sBAAsB,CAAC;AAAA,MACnD;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,YAAY,CAAC,oBAAiC;AACnE,QACE,iBAAiB,YAAY,MAAM,eACnC,oBAAoB,aACpB;AACA,8BAAwB,EAAE,sBAAsB,CAAC;AAAA,IACnD;AAEA,qBAAiB,eAAe,eAAe;AAAA,EACjD,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
4
+ "sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { getDocumentScrollOffset } from \"../interactions/selectionGeometry\";\nimport { hideSelectionFrame, showAreaSelectionFrames } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { resetHoverRectDedup } from \"./useSelectionHover\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n // The parent wiped its hover overlay as part of this clear; the hover\n // hook's rect dedup must forget its last post or an identical rect would\n // never re-render the overlay.\n resetHoverRectDedup();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n ({\n selectionType,\n selectors,\n }: {\n selectionType: SelectModeSelectionType;\n selectors?: string[];\n }) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n return;\n }\n const session = interactionSessionRef.current;\n if (selectionType !== \"area\" || !selectors || session.type !== \"area\") {\n return;\n }\n // The editor can remove a single member (the tag's X) without a\n // pointer event ever reaching the iframe, so trim the session to the\n // members the parent still holds.\n const retainedSelectors = new Set(selectors);\n const items = session.items.filter((item) => {\n return (\n item.data.kind !== \"react-element\" ||\n retainedSelectors.has(item.data.selector)\n );\n });\n if (items.length === session.items.length) {\n return;\n }\n if (items.length === 0) {\n resetInteractionSession({ interactionSessionRef });\n return;\n }\n interactionSessionRef.current = { ...session, items };\n showAreaSelectionFrames({\n elements: items.map((item) => item.element),\n scrollOffset: getDocumentScrollOffset(),\n });\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
5
+ "mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AACxC,SAAS,oBAAoB,+BAA+B;AAC5D,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAQpC,SAAS,wBAAwB;AAAA,EAC/B;AACF,GAEG;AACD,QAAM,iBAAiB,sBAAsB;AAC7C,MAAI,eAAe,SAAS,UAAU,eAAe,sBAAsB;AACzE,yBAAqB,GAAG,gBAAgB;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,CAAC;AAAA,IACnB,CAAC;AAAA,EACH;AACA,qBAAmB;AAInB,sBAAoB;AACpB,wBAAsB,UAAU;AAClC;AAEO,SAAS,0BAA0B;AAGxC,QAAM,cAAc,eAAe;AACnC,QAAM,wBACJ,OAAqC,YAAY;AAQnD,QAAM,mBAAmB;AAAA,IACvB,CAAC;AAAA,MACC;AAAA,MACA;AAAA,IACF,MAGM;AACJ,UAAI,kBAAkB,QAAQ;AAC5B,gCAAwB,EAAE,sBAAsB,CAAC;AACjD;AAAA,MACF;AACA,YAAM,UAAU,sBAAsB;AACtC,UAAI,kBAAkB,UAAU,CAAC,aAAa,QAAQ,SAAS,QAAQ;AACrE;AAAA,MACF;AAIA,YAAM,oBAAoB,IAAI,IAAI,SAAS;AAC3C,YAAM,QAAQ,QAAQ,MAAM,OAAO,CAAC,SAAS;AAC3C,eACE,KAAK,KAAK,SAAS,mBACnB,kBAAkB,IAAI,KAAK,KAAK,QAAQ;AAAA,MAE5C,CAAC;AACD,UAAI,MAAM,WAAW,QAAQ,MAAM,QAAQ;AACzC;AAAA,MACF;AACA,UAAI,MAAM,WAAW,GAAG;AACtB,gCAAwB,EAAE,sBAAsB,CAAC;AACjD;AAAA,MACF;AACA,4BAAsB,UAAU,EAAE,GAAG,SAAS,MAAM;AACpD,8BAAwB;AAAA,QACtB,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO;AAAA,QAC1C,cAAc,wBAAwB;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,YAAY,CAAC,oBAAiC;AACnE,QACE,iBAAiB,YAAY,MAAM,eACnC,oBAAoB,aACpB;AACA,8BAAwB,EAAE,sBAAsB,CAAC;AAAA,IACnD;AAEA,qBAAiB,eAAe,eAAe;AAAA,EACjD,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
6
6
  "names": []
7
7
  }