@replohq/sandbox-runtime 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { useEffect } from "react";
3
3
  import { displayModeStore } from "./select-mode/displayModeStore";
4
+ const EXTERNAL_NAVIGATION_HANDLER_KEY = "__reploExternalNavigationHandler";
4
5
  function blockNavigation(event) {
5
6
  event.preventDefault();
6
7
  event.stopPropagation();
@@ -25,14 +26,20 @@ function shouldInterceptNavigationForAgentPreview() {
25
26
  }
26
27
  return new URLSearchParams(window.location.search).get("__reploAgentPreview") === "1";
27
28
  }
28
- function urlLeavesSandbox(rawUrl) {
29
+ function resolveUrl(rawUrl) {
29
30
  try {
30
- const resolved = typeof rawUrl === "string" ? new URL(rawUrl, window.location.href) : new URL(rawUrl.href, window.location.href);
31
- return resolved.origin !== window.location.origin;
31
+ return new URL(
32
+ typeof rawUrl === "string" ? rawUrl : rawUrl.href,
33
+ window.location.href
34
+ );
32
35
  } catch {
33
36
  return null;
34
37
  }
35
38
  }
39
+ function urlLeavesSandbox(rawUrl) {
40
+ const resolved = resolveUrl(rawUrl);
41
+ return resolved === null ? null : resolved.origin !== window.location.origin;
42
+ }
36
43
  function LinkInterceptor() {
37
44
  useEffect(() => {
38
45
  if (!shouldInterceptNavigationForAgentPreview()) {
@@ -43,25 +50,34 @@ function LinkInterceptor() {
43
50
  const originalReplaceState = history.replaceState;
44
51
  const navigation = getWindowNavigation();
45
52
  const reportNavigated = (rawUrl) => {
46
- let route;
47
- try {
48
- route = new URL(
49
- typeof rawUrl === "string" ? rawUrl : rawUrl.href,
50
- window.location.href
51
- ).pathname;
52
- } catch {
53
- route = String(rawUrl);
54
- }
55
53
  window.parent.postMessage(
56
54
  {
57
55
  type: "replo:navigated",
58
- route
56
+ route: resolveUrl(rawUrl)?.pathname ?? String(rawUrl)
57
+ },
58
+ "*"
59
+ );
60
+ };
61
+ const openInParent = (rawUrl) => {
62
+ const resolved = resolveUrl(rawUrl);
63
+ if (!resolved) {
64
+ return;
65
+ }
66
+ window.parent.postMessage(
67
+ {
68
+ type: "replo:open-external",
69
+ url: resolved.toString()
59
70
  },
60
71
  "*"
61
72
  );
62
73
  };
74
+ Reflect.set(window, EXTERNAL_NAVIGATION_HANDLER_KEY, (url) => {
75
+ openInParent(url);
76
+ return true;
77
+ });
63
78
  window.open = function(url, target, features) {
64
79
  if (url !== void 0 && url !== "" && urlLeavesSandbox(url) === true) {
80
+ openInParent(url);
65
81
  return null;
66
82
  }
67
83
  return originalWindowOpen.call(window, url, target, features);
@@ -92,6 +108,7 @@ function LinkInterceptor() {
92
108
  return;
93
109
  }
94
110
  event.preventDefault?.();
111
+ openInParent(destinationUrl);
95
112
  };
96
113
  const handleClick = (event) => {
97
114
  if (displayModeStore.getSnapshot() === "selection") {
@@ -115,10 +132,12 @@ function LinkInterceptor() {
115
132
  const linkTarget = link.getAttribute("target");
116
133
  if (linkTarget && linkTarget !== "_self") {
117
134
  blockNavigation(event);
135
+ openInParent(href);
118
136
  return;
119
137
  }
120
138
  if (urlLeavesSandbox(href) === true) {
121
139
  blockNavigation(event);
140
+ openInParent(href);
122
141
  return;
123
142
  }
124
143
  if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
@@ -137,6 +156,7 @@ function LinkInterceptor() {
137
156
  navigation?.addEventListener("navigate", handleNavigate);
138
157
  document.addEventListener("click", handleClick, true);
139
158
  return () => {
159
+ Reflect.deleteProperty(window, EXTERNAL_NAVIGATION_HANDLER_KEY);
140
160
  window.open = originalWindowOpen;
141
161
  history.pushState = originalPushState;
142
162
  history.replaceState = originalReplaceState;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../LinkInterceptor.tsx"],
4
- "sourcesContent": ["\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { displayModeStore } from \"./select-mode/displayModeStore\";\n\ntype NavigationNavigateEvent = {\n destination?: { url?: string };\n preventDefault?: () => void;\n};\n\ntype NavigationLike = {\n addEventListener: (\n type: \"navigate\",\n listener: (event: NavigationNavigateEvent) => void,\n ) => void;\n removeEventListener: (\n type: \"navigate\",\n listener: (event: NavigationNavigateEvent) => void,\n ) => void;\n};\n\nfunction blockNavigation(event: MouseEvent) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n}\n\nfunction isNavigationLike(value: unknown): value is NavigationLike {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n return (\n typeof Reflect.get(value, \"addEventListener\") === \"function\" &&\n typeof Reflect.get(value, \"removeEventListener\") === \"function\"\n );\n}\n\nfunction getWindowNavigation() {\n const navigation = Reflect.get(window, \"navigation\");\n return isNavigationLike(navigation) ? navigation : null;\n}\n\nfunction shouldInterceptNavigationForAgentPreview() {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n if (window.parent === window) {\n return false;\n }\n\n return (\n new URLSearchParams(window.location.search).get(\"__reploAgentPreview\") ===\n \"1\"\n );\n}\n\nfunction urlLeavesSandbox(rawUrl: string | URL): boolean | null {\n try {\n const resolved =\n typeof rawUrl === \"string\"\n ? new URL(rawUrl, window.location.href)\n : new URL(rawUrl.href, window.location.href);\n return resolved.origin !== window.location.origin;\n } catch {\n return null;\n }\n}\n\nexport default function LinkInterceptor() {\n // eslint-disable-next-line replo/no-use-effect -- Browser navigation APIs must be patched at client runtime and restored when the interceptor unmounts.\n useEffect(() => {\n if (!shouldInterceptNavigationForAgentPreview()) {\n return;\n }\n\n const originalWindowOpen = window.open;\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n const navigation = getWindowNavigation();\n\n // Tell the parent editor where the site's own navigation went so it can\n // keep its URL, page list, and select-mode in sync without reloading the\n // iframe. A fact, not a request: it is only sent once a navigation is\n // actually happening.\n const reportNavigated = (rawUrl: string | URL) => {\n let route: string;\n try {\n route = new URL(\n typeof rawUrl === \"string\" ? rawUrl : rawUrl.href,\n window.location.href,\n ).pathname;\n } catch {\n route = String(rawUrl);\n }\n window.parent.postMessage(\n {\n type: \"replo:navigated\",\n route,\n },\n \"*\",\n );\n };\n\n window.open = function (\n url?: string | URL,\n target?: string,\n features?: string,\n ) {\n if (url !== undefined && url !== \"\" && urlLeavesSandbox(url) === true) {\n return null;\n }\n\n return originalWindowOpen.call(window, url, target, features);\n };\n\n // The history patches are where client-side navigations are reported:\n // next/link cancels its click and routes through pushState, so the click\n // handler below cannot know whether the site actually navigated \u2014 these\n // patches fire exactly when it did (link clicks and programmatic\n // router.push/redirects alike).\n history.pushState = function (\n state: Parameters<History[\"pushState\"]>[0],\n unused: string,\n url?: string | URL | null,\n ) {\n if (url && urlLeavesSandbox(url) === true) {\n return;\n }\n\n const result = originalPushState.call(history, state, unused, url);\n if (url) {\n reportNavigated(url);\n }\n return result;\n };\n\n history.replaceState = function (\n state: Parameters<History[\"replaceState\"]>[0],\n unused: string,\n url?: string | URL | null,\n ) {\n if (url && urlLeavesSandbox(url) === true) {\n return;\n }\n\n const result = originalReplaceState.call(history, state, unused, url);\n if (url) {\n reportNavigated(url);\n }\n return result;\n };\n\n const handleNavigate = (event: NavigationNavigateEvent) => {\n const destinationUrl = event.destination?.url;\n if (\n destinationUrl === undefined ||\n destinationUrl === \"\" ||\n urlLeavesSandbox(destinationUrl) !== true\n ) {\n return;\n }\n\n event.preventDefault?.();\n };\n\n const handleClick = (event: MouseEvent) => {\n // While select mode is active a link click must select the element, not\n // navigate. `useSelectionInteractions` prevents the default nav, but this\n // sibling capture listener would still post `replo:navigate` and move the\n // preview off the page \u2014 `stopPropagation` there can't suppress a\n // listener on the same node, so we bail out here explicitly.\n if (displayModeStore.getSnapshot() === \"selection\") {\n return;\n }\n\n const target = event.target;\n\n if (!(target instanceof Element)) {\n return;\n }\n\n const link = target.closest(\"a[href]\");\n\n if (!(link instanceof HTMLAnchorElement)) {\n return;\n }\n\n const href = link.getAttribute(\"href\");\n if (!href) {\n return;\n }\n\n if (href.startsWith(\"#\")) {\n return;\n }\n\n const linkTarget = link.getAttribute(\"target\");\n if (linkTarget && linkTarget !== \"_self\") {\n blockNavigation(event);\n return;\n }\n\n if (urlLeavesSandbox(href) === true) {\n blockNavigation(event);\n return;\n }\n\n // A modified click (new tab / etc.) is the browser's to handle \u2014 the\n // iframe sandbox has no `allow-popups`, so it can't escape anyway.\n // Don't hijack it or tell the parent we navigated.\n if (\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n\n // A download link never navigates, so there is nothing to report.\n if (link.hasAttribute(\"download\")) {\n return;\n }\n\n // Let the site navigate itself \u2014 next/link does an instant client-side\n // transition, exactly like a normal Next.js app \u2014 instead of cancelling\n // the click and making the parent editor tear down and reload the whole\n // preview iframe. next/link navigations are reported by the pushState\n // patch above; this deferred report covers plain anchors the browser\n // itself navigates (a full document load inside the iframe). Deferred a\n // microtask because this capture listener runs before the site's own\n // handlers: a cancelled click (next/link included) navigates nothing.\n queueMicrotask(() => {\n if (event.defaultPrevented) {\n return;\n }\n reportNavigated(href);\n });\n };\n\n navigation?.addEventListener(\"navigate\", handleNavigate);\n document.addEventListener(\"click\", handleClick, true);\n\n return () => {\n window.open = originalWindowOpen;\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n navigation?.removeEventListener(\"navigate\", handleNavigate);\n document.removeEventListener(\"click\", handleClick, true);\n };\n }, []);\n\n return null;\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\"use client\";\n\nimport { useEffect } from \"react\";\n\nimport { displayModeStore } from \"./select-mode/displayModeStore\";\n\ntype NavigationNavigateEvent = {\n destination?: { url?: string };\n preventDefault?: () => void;\n};\n\ntype NavigationLike = {\n addEventListener: (\n type: \"navigate\",\n listener: (event: NavigationNavigateEvent) => void,\n ) => void;\n removeEventListener: (\n type: \"navigate\",\n listener: (event: NavigationNavigateEvent) => void,\n ) => void;\n};\n\n// NOTE (Max, 2026-08-26): Shared by name with @replohq/sdk's `navigateExternal`\n// (packages/sdk/lib/external-navigation.ts) rather than by import \u2014 the two\n// packages publish independently and must not depend on each other.\nconst EXTERNAL_NAVIGATION_HANDLER_KEY = \"__reploExternalNavigationHandler\";\n\nfunction blockNavigation(event: MouseEvent) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n}\n\nfunction isNavigationLike(value: unknown): value is NavigationLike {\n if (typeof value !== \"object\" || value === null) {\n return false;\n }\n\n return (\n typeof Reflect.get(value, \"addEventListener\") === \"function\" &&\n typeof Reflect.get(value, \"removeEventListener\") === \"function\"\n );\n}\n\nfunction getWindowNavigation() {\n const navigation = Reflect.get(window, \"navigation\");\n return isNavigationLike(navigation) ? navigation : null;\n}\n\nfunction shouldInterceptNavigationForAgentPreview() {\n if (typeof window === \"undefined\") {\n return false;\n }\n\n if (window.parent === window) {\n return false;\n }\n\n return (\n new URLSearchParams(window.location.search).get(\"__reploAgentPreview\") ===\n \"1\"\n );\n}\n\nfunction resolveUrl(rawUrl: string | URL): URL | null {\n try {\n return new URL(\n typeof rawUrl === \"string\" ? rawUrl : rawUrl.href,\n window.location.href,\n );\n } catch {\n return null;\n }\n}\n\nfunction urlLeavesSandbox(rawUrl: string | URL): boolean | null {\n const resolved = resolveUrl(rawUrl);\n return resolved === null ? null : resolved.origin !== window.location.origin;\n}\n\nexport default function LinkInterceptor() {\n // eslint-disable-next-line replo/no-use-effect -- Browser navigation APIs must be patched at client runtime and restored when the interceptor unmounts.\n useEffect(() => {\n if (!shouldInterceptNavigationForAgentPreview()) {\n return;\n }\n\n const originalWindowOpen = window.open;\n const originalPushState = history.pushState;\n const originalReplaceState = history.replaceState;\n const navigation = getWindowNavigation();\n\n // Tell the parent editor where the site's own navigation went so it can\n // keep its URL, page list, and select-mode in sync without reloading the\n // iframe. A fact, not a request: it is only sent once a navigation is\n // actually happening.\n const reportNavigated = (rawUrl: string | URL) => {\n window.parent.postMessage(\n {\n type: \"replo:navigated\",\n route: resolveUrl(rawUrl)?.pathname ?? String(rawUrl),\n },\n \"*\",\n );\n };\n\n // A navigation that leaves the sandbox cannot happen in place: checkout\n // providers refuse to be framed and the iframe has no `allow-popups`. Hand\n // the destination to the editor, which opens it in a real browser tab.\n const openInParent = (rawUrl: string | URL) => {\n const resolved = resolveUrl(rawUrl);\n if (!resolved) {\n return;\n }\n window.parent.postMessage(\n {\n type: \"replo:open-external\",\n url: resolved.toString(),\n },\n \"*\",\n );\n };\n\n Reflect.set(window, EXTERNAL_NAVIGATION_HANDLER_KEY, (url: string) => {\n openInParent(url);\n return true;\n });\n\n window.open = function (\n url?: string | URL,\n target?: string,\n features?: string,\n ) {\n if (url !== undefined && url !== \"\" && urlLeavesSandbox(url) === true) {\n openInParent(url);\n return null;\n }\n\n return originalWindowOpen.call(window, url, target, features);\n };\n\n // The history patches are where client-side navigations are reported:\n // next/link cancels its click and routes through pushState, so the click\n // handler below cannot know whether the site actually navigated \u2014 these\n // patches fire exactly when it did (link clicks and programmatic\n // router.push/redirects alike).\n history.pushState = function (\n state: Parameters<History[\"pushState\"]>[0],\n unused: string,\n url?: string | URL | null,\n ) {\n if (url && urlLeavesSandbox(url) === true) {\n return;\n }\n\n const result = originalPushState.call(history, state, unused, url);\n if (url) {\n reportNavigated(url);\n }\n return result;\n };\n\n history.replaceState = function (\n state: Parameters<History[\"replaceState\"]>[0],\n unused: string,\n url?: string | URL | null,\n ) {\n if (url && urlLeavesSandbox(url) === true) {\n return;\n }\n\n const result = originalReplaceState.call(history, state, unused, url);\n if (url) {\n reportNavigated(url);\n }\n return result;\n };\n\n const handleNavigate = (event: NavigationNavigateEvent) => {\n const destinationUrl = event.destination?.url;\n if (\n destinationUrl === undefined ||\n destinationUrl === \"\" ||\n urlLeavesSandbox(destinationUrl) !== true\n ) {\n return;\n }\n\n event.preventDefault?.();\n openInParent(destinationUrl);\n };\n\n const handleClick = (event: MouseEvent) => {\n // While select mode is active a link click must select the element, not\n // navigate. `useSelectionInteractions` prevents the default nav, but this\n // sibling capture listener would still post `replo:navigate` and move the\n // preview off the page \u2014 `stopPropagation` there can't suppress a\n // listener on the same node, so we bail out here explicitly.\n if (displayModeStore.getSnapshot() === \"selection\") {\n return;\n }\n\n const target = event.target;\n\n if (!(target instanceof Element)) {\n return;\n }\n\n const link = target.closest(\"a[href]\");\n\n if (!(link instanceof HTMLAnchorElement)) {\n return;\n }\n\n const href = link.getAttribute(\"href\");\n if (!href) {\n return;\n }\n\n if (href.startsWith(\"#\")) {\n return;\n }\n\n const linkTarget = link.getAttribute(\"target\");\n if (linkTarget && linkTarget !== \"_self\") {\n blockNavigation(event);\n openInParent(href);\n return;\n }\n\n if (urlLeavesSandbox(href) === true) {\n blockNavigation(event);\n openInParent(href);\n return;\n }\n\n // A modified click (new tab / etc.) is the browser's to handle \u2014 the\n // iframe sandbox has no `allow-popups`, so it can't escape anyway.\n // Don't hijack it or tell the parent we navigated.\n if (\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n ) {\n return;\n }\n\n // A download link never navigates, so there is nothing to report.\n if (link.hasAttribute(\"download\")) {\n return;\n }\n\n // Let the site navigate itself \u2014 next/link does an instant client-side\n // transition, exactly like a normal Next.js app \u2014 instead of cancelling\n // the click and making the parent editor tear down and reload the whole\n // preview iframe. next/link navigations are reported by the pushState\n // patch above; this deferred report covers plain anchors the browser\n // itself navigates (a full document load inside the iframe). Deferred a\n // microtask because this capture listener runs before the site's own\n // handlers: a cancelled click (next/link included) navigates nothing.\n queueMicrotask(() => {\n if (event.defaultPrevented) {\n return;\n }\n reportNavigated(href);\n });\n };\n\n navigation?.addEventListener(\"navigate\", handleNavigate);\n document.addEventListener(\"click\", handleClick, true);\n\n return () => {\n Reflect.deleteProperty(window, EXTERNAL_NAVIGATION_HANDLER_KEY);\n window.open = originalWindowOpen;\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n navigation?.removeEventListener(\"navigate\", handleNavigate);\n document.removeEventListener(\"click\", handleClick, true);\n };\n }, []);\n\n return null;\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -19,6 +19,8 @@ var DATA_LOADER_KEYS = {
19
19
  YOTPO_PRODUCT_REVIEW_AGGREGATE: "yotpo.product_review_aggregate",
20
20
  REVIEWS_IO_PRODUCT_REVIEWS: "reviews_io.product_reviews",
21
21
  REVIEWS_IO_PRODUCT_REVIEW_AGGREGATE: "reviews_io.product_review_aggregate",
22
+ LOOX_PRODUCT_REVIEWS: "loox.product_reviews",
23
+ LOOX_PRODUCT_REVIEW_AGGREGATE: "loox.product_review_aggregate",
22
24
  STATSIG_EXPERIMENT: "statsig.experiment",
23
25
  CONTENTFUL_ENTRY: "contentful.entry",
24
26
  CONTENTFUL_ENTRIES: "contentful.entries"
@@ -28,6 +28,8 @@ export declare const DATA_LOADER_KEYS: {
28
28
  readonly YOTPO_PRODUCT_REVIEW_AGGREGATE: "yotpo.product_review_aggregate";
29
29
  readonly REVIEWS_IO_PRODUCT_REVIEWS: "reviews_io.product_reviews";
30
30
  readonly REVIEWS_IO_PRODUCT_REVIEW_AGGREGATE: "reviews_io.product_review_aggregate";
31
+ readonly LOOX_PRODUCT_REVIEWS: "loox.product_reviews";
32
+ readonly LOOX_PRODUCT_REVIEW_AGGREGATE: "loox.product_review_aggregate";
31
33
  readonly STATSIG_EXPERIMENT: "statsig.experiment";
32
34
  readonly CONTENTFUL_ENTRY: "contentful.entry";
33
35
  readonly CONTENTFUL_ENTRIES: "contentful.entries";
@@ -51,6 +53,8 @@ export declare const dataLoaderKeySchema: z.ZodEnum<{
51
53
  "yotpo.product_review_aggregate": "yotpo.product_review_aggregate";
52
54
  "reviews_io.product_reviews": "reviews_io.product_reviews";
53
55
  "reviews_io.product_review_aggregate": "reviews_io.product_review_aggregate";
56
+ "loox.product_reviews": "loox.product_reviews";
57
+ "loox.product_review_aggregate": "loox.product_review_aggregate";
54
58
  "statsig.experiment": "statsig.experiment";
55
59
  "contentful.entry": "contentful.entry";
56
60
  "contentful.entries": "contentful.entries";
@@ -14,6 +14,8 @@ import { getDocumentScrollOffset } from "../select-mode/interactions/selectionGe
14
14
  import { isAllowedEditorMessage, isAllowedEditorOrigin } from "./bridgeGate";
15
15
  const CLUSTER_DISTANCE_PX = 24;
16
16
  const CLUSTER_FAN_RADIUS_PX = 30;
17
+ const BADGE_RADIUS_PX = 15;
18
+ const BADGE_EDGE_INSET_PX = BADGE_RADIUS_PX + 4;
17
19
  const REMEASURE_DEBOUNCE_MS = 250;
18
20
  const PAGE_TOP_STACK_X_PX = 20;
19
21
  const PAGE_TOP_STACK_Y_PX = 20;
@@ -130,6 +132,21 @@ function clusterResolvedMarkers(resolved) {
130
132
  }
131
133
  return clusters;
132
134
  }
135
+ function clampBadgeCenter({
136
+ x,
137
+ y,
138
+ isFixed
139
+ }) {
140
+ const width = isFixed ? window.innerWidth : Math.max(document.documentElement.scrollWidth, window.innerWidth);
141
+ const height = isFixed ? window.innerHeight : Math.max(document.documentElement.scrollHeight, window.innerHeight);
142
+ const clamp = (value, limit) => {
143
+ return Math.min(
144
+ Math.max(value, BADGE_EDGE_INSET_PX),
145
+ Math.max(BADGE_EDGE_INSET_PX, limit - BADGE_EDGE_INSET_PX)
146
+ );
147
+ };
148
+ return { x: clamp(x, width), y: clamp(y, height) };
149
+ }
133
150
  function getMostSevereColorHex(members) {
134
151
  const color = COLOR_SEVERITY.find((candidate) => {
135
152
  return members.some((member) => member.marker.color === candidate);
@@ -152,7 +169,7 @@ function buildMemberSpec({
152
169
  return {
153
170
  id: marker.id,
154
171
  pageRect: member.rect,
155
- badgeCenter,
172
+ badgeCenter: clampBadgeCenter({ ...badgeCenter, isFixed: member.isFixed }),
156
173
  positioning: member.isFixed ? "fixed" : "absolute",
157
174
  colorHex: COLOR_HEX[marker.color],
158
175
  badgeLabel: String(member.displayNumber),
@@ -182,7 +199,11 @@ function buildClusterSpecs(cluster) {
182
199
  {
183
200
  id: `cluster:${cluster.id}`,
184
201
  pageRect: hub,
185
- badgeCenter: { x: cluster.centerX, y: cluster.centerY },
202
+ badgeCenter: clampBadgeCenter({
203
+ x: cluster.centerX,
204
+ y: cluster.centerY,
205
+ isFixed: cluster.isFixed
206
+ }),
186
207
  positioning: cluster.isFixed ? "fixed" : "absolute",
187
208
  colorHex: getMostSevereColorHex(cluster.members),
188
209
  badgeLabel: String(cluster.members.length),
@@ -209,7 +230,11 @@ function buildClusterSpecs(cluster) {
209
230
  {
210
231
  id: `cluster:${cluster.id}`,
211
232
  pageRect: hub,
212
- badgeCenter: { x: cluster.centerX, y: cluster.centerY },
233
+ badgeCenter: clampBadgeCenter({
234
+ x: cluster.centerX,
235
+ y: cluster.centerY,
236
+ isFixed: cluster.isFixed
237
+ }),
213
238
  positioning: cluster.isFixed ? "fixed" : "absolute",
214
239
  colorHex: "#94a3b8",
215
240
  badgeLabel: "\xD7",
@@ -320,6 +345,14 @@ function syncObservers() {
320
345
  resizeObserver = null;
321
346
  mutationObserver?.disconnect();
322
347
  mutationObserver = null;
348
+ if (remeasureDebounceId !== null) {
349
+ clearTimeout(remeasureDebounceId);
350
+ remeasureDebounceId = null;
351
+ }
352
+ if (remeasureFrameId !== null) {
353
+ cancelAnimationFrame(remeasureFrameId);
354
+ remeasureFrameId = null;
355
+ }
323
356
  }
324
357
  }
325
358
  function handleMessage(event) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../annotations/annotationController.ts"],
4
- "sourcesContent": ["/**\n * In-iframe annotation controller: consumes declarative marker sets from the\n * editor bridge, resolves selectors against the live DOM, and drives the\n * shared annotation frame renderer. Scroll costs zero JavaScript (frames use\n * page coordinates); re-measure runs only on resize/mutation/container\n * scroll, batched per animation frame. Data-only protocol \u2014 the editor can\n * describe dots, never execute code.\n */\nimport type {\n PageAnnotationMarker,\n PageAnnotationsIframeMessage,\n} from \"schemas/generated/pageAnnotations\";\nimport type { AnnotationFrameSpec } from \"../select-mode/annotationFrames\";\n\nimport { exhaustiveSwitch } from \"replo-utils/lib/misc\";\nimport {\n PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n pageAnnotationsParentMessageSchema,\n} from \"schemas/pageAnnotations\";\n\nimport {\n ANNOTATION_CONTAINER_ID,\n clearAnnotationFrames,\n getAnnotationBadgeViewportRect,\n setActiveAnnotationBadge,\n showAnnotationFrames,\n} from \"../select-mode/annotationFrames\";\nimport { getDocumentScrollOffset } from \"../select-mode/interactions/selectionGeometry\";\nimport { isAllowedEditorMessage, isAllowedEditorOrigin } from \"./bridgeGate\";\n\nconst CLUSTER_DISTANCE_PX = 24;\nconst CLUSTER_FAN_RADIUS_PX = 30;\nconst REMEASURE_DEBOUNCE_MS = 250;\n// Page-wide findings stack in a row near the top-left of the page and scroll\n// with it like any other annotation.\nconst PAGE_TOP_STACK_X_PX = 20;\nconst PAGE_TOP_STACK_Y_PX = 20;\nconst PAGE_TOP_STACK_GAP_PX = 36;\n\n// Replo brand palette (lattice --color-brand-*): the wire enum names stay\n// red/purple/amber but the rendered hues are brand magenta/purple/blue,\n// matching the editor-side dots and cards.\nconst COLOR_HEX: Record<PageAnnotationMarker[\"color\"], string> = {\n red: \"#ef196e\",\n purple: \"#8b32a8\",\n amber: \"#274ae2\",\n};\nconst COLOR_SEVERITY: PageAnnotationMarker[\"color\"][] = [\n \"red\",\n \"purple\",\n \"amber\",\n];\n\ninterface ResolvedMarker {\n marker: PageAnnotationMarker;\n element: Element | null;\n isFixed: boolean;\n rect: { x: number; y: number; width: number; height: number };\n // The editor-assigned wire number, rendered verbatim.\n displayNumber: number;\n}\n\ninterface MarkerCluster {\n id: string;\n centerX: number;\n centerY: number;\n isFixed: boolean;\n members: ResolvedMarker[];\n}\n\nlet isInitialized = false;\nlet editorOrigin: string | null = null;\nlet markers: PageAnnotationMarker[] = [];\nlet setId: string | null = null;\nlet expandedClusterId: string | null = null;\nlet activeMarkerId: string | null = null;\nlet remeasureFrameId: number | null = null;\nlet remeasureDebounceId: ReturnType<typeof setTimeout> | null = null;\nlet activeRectFrameId: number | null = null;\nlet observersActive = false;\nlet lastRenderSignature: string | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet mutationObserver: MutationObserver | null = null;\n\nfunction postToEditor(message: PageAnnotationsIframeMessage) {\n if (editorOrigin === null) {\n return;\n }\n window.parent.postMessage(message, editorOrigin);\n}\n\nfunction hasFixedAncestor(element: Element): boolean {\n let current: Element | null = element;\n while (current !== null && current !== document.documentElement) {\n const position = getComputedStyle(current).position;\n if (position === \"fixed\" || position === \"sticky\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\nfunction resolveMarkers(): {\n resolved: ResolvedMarker[];\n unresolvedIds: string[];\n} {\n const resolved: ResolvedMarker[] = [];\n const unresolvedIds: string[] = [];\n let pageTopSlot = 0;\n for (const marker of markers) {\n if (marker.placement === \"pageTop\") {\n // Page-wide finding: a fixed slot in the top-of-page stack, no selector.\n resolved.push({\n marker,\n element: null,\n isFixed: false,\n rect: {\n x: PAGE_TOP_STACK_X_PX + pageTopSlot * PAGE_TOP_STACK_GAP_PX,\n y: PAGE_TOP_STACK_Y_PX,\n width: 0,\n height: 0,\n },\n // The editor owns numbering; rendering the wire number keeps dots in sync with the panel.\n displayNumber: marker.number,\n });\n pageTopSlot += 1;\n continue;\n }\n let element: Element | null = null;\n try {\n element = document.querySelector(marker.selector);\n } catch {\n // Malformed selector \u2014 unresolved, never a throw.\n }\n const domRect = element?.getBoundingClientRect();\n if (!element || !domRect || (domRect.width === 0 && domRect.height === 0)) {\n unresolvedIds.push(marker.id);\n continue;\n }\n const isFixed = hasFixedAncestor(element);\n const scrollOffset = getDocumentScrollOffset();\n resolved.push({\n marker,\n element,\n isFixed,\n // Fixed frames use viewport coords; absolute frames use page coords.\n rect: {\n x: domRect.x + (isFixed ? 0 : scrollOffset.left),\n y: domRect.y + (isFixed ? 0 : scrollOffset.top),\n width: domRect.width,\n height: domRect.height,\n },\n displayNumber: marker.number,\n });\n }\n return { resolved, unresolvedIds };\n}\n\n// Greedy: a marker joins the first cluster whose badge anchor lies within\n// 24px; fixed and absolute markers never co-cluster (their coordinate spaces\n// diverge as soon as the page scrolls).\nfunction clusterResolvedMarkers(resolved: ResolvedMarker[]): MarkerCluster[] {\n const clusters: MarkerCluster[] = [];\n for (const entry of resolved) {\n const centerX = entry.rect.x + entry.rect.width / 2;\n const centerY = entry.rect.y;\n const host = clusters.find((cluster) => {\n return (\n cluster.isFixed === entry.isFixed &&\n Math.hypot(cluster.centerX - centerX, cluster.centerY - centerY) <\n CLUSTER_DISTANCE_PX\n );\n });\n if (host) {\n host.members.push(entry);\n } else {\n clusters.push({\n id: entry.marker.id,\n centerX,\n centerY,\n isFixed: entry.isFixed,\n members: [entry],\n });\n }\n }\n return clusters;\n}\n\nfunction getMostSevereColorHex(members: ResolvedMarker[]): string {\n const color =\n COLOR_SEVERITY.find((candidate) => {\n return members.some((member) => member.marker.color === candidate);\n }) ?? \"amber\";\n return COLOR_HEX[color];\n}\n\nfunction activateMarker(id: string) {\n activeMarkerId = id;\n const viewportRect = getAnnotationBadgeViewportRect(id);\n if (viewportRect) {\n postToEditor({ type: \"replo:annotations:clicked\", id, viewportRect });\n }\n // Style-only: a rebuild here would race the pointer sequence and swallow\n // the very click being handled.\n setActiveAnnotationBadge(id);\n}\n\nfunction buildMemberSpec({\n member,\n badgeCenter,\n}: {\n member: ResolvedMarker;\n badgeCenter: { x: number; y: number };\n}): AnnotationFrameSpec {\n const marker = member.marker;\n return {\n id: marker.id,\n pageRect: member.rect,\n badgeCenter,\n positioning: member.isFixed ? \"fixed\" : \"absolute\",\n colorHex: COLOR_HEX[marker.color],\n badgeLabel: String(member.displayNumber),\n badgeAriaLabel: `Page issue ${member.displayNumber}`,\n isActive: activeMarkerId === marker.id,\n onBadgeClick: () => activateMarker(marker.id),\n };\n}\n\nfunction buildClusterSpecs(cluster: MarkerCluster): AnnotationFrameSpec[] {\n const [only] = cluster.members;\n if (cluster.members.length === 1 && only) {\n return [\n buildMemberSpec({\n member: only,\n badgeCenter: { x: cluster.centerX, y: cluster.centerY },\n }),\n ];\n }\n const hub = {\n x: cluster.centerX,\n y: cluster.centerY,\n width: 0,\n height: 0,\n };\n if (expandedClusterId !== cluster.id) {\n return [\n {\n id: `cluster:${cluster.id}`,\n pageRect: hub,\n badgeCenter: { x: cluster.centerX, y: cluster.centerY },\n positioning: cluster.isFixed ? \"fixed\" : \"absolute\",\n colorHex: getMostSevereColorHex(cluster.members),\n badgeLabel: String(cluster.members.length),\n badgeAriaLabel: `${cluster.members.length} page issues near this element`,\n isActive: false,\n onBadgeClick: () => {\n expandedClusterId = cluster.id;\n render();\n },\n },\n ];\n }\n const memberSpecs = cluster.members.map((member, index) => {\n const angle = (index / cluster.members.length) * 2 * Math.PI - Math.PI / 2;\n return buildMemberSpec({\n member,\n badgeCenter: {\n x: cluster.centerX + Math.cos(angle) * CLUSTER_FAN_RADIUS_PX,\n y: cluster.centerY + Math.sin(angle) * CLUSTER_FAN_RADIUS_PX,\n },\n });\n });\n return [\n {\n id: `cluster:${cluster.id}`,\n pageRect: hub,\n badgeCenter: { x: cluster.centerX, y: cluster.centerY },\n positioning: cluster.isFixed ? \"fixed\" : \"absolute\",\n colorHex: \"#94a3b8\",\n badgeLabel: \"\u00D7\",\n badgeAriaLabel: \"Collapse issue cluster\",\n isActive: false,\n onBadgeClick: () => {\n expandedClusterId = null;\n render();\n },\n },\n ...memberSpecs,\n ];\n}\n\nfunction render() {\n if (markers.length === 0) {\n lastRenderSignature = null;\n clearAnnotationFrames();\n syncObservers();\n return;\n }\n const { resolved, unresolvedIds } = resolveMarkers();\n if (activeMarkerId !== null && unresolvedIds.includes(activeMarkerId)) {\n activeMarkerId = null;\n }\n // Rebuilding the node pool mid-pointer-sequence swallows clicks, so skip\n // renders whose output would be identical to what is already on screen.\n const signature = [\n resolved\n .map((entry) => {\n return `${entry.marker.id}:${Math.round(entry.rect.x)},${Math.round(entry.rect.y)}`;\n })\n .join(\"|\"),\n `active:${activeMarkerId ?? \"\"}`,\n `expanded:${expandedClusterId ?? \"\"}`,\n `unresolved:${unresolvedIds.join(\",\")}`,\n ].join(\"#\");\n if (signature === lastRenderSignature) {\n return;\n }\n lastRenderSignature = signature;\n const frames = clusterResolvedMarkers(resolved).flatMap((cluster) => {\n return buildClusterSpecs(cluster);\n });\n showAnnotationFrames({ frames });\n if (setId !== null) {\n postToEditor({\n type: \"replo:annotations:unresolved\",\n setId,\n ids: unresolvedIds,\n });\n }\n syncObservers();\n}\n\nfunction scheduleRemeasure() {\n remeasureFrameId ??= requestAnimationFrame(() => {\n remeasureFrameId = null;\n render();\n });\n}\n\nfunction scheduleDebouncedRemeasure() {\n if (remeasureDebounceId !== null) {\n clearTimeout(remeasureDebounceId);\n }\n remeasureDebounceId = setTimeout(scheduleRemeasure, REMEASURE_DEBOUNCE_MS);\n}\n\nfunction handleScroll(event: Event) {\n // Document scroll is free for absolute frames; scrolls of inner overflow\n // containers move elements relative to the page, so re-measure everything.\n if (event.target !== document) {\n scheduleRemeasure();\n }\n if (activeMarkerId !== null) {\n const id = activeMarkerId;\n activeRectFrameId ??= requestAnimationFrame(() => {\n activeRectFrameId = null;\n const viewportRect = getAnnotationBadgeViewportRect(id);\n if (viewportRect) {\n postToEditor({\n type: \"replo:annotations:activeRect\",\n id,\n viewportRect,\n });\n }\n });\n }\n}\n\nfunction syncObservers() {\n const shouldObserve = markers.length > 0;\n if (shouldObserve === observersActive) {\n return;\n }\n observersActive = shouldObserve;\n if (shouldObserve) {\n window.addEventListener(\"resize\", scheduleRemeasure);\n window.addEventListener(\"scroll\", handleScroll, { capture: true });\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(scheduleRemeasure);\n resizeObserver.observe(document.body);\n }\n mutationObserver = new MutationObserver((records) => {\n // Rendering our own frames mutates body's subtree; reacting to that\n // would re-render forever. Only foreign mutations can move elements.\n const container = document.getElementById(ANNOTATION_CONTAINER_ID);\n const hasForeignMutation = records.some((record) => {\n return !container || !container.contains(record.target);\n });\n if (hasForeignMutation) {\n scheduleDebouncedRemeasure();\n }\n });\n mutationObserver.observe(document.body, { childList: true, subtree: true });\n } else {\n window.removeEventListener(\"resize\", scheduleRemeasure);\n window.removeEventListener(\"scroll\", handleScroll, { capture: true });\n resizeObserver?.disconnect();\n resizeObserver = null;\n mutationObserver?.disconnect();\n mutationObserver = null;\n }\n}\n\nfunction handleMessage(event: MessageEvent) {\n // Origin gate BEFORE any parsing \u2014 hostile embedders never reach the schema.\n if (!isAllowedEditorMessage(event)) {\n return;\n }\n const parsed = pageAnnotationsParentMessageSchema.safeParse(event.data);\n if (!parsed.success) {\n return;\n }\n editorOrigin = event.origin;\n exhaustiveSwitch(parsed.data)({\n \"replo:annotations:set\": (message) => {\n markers = message.markers;\n setId = message.setId;\n expandedClusterId = null;\n activeMarkerId = null;\n // A fresh set always paints (and re-posts unresolved), even if geometry\n // happens to match the previous render.\n lastRenderSignature = null;\n render();\n },\n \"replo:annotations:clear\": () => {\n markers = [];\n setId = null;\n expandedClusterId = null;\n activeMarkerId = null;\n render();\n },\n \"replo:annotations:requestReady\": () => {\n postToEditor({\n type: \"replo:annotations:ready\",\n protocolVersion: PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n });\n },\n });\n}\n\nexport function initAnnotationLayer() {\n if (isInitialized || typeof window === \"undefined\") {\n return;\n }\n if (window.self === window.top) {\n return;\n }\n isInitialized = true;\n window.addEventListener(\"message\", handleMessage);\n // Announce on load for iframe-reload cases where the editor isn't polling;\n // the referrer origin is verified against the same allowlist first.\n try {\n const referrerOrigin = new URL(document.referrer).origin;\n if (isAllowedEditorOrigin(referrerOrigin)) {\n window.parent.postMessage(\n {\n type: \"replo:annotations:ready\",\n protocolVersion: PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n },\n referrerOrigin,\n );\n }\n } catch {\n // No usable referrer \u2014 the editor's requestReady polling covers this.\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * In-iframe annotation controller: consumes declarative marker sets from the\n * editor bridge, resolves selectors against the live DOM, and drives the\n * shared annotation frame renderer. Scroll costs zero JavaScript (frames use\n * page coordinates); re-measure runs only on resize/mutation/container\n * scroll, batched per animation frame. Data-only protocol \u2014 the editor can\n * describe dots, never execute code.\n */\nimport type {\n PageAnnotationMarker,\n PageAnnotationsIframeMessage,\n} from \"schemas/generated/pageAnnotations\";\nimport type { AnnotationFrameSpec } from \"../select-mode/annotationFrames\";\n\nimport { exhaustiveSwitch } from \"replo-utils/lib/misc\";\nimport {\n PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n pageAnnotationsParentMessageSchema,\n} from \"schemas/pageAnnotations\";\n\nimport {\n ANNOTATION_CONTAINER_ID,\n clearAnnotationFrames,\n getAnnotationBadgeViewportRect,\n setActiveAnnotationBadge,\n showAnnotationFrames,\n} from \"../select-mode/annotationFrames\";\nimport { getDocumentScrollOffset } from \"../select-mode/interactions/selectionGeometry\";\nimport { isAllowedEditorMessage, isAllowedEditorOrigin } from \"./bridgeGate\";\n\nconst CLUSTER_DISTANCE_PX = 24;\nconst CLUSTER_FAN_RADIUS_PX = 30;\n// Badges center on their element's top edge, so edge-hugging ones need insetting.\nconst BADGE_RADIUS_PX = 15;\nconst BADGE_EDGE_INSET_PX = BADGE_RADIUS_PX + 4;\nconst REMEASURE_DEBOUNCE_MS = 250;\n// Page-wide findings stack in a row near the top-left of the page and scroll\n// with it like any other annotation.\nconst PAGE_TOP_STACK_X_PX = 20;\nconst PAGE_TOP_STACK_Y_PX = 20;\nconst PAGE_TOP_STACK_GAP_PX = 36;\n\n// Replo brand palette (lattice --color-brand-*): the wire enum names stay\n// red/purple/amber but the rendered hues are brand magenta/purple/blue,\n// matching the editor-side dots and cards.\nconst COLOR_HEX: Record<PageAnnotationMarker[\"color\"], string> = {\n red: \"#ef196e\",\n purple: \"#8b32a8\",\n amber: \"#274ae2\",\n};\nconst COLOR_SEVERITY: PageAnnotationMarker[\"color\"][] = [\n \"red\",\n \"purple\",\n \"amber\",\n];\n\ninterface ResolvedMarker {\n marker: PageAnnotationMarker;\n element: Element | null;\n isFixed: boolean;\n rect: { x: number; y: number; width: number; height: number };\n // The editor-assigned wire number, rendered verbatim.\n displayNumber: number;\n}\n\ninterface MarkerCluster {\n id: string;\n centerX: number;\n centerY: number;\n isFixed: boolean;\n members: ResolvedMarker[];\n}\n\nlet isInitialized = false;\nlet editorOrigin: string | null = null;\nlet markers: PageAnnotationMarker[] = [];\nlet setId: string | null = null;\nlet expandedClusterId: string | null = null;\nlet activeMarkerId: string | null = null;\nlet remeasureFrameId: number | null = null;\nlet remeasureDebounceId: ReturnType<typeof setTimeout> | null = null;\nlet activeRectFrameId: number | null = null;\nlet observersActive = false;\nlet lastRenderSignature: string | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet mutationObserver: MutationObserver | null = null;\n\nfunction postToEditor(message: PageAnnotationsIframeMessage) {\n if (editorOrigin === null) {\n return;\n }\n window.parent.postMessage(message, editorOrigin);\n}\n\nfunction hasFixedAncestor(element: Element): boolean {\n let current: Element | null = element;\n while (current !== null && current !== document.documentElement) {\n const position = getComputedStyle(current).position;\n if (position === \"fixed\" || position === \"sticky\") {\n return true;\n }\n current = current.parentElement;\n }\n return false;\n}\n\nfunction resolveMarkers(): {\n resolved: ResolvedMarker[];\n unresolvedIds: string[];\n} {\n const resolved: ResolvedMarker[] = [];\n const unresolvedIds: string[] = [];\n let pageTopSlot = 0;\n for (const marker of markers) {\n if (marker.placement === \"pageTop\") {\n // Page-wide finding: a fixed slot in the top-of-page stack, no selector.\n resolved.push({\n marker,\n element: null,\n isFixed: false,\n rect: {\n x: PAGE_TOP_STACK_X_PX + pageTopSlot * PAGE_TOP_STACK_GAP_PX,\n y: PAGE_TOP_STACK_Y_PX,\n width: 0,\n height: 0,\n },\n // The editor owns numbering; rendering the wire number keeps dots in sync with the panel.\n displayNumber: marker.number,\n });\n pageTopSlot += 1;\n continue;\n }\n let element: Element | null = null;\n try {\n element = document.querySelector(marker.selector);\n } catch {\n // Malformed selector \u2014 unresolved, never a throw.\n }\n const domRect = element?.getBoundingClientRect();\n if (!element || !domRect || (domRect.width === 0 && domRect.height === 0)) {\n unresolvedIds.push(marker.id);\n continue;\n }\n const isFixed = hasFixedAncestor(element);\n const scrollOffset = getDocumentScrollOffset();\n resolved.push({\n marker,\n element,\n isFixed,\n // Fixed frames use viewport coords; absolute frames use page coords.\n rect: {\n x: domRect.x + (isFixed ? 0 : scrollOffset.left),\n y: domRect.y + (isFixed ? 0 : scrollOffset.top),\n width: domRect.width,\n height: domRect.height,\n },\n displayNumber: marker.number,\n });\n }\n return { resolved, unresolvedIds };\n}\n\n// Greedy: a marker joins the first cluster whose badge anchor lies within\n// 24px; fixed and absolute markers never co-cluster (their coordinate spaces\n// diverge as soon as the page scrolls).\nfunction clusterResolvedMarkers(resolved: ResolvedMarker[]): MarkerCluster[] {\n const clusters: MarkerCluster[] = [];\n for (const entry of resolved) {\n const centerX = entry.rect.x + entry.rect.width / 2;\n const centerY = entry.rect.y;\n const host = clusters.find((cluster) => {\n return (\n cluster.isFixed === entry.isFixed &&\n Math.hypot(cluster.centerX - centerX, cluster.centerY - centerY) <\n CLUSTER_DISTANCE_PX\n );\n });\n if (host) {\n host.members.push(entry);\n } else {\n clusters.push({\n id: entry.marker.id,\n centerX,\n centerY,\n isFixed: entry.isFixed,\n members: [entry],\n });\n }\n }\n return clusters;\n}\n\n// Keeps a badge fully on screen: page box for absolute, viewport for fixed.\nfunction clampBadgeCenter({\n x,\n y,\n isFixed,\n}: {\n x: number;\n y: number;\n isFixed: boolean;\n}): { x: number; y: number } {\n const width = isFixed\n ? window.innerWidth\n : Math.max(document.documentElement.scrollWidth, window.innerWidth);\n const height = isFixed\n ? window.innerHeight\n : Math.max(document.documentElement.scrollHeight, window.innerHeight);\n const clamp = (value: number, limit: number) => {\n return Math.min(\n Math.max(value, BADGE_EDGE_INSET_PX),\n Math.max(BADGE_EDGE_INSET_PX, limit - BADGE_EDGE_INSET_PX),\n );\n };\n return { x: clamp(x, width), y: clamp(y, height) };\n}\n\nfunction getMostSevereColorHex(members: ResolvedMarker[]): string {\n const color =\n COLOR_SEVERITY.find((candidate) => {\n return members.some((member) => member.marker.color === candidate);\n }) ?? \"amber\";\n return COLOR_HEX[color];\n}\n\nfunction activateMarker(id: string) {\n activeMarkerId = id;\n const viewportRect = getAnnotationBadgeViewportRect(id);\n if (viewportRect) {\n postToEditor({ type: \"replo:annotations:clicked\", id, viewportRect });\n }\n // Style-only: a rebuild here would race the pointer sequence and swallow\n // the very click being handled.\n setActiveAnnotationBadge(id);\n}\n\nfunction buildMemberSpec({\n member,\n badgeCenter,\n}: {\n member: ResolvedMarker;\n badgeCenter: { x: number; y: number };\n}): AnnotationFrameSpec {\n const marker = member.marker;\n return {\n id: marker.id,\n pageRect: member.rect,\n badgeCenter: clampBadgeCenter({ ...badgeCenter, isFixed: member.isFixed }),\n positioning: member.isFixed ? \"fixed\" : \"absolute\",\n colorHex: COLOR_HEX[marker.color],\n badgeLabel: String(member.displayNumber),\n badgeAriaLabel: `Page issue ${member.displayNumber}`,\n isActive: activeMarkerId === marker.id,\n onBadgeClick: () => activateMarker(marker.id),\n };\n}\n\nfunction buildClusterSpecs(cluster: MarkerCluster): AnnotationFrameSpec[] {\n const [only] = cluster.members;\n if (cluster.members.length === 1 && only) {\n return [\n buildMemberSpec({\n member: only,\n badgeCenter: { x: cluster.centerX, y: cluster.centerY },\n }),\n ];\n }\n const hub = {\n x: cluster.centerX,\n y: cluster.centerY,\n width: 0,\n height: 0,\n };\n if (expandedClusterId !== cluster.id) {\n return [\n {\n id: `cluster:${cluster.id}`,\n pageRect: hub,\n badgeCenter: clampBadgeCenter({\n x: cluster.centerX,\n y: cluster.centerY,\n isFixed: cluster.isFixed,\n }),\n positioning: cluster.isFixed ? \"fixed\" : \"absolute\",\n colorHex: getMostSevereColorHex(cluster.members),\n badgeLabel: String(cluster.members.length),\n badgeAriaLabel: `${cluster.members.length} page issues near this element`,\n isActive: false,\n onBadgeClick: () => {\n expandedClusterId = cluster.id;\n render();\n },\n },\n ];\n }\n const memberSpecs = cluster.members.map((member, index) => {\n const angle = (index / cluster.members.length) * 2 * Math.PI - Math.PI / 2;\n return buildMemberSpec({\n member,\n badgeCenter: {\n x: cluster.centerX + Math.cos(angle) * CLUSTER_FAN_RADIUS_PX,\n y: cluster.centerY + Math.sin(angle) * CLUSTER_FAN_RADIUS_PX,\n },\n });\n });\n return [\n {\n id: `cluster:${cluster.id}`,\n pageRect: hub,\n badgeCenter: clampBadgeCenter({\n x: cluster.centerX,\n y: cluster.centerY,\n isFixed: cluster.isFixed,\n }),\n positioning: cluster.isFixed ? \"fixed\" : \"absolute\",\n colorHex: \"#94a3b8\",\n badgeLabel: \"\u00D7\",\n badgeAriaLabel: \"Collapse issue cluster\",\n isActive: false,\n onBadgeClick: () => {\n expandedClusterId = null;\n render();\n },\n },\n ...memberSpecs,\n ];\n}\n\nfunction render() {\n if (markers.length === 0) {\n lastRenderSignature = null;\n clearAnnotationFrames();\n syncObservers();\n return;\n }\n const { resolved, unresolvedIds } = resolveMarkers();\n if (activeMarkerId !== null && unresolvedIds.includes(activeMarkerId)) {\n activeMarkerId = null;\n }\n // Rebuilding the node pool mid-pointer-sequence swallows clicks, so skip\n // renders whose output would be identical to what is already on screen.\n const signature = [\n resolved\n .map((entry) => {\n return `${entry.marker.id}:${Math.round(entry.rect.x)},${Math.round(entry.rect.y)}`;\n })\n .join(\"|\"),\n `active:${activeMarkerId ?? \"\"}`,\n `expanded:${expandedClusterId ?? \"\"}`,\n `unresolved:${unresolvedIds.join(\",\")}`,\n ].join(\"#\");\n if (signature === lastRenderSignature) {\n return;\n }\n lastRenderSignature = signature;\n const frames = clusterResolvedMarkers(resolved).flatMap((cluster) => {\n return buildClusterSpecs(cluster);\n });\n showAnnotationFrames({ frames });\n if (setId !== null) {\n postToEditor({\n type: \"replo:annotations:unresolved\",\n setId,\n ids: unresolvedIds,\n });\n }\n syncObservers();\n}\n\nfunction scheduleRemeasure() {\n remeasureFrameId ??= requestAnimationFrame(() => {\n remeasureFrameId = null;\n render();\n });\n}\n\nfunction scheduleDebouncedRemeasure() {\n if (remeasureDebounceId !== null) {\n clearTimeout(remeasureDebounceId);\n }\n remeasureDebounceId = setTimeout(scheduleRemeasure, REMEASURE_DEBOUNCE_MS);\n}\n\nfunction handleScroll(event: Event) {\n // Document scroll is free for absolute frames; scrolls of inner overflow\n // containers move elements relative to the page, so re-measure everything.\n if (event.target !== document) {\n scheduleRemeasure();\n }\n if (activeMarkerId !== null) {\n const id = activeMarkerId;\n activeRectFrameId ??= requestAnimationFrame(() => {\n activeRectFrameId = null;\n const viewportRect = getAnnotationBadgeViewportRect(id);\n if (viewportRect) {\n postToEditor({\n type: \"replo:annotations:activeRect\",\n id,\n viewportRect,\n });\n }\n });\n }\n}\n\nfunction syncObservers() {\n const shouldObserve = markers.length > 0;\n if (shouldObserve === observersActive) {\n return;\n }\n observersActive = shouldObserve;\n if (shouldObserve) {\n window.addEventListener(\"resize\", scheduleRemeasure);\n window.addEventListener(\"scroll\", handleScroll, { capture: true });\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(scheduleRemeasure);\n resizeObserver.observe(document.body);\n }\n mutationObserver = new MutationObserver((records) => {\n // Rendering our own frames mutates body's subtree; reacting to that\n // would re-render forever. Only foreign mutations can move elements.\n const container = document.getElementById(ANNOTATION_CONTAINER_ID);\n const hasForeignMutation = records.some((record) => {\n return !container || !container.contains(record.target);\n });\n if (hasForeignMutation) {\n scheduleDebouncedRemeasure();\n }\n });\n mutationObserver.observe(document.body, { childList: true, subtree: true });\n } else {\n window.removeEventListener(\"resize\", scheduleRemeasure);\n window.removeEventListener(\"scroll\", handleScroll, { capture: true });\n resizeObserver?.disconnect();\n resizeObserver = null;\n mutationObserver?.disconnect();\n mutationObserver = null;\n // A pending remeasure would otherwise fire into a torn-down document.\n if (remeasureDebounceId !== null) {\n clearTimeout(remeasureDebounceId);\n remeasureDebounceId = null;\n }\n if (remeasureFrameId !== null) {\n cancelAnimationFrame(remeasureFrameId);\n remeasureFrameId = null;\n }\n }\n}\n\nfunction handleMessage(event: MessageEvent) {\n // Origin gate BEFORE any parsing \u2014 hostile embedders never reach the schema.\n if (!isAllowedEditorMessage(event)) {\n return;\n }\n const parsed = pageAnnotationsParentMessageSchema.safeParse(event.data);\n if (!parsed.success) {\n return;\n }\n editorOrigin = event.origin;\n exhaustiveSwitch(parsed.data)({\n \"replo:annotations:set\": (message) => {\n markers = message.markers;\n setId = message.setId;\n expandedClusterId = null;\n activeMarkerId = null;\n // A fresh set always paints (and re-posts unresolved), even if geometry\n // happens to match the previous render.\n lastRenderSignature = null;\n render();\n },\n \"replo:annotations:clear\": () => {\n markers = [];\n setId = null;\n expandedClusterId = null;\n activeMarkerId = null;\n render();\n },\n \"replo:annotations:requestReady\": () => {\n postToEditor({\n type: \"replo:annotations:ready\",\n protocolVersion: PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n });\n },\n });\n}\n\nexport function initAnnotationLayer() {\n if (isInitialized || typeof window === \"undefined\") {\n return;\n }\n if (window.self === window.top) {\n return;\n }\n isInitialized = true;\n window.addEventListener(\"message\", handleMessage);\n // Announce on load for iframe-reload cases where the editor isn't polling;\n // the referrer origin is verified against the same allowlist first.\n try {\n const referrerOrigin = new URL(document.referrer).origin;\n if (isAllowedEditorOrigin(referrerOrigin)) {\n window.parent.postMessage(\n {\n type: \"replo:annotations:ready\",\n protocolVersion: PAGE_ANNOTATIONS_PROTOCOL_VERSION,\n },\n referrerOrigin,\n );\n }\n } catch {\n // No usable referrer \u2014 the editor's requestReady polling covers this.\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "branch": "HEAD",
5
- "commit": "544d5eef4f0142da5e8569baa4832a525d11fc06",
6
- "builtAt": "2026-08-26T18:10:31.971Z"
5
+ "commit": "ec3c1ba407bf796e64a94f759ceffa7872c21d74",
6
+ "builtAt": "2026-08-28T01:00:56.326Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.3.0",
3
+ "version": "1.4.1",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.3.0",
6
+ "version": "1.4.1",
7
7
  "branch": "HEAD",
8
- "commit": "544d5eef4f0142da5e8569baa4832a525d11fc06",
9
- "builtAt": "2026-08-26T18:10:31.971Z"
8
+ "commit": "ec3c1ba407bf796e64a94f759ceffa7872c21d74",
9
+ "builtAt": "2026-08-28T01:00:56.326Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",