@replohq/sandbox-runtime 1.7.9 → 1.7.11

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 (40) hide show
  1. package/_vendor/replo-utils/react/zustand.d.ts +13 -0
  2. package/_vendor/replo-utils/react/zustand.mjs +24 -0
  3. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +12 -2
  4. package/_vendor/schemas/selectModeIframeRpc.d.ts +66 -4
  5. package/_vendor/schemas/selectModeIframeRpc.mjs +16 -3
  6. package/buildMetadata.js +3 -3
  7. package/package.json +6 -5
  8. package/select-mode/EditorSelection.d.ts +1 -22
  9. package/select-mode/EditorSelection.js +61 -100
  10. package/select-mode/SelectionFrames.d.ts +1 -0
  11. package/select-mode/SelectionFrames.js +58 -0
  12. package/select-mode/annotationFrames.d.ts +13 -0
  13. package/select-mode/annotationFrames.js +31 -1
  14. package/select-mode/descriptor/elementDescriptor.js +1 -1
  15. package/select-mode/displayModeStore.d.ts +0 -2
  16. package/select-mode/displayModeStore.js +11 -30
  17. package/select-mode/hooks/useParentMessaging.js +3 -0
  18. package/select-mode/hooks/useSelectionHover.d.ts +1 -19
  19. package/select-mode/hooks/useSelectionHover.js +37 -49
  20. package/select-mode/hooks/useSelectionInteractions.d.ts +1 -12
  21. package/select-mode/hooks/useSelectionInteractions.js +57 -39
  22. package/select-mode/hooks/useSelectionScrollSync.d.ts +1 -25
  23. package/select-mode/hooks/useSelectionScrollSync.js +48 -40
  24. package/select-mode/iframeRpcChannel.d.ts +24 -1
  25. package/select-mode/inlineEditFeedback.d.ts +6 -0
  26. package/select-mode/inlineEditFeedback.js +47 -0
  27. package/select-mode/isTextEditableElement.d.ts +1 -0
  28. package/select-mode/isTextEditableElement.js +15 -0
  29. package/select-mode/selectModeStore.d.ts +160 -0
  30. package/select-mode/selectModeStore.js +585 -0
  31. package/select-mode/selectionFrame.d.ts +10 -59
  32. package/select-mode/selectionFrame.js +26 -150
  33. package/select-mode/symbolicate.js +4 -0
  34. package/select-mode/tiptapInlineEditor.js +16 -1
  35. package/select-mode/hooks/useCrosshairCursor.d.ts +0 -10
  36. package/select-mode/hooks/useCrosshairCursor.js +0 -23
  37. package/select-mode/hooks/useEditModeStateMachine.d.ts +0 -16
  38. package/select-mode/hooks/useEditModeStateMachine.js +0 -228
  39. package/select-mode/hooks/useSelectModeController.d.ts +0 -11
  40. package/select-mode/hooks/useSelectModeController.js +0 -72
@@ -0,0 +1,13 @@
1
+ import type { StateCreator, StoreApi } from "zustand";
2
+ /**
3
+ * Drop-in replacement for zustand's `useStore` that uses shallow equality by default.
4
+ * Use this in context-based stores so all selectors automatically get shallow comparison.
5
+ * Subscribes through React's useSyncExternalStore so the consumer's React is
6
+ * used, not Zustand's nested use-sync-external-store copy.
7
+ */
8
+ export declare function useShallowStore<StateType, SelectedType>(store: StoreApi<StateType>, selector: (state: StateType) => SelectedType): SelectedType;
9
+ /**
10
+ * Drop-in replacement for zustand's `create` that uses shallow equality by default.
11
+ * Use this instead of `create()` so all selectors automatically get shallow comparison.
12
+ */
13
+ export declare function createShallowStore<StateType>(initializer: StateCreator<StateType, [], []>): import("zustand/traditional").UseBoundStoreWithEqualityFn<StoreApi<StateType>>;
@@ -0,0 +1,24 @@
1
+ import { useRef, useSyncExternalStore } from 'react';
2
+ import { shallow } from 'zustand/shallow';
3
+ import { createWithEqualityFn } from 'zustand/traditional';
4
+
5
+ function useShallowStore(store, selector) {
6
+ const lastSelectedRef = useRef(void 0);
7
+ return useSyncExternalStore(
8
+ store.subscribe,
9
+ () => {
10
+ const next = selector(store.getState());
11
+ if (lastSelectedRef.current !== void 0 && shallow(lastSelectedRef.current, next)) {
12
+ return lastSelectedRef.current;
13
+ }
14
+ lastSelectedRef.current = next;
15
+ return next;
16
+ },
17
+ () => selector(store.getState())
18
+ );
19
+ }
20
+ function createShallowStore(initializer) {
21
+ return createWithEqualityFn(initializer, shallow);
22
+ }
23
+
24
+ export { createShallowStore, useShallowStore };
@@ -1,4 +1,8 @@
1
- import type { SelectedElement, DisplayMode, SelectModeSelectionType, SelectModeOptimisticUpdatePayload, SelectModeFormatCommand, SelectableData, SelectModeBoundingRect, SelectModeClickPosition, SelectModeSelectionRect, SelectModeTiptapActiveMarks } from "./selectMode";
1
+ import type { SelectModeBoundingRect, SelectedElement, DisplayMode, SelectModeSelectionType, SelectModeOptimisticUpdatePayload, SelectModeFormatCommand, SelectableData, SelectModeClickPosition, SelectModeSelectionRect, SelectModeTiptapActiveMarks } from "./selectMode";
2
+ export type InlineEditFeedback = {
3
+ status: "pending" | "unavailable" | "failed";
4
+ rect: SelectModeBoundingRect;
5
+ };
2
6
  export type SelectedComponentEditRequest = {
3
7
  siteId: string;
4
8
  element: SelectedElement;
@@ -105,11 +109,17 @@ export type SelectModeIframeMessage = {
105
109
  };
106
110
  originalInnerHTML: string;
107
111
  };
112
+ } | {
113
+ method: "inlineEditFeedbackChanged";
114
+ data: {
115
+ feedback: InlineEditFeedback | null;
116
+ };
108
117
  } | {
109
118
  method: "editorStateUpdate";
110
119
  data: {
111
120
  activeMarks: SelectModeTiptapActiveMarks;
112
- isEditing: boolean;
121
+ isInlineEditing: boolean;
122
+ isInlineEditPending: boolean;
113
123
  };
114
124
  } | {
115
125
  method: "fastRefreshComplete";
@@ -2,6 +2,23 @@ import type { SelectModeIframeMessage, SelectModeParentMessage } from "./generat
2
2
  import { z } from "zod";
3
3
  export declare const SELECT_MODE_IFRAME_RPC_CHANNEL_ID: "replo:select-mode-iframe-rpc";
4
4
  export declare const SELECT_MODE_IFRAME_RPC_CHANNEL_VERSION: 1;
5
+ export declare const inlineEditFeedbackSchema: z.ZodObject<{
6
+ status: z.ZodEnum<{
7
+ unavailable: "unavailable";
8
+ pending: "pending";
9
+ failed: "failed";
10
+ }>;
11
+ rect: z.ZodObject<{
12
+ x: z.ZodNumber;
13
+ y: z.ZodNumber;
14
+ left: z.ZodNumber;
15
+ top: z.ZodNumber;
16
+ right: z.ZodNumber;
17
+ bottom: z.ZodNumber;
18
+ width: z.ZodNumber;
19
+ height: z.ZodNumber;
20
+ }, z.core.$strip>;
21
+ }, z.core.$strip>;
5
22
  export declare const iframeRuntimeBuildMetadataSchema: z.ZodObject<{
6
23
  packageName: z.ZodString;
7
24
  version: z.ZodString;
@@ -258,8 +275,8 @@ export declare const websiteBuilderToCanopyMethodConfig: {
258
275
  inputSchema: z.ZodObject<{
259
276
  sessionId: z.ZodString;
260
277
  outcome: z.ZodEnum<{
261
- persisted: "persisted";
262
278
  failed: "failed";
279
+ persisted: "persisted";
263
280
  }>;
264
281
  }, z.core.$strip>;
265
282
  outputSchema: z.ZodVoid;
@@ -621,6 +638,28 @@ export declare const canopyToWebsiteBuilderMethodConfig: {
621
638
  }, z.core.$strip>;
622
639
  outputSchema: z.ZodVoid;
623
640
  };
641
+ inlineEditFeedbackChanged: {
642
+ inputSchema: z.ZodObject<{
643
+ feedback: z.ZodNullable<z.ZodObject<{
644
+ status: z.ZodEnum<{
645
+ unavailable: "unavailable";
646
+ pending: "pending";
647
+ failed: "failed";
648
+ }>;
649
+ rect: z.ZodObject<{
650
+ x: z.ZodNumber;
651
+ y: z.ZodNumber;
652
+ left: z.ZodNumber;
653
+ top: z.ZodNumber;
654
+ right: z.ZodNumber;
655
+ bottom: z.ZodNumber;
656
+ width: z.ZodNumber;
657
+ height: z.ZodNumber;
658
+ }, z.core.$strip>;
659
+ }, z.core.$strip>>;
660
+ }, z.core.$strip>;
661
+ outputSchema: z.ZodVoid;
662
+ };
624
663
  editorStateUpdate: {
625
664
  inputSchema: z.ZodObject<{
626
665
  activeMarks: z.ZodObject<{
@@ -629,7 +668,8 @@ export declare const canopyToWebsiteBuilderMethodConfig: {
629
668
  underline: z.ZodBoolean;
630
669
  strike: z.ZodBoolean;
631
670
  }, z.core.$strip>;
632
- isEditing: z.ZodBoolean;
671
+ isInlineEditing: z.ZodBoolean;
672
+ isInlineEditPending: z.ZodBoolean;
633
673
  }, z.core.$strip>;
634
674
  outputSchema: z.ZodVoid;
635
675
  };
@@ -758,8 +798,8 @@ export declare const selectModeParentMessageSchema: z.ZodDiscriminatedUnion<[z.Z
758
798
  data: z.ZodObject<{
759
799
  sessionId: z.ZodString;
760
800
  outcome: z.ZodEnum<{
761
- persisted: "persisted";
762
801
  failed: "failed";
802
+ persisted: "persisted";
763
803
  }>;
764
804
  }, z.core.$strip>;
765
805
  }, z.core.$strip>, z.ZodObject<{
@@ -1036,6 +1076,27 @@ export declare const selectModeIframeMessageSchema: z.ZodDiscriminatedUnion<[z.Z
1036
1076
  json: z.ZodRecord<z.ZodString, z.ZodUnknown>;
1037
1077
  originalInnerHTML: z.ZodString;
1038
1078
  }, z.core.$strip>;
1079
+ }, z.core.$strip>, z.ZodObject<{
1080
+ method: z.ZodLiteral<"inlineEditFeedbackChanged">;
1081
+ data: z.ZodObject<{
1082
+ feedback: z.ZodNullable<z.ZodObject<{
1083
+ status: z.ZodEnum<{
1084
+ unavailable: "unavailable";
1085
+ pending: "pending";
1086
+ failed: "failed";
1087
+ }>;
1088
+ rect: z.ZodObject<{
1089
+ x: z.ZodNumber;
1090
+ y: z.ZodNumber;
1091
+ left: z.ZodNumber;
1092
+ top: z.ZodNumber;
1093
+ right: z.ZodNumber;
1094
+ bottom: z.ZodNumber;
1095
+ width: z.ZodNumber;
1096
+ height: z.ZodNumber;
1097
+ }, z.core.$strip>;
1098
+ }, z.core.$strip>>;
1099
+ }, z.core.$strip>;
1039
1100
  }, z.core.$strip>, z.ZodObject<{
1040
1101
  method: z.ZodLiteral<"editorStateUpdate">;
1041
1102
  data: z.ZodObject<{
@@ -1045,7 +1106,8 @@ export declare const selectModeIframeMessageSchema: z.ZodDiscriminatedUnion<[z.Z
1045
1106
  underline: z.ZodBoolean;
1046
1107
  strike: z.ZodBoolean;
1047
1108
  }, z.core.$strip>;
1048
- isEditing: z.ZodBoolean;
1109
+ isInlineEditing: z.ZodBoolean;
1110
+ isInlineEditPending: z.ZodBoolean;
1049
1111
  }, z.core.$strip>;
1050
1112
  }, z.core.$strip>, z.ZodObject<{
1051
1113
  method: z.ZodLiteral<"fastRefreshComplete">;
@@ -1,4 +1,4 @@
1
- import { selectedElementSchema, selectModeFormatCommandSchema, selectModeOptimisticUpdatePayloadSchema, selectModeSelectionTypeSchema, displayModeSchema, selectModeTiptapActiveMarksSchema, selectModeBoundingRectSchema, selectableDataSchema, selectModeSelectionRectSchema, selectModeClickPositionSchema } from '../../chunk-4T7WSMC3.mjs';
1
+ import { selectModeBoundingRectSchema, selectedElementSchema, selectModeFormatCommandSchema, selectModeOptimisticUpdatePayloadSchema, selectModeSelectionTypeSchema, displayModeSchema, selectModeTiptapActiveMarksSchema, selectableDataSchema, selectModeSelectionRectSchema, selectModeClickPositionSchema } from '../../chunk-4T7WSMC3.mjs';
2
2
  import '../../chunk-JUKCUPQ6.mjs';
3
3
  import '../../chunk-FLYEIRFK.mjs';
4
4
  import '../../chunk-TESFJQID.mjs';
@@ -7,6 +7,10 @@ import { z } from 'zod';
7
7
 
8
8
  var SELECT_MODE_IFRAME_RPC_CHANNEL_ID = "replo:select-mode-iframe-rpc";
9
9
  var SELECT_MODE_IFRAME_RPC_CHANNEL_VERSION = 1;
10
+ var inlineEditFeedbackSchema = z.object({
11
+ status: z.enum(["pending", "unavailable", "failed"]),
12
+ rect: selectModeBoundingRectSchema
13
+ }).describe("InlineEditFeedback");
10
14
  var iframeRuntimeBuildMetadataSchema = z.object({
11
15
  packageName: z.string(),
12
16
  version: z.string(),
@@ -144,10 +148,15 @@ var canopyToWebsiteBuilderMethodConfig = {
144
148
  }),
145
149
  outputSchema: z.void()
146
150
  },
151
+ inlineEditFeedbackChanged: {
152
+ inputSchema: z.object({ feedback: inlineEditFeedbackSchema.nullable() }),
153
+ outputSchema: z.void()
154
+ },
147
155
  editorStateUpdate: {
148
156
  inputSchema: z.object({
149
157
  activeMarks: selectModeTiptapActiveMarksSchema,
150
- isEditing: z.boolean()
158
+ isInlineEditing: z.boolean(),
159
+ isInlineEditPending: z.boolean()
151
160
  }),
152
161
  outputSchema: z.void()
153
162
  },
@@ -229,6 +238,10 @@ var selectModeIframeMessageSchema = z.discriminatedUnion("method", [
229
238
  method: z.literal("textCommitted"),
230
239
  data: canopyToWebsiteBuilderMethodConfig.textCommitted.inputSchema
231
240
  }),
241
+ z.object({
242
+ method: z.literal("inlineEditFeedbackChanged"),
243
+ data: canopyToWebsiteBuilderMethodConfig.inlineEditFeedbackChanged.inputSchema
244
+ }),
232
245
  z.object({
233
246
  method: z.literal("editorStateUpdate"),
234
247
  data: canopyToWebsiteBuilderMethodConfig.editorStateUpdate.inputSchema
@@ -238,4 +251,4 @@ var selectModeIframeMessageSchema = z.discriminatedUnion("method", [
238
251
  })
239
252
  ]).describe("SelectModeIframeMessage");
240
253
 
241
- export { SELECT_MODE_IFRAME_RPC_CHANNEL_ID, SELECT_MODE_IFRAME_RPC_CHANNEL_VERSION, canopyToWebsiteBuilderMethodConfig, iframeRuntimeBuildMetadataSchema, selectModeIframeMessageSchema, selectModeParentMessageSchema, selectedComponentEditRequestSchema, websiteBuilderToCanopyMethodConfig };
254
+ export { SELECT_MODE_IFRAME_RPC_CHANNEL_ID, SELECT_MODE_IFRAME_RPC_CHANNEL_VERSION, canopyToWebsiteBuilderMethodConfig, iframeRuntimeBuildMetadataSchema, inlineEditFeedbackSchema, selectModeIframeMessageSchema, selectModeParentMessageSchema, selectedComponentEditRequestSchema, websiteBuilderToCanopyMethodConfig };
package/buildMetadata.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const BUILD_METADATA = {
2
2
  "packageName": "@replohq/sandbox-runtime",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
4
4
  "branch": "HEAD",
5
- "commit": "b01d429fa2ffd0346a2187126d2b7ea58e40441f",
6
- "builtAt": "2026-09-20T05:20:23.775Z"
5
+ "commit": "14ba23d4044e71ee124025f7b496f0cc9f6e4b06",
6
+ "builtAt": "2026-09-21T17:30:09.582Z"
7
7
  };
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@replohq/sandbox-runtime",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
4
4
  "reploBuild": {
5
5
  "packageName": "@replohq/sandbox-runtime",
6
- "version": "1.7.9",
6
+ "version": "1.7.11",
7
7
  "branch": "HEAD",
8
- "commit": "b01d429fa2ffd0346a2187126d2b7ea58e40441f",
9
- "builtAt": "2026-09-20T05:20:23.775Z"
8
+ "commit": "14ba23d4044e71ee124025f7b496f0cc9f6e4b06",
9
+ "builtAt": "2026-09-21T17:30:09.582Z"
10
10
  },
11
11
  "description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
12
12
  "license": "SEE LICENSE IN LICENSE",
@@ -41,7 +41,8 @@
41
41
  "@tiptap/extension-underline": "^2.25.0",
42
42
  "@tiptap/pm": "^2.25.0",
43
43
  "uuid": "^11.1.0",
44
- "zod": "^4.3.6"
44
+ "zod": "^4.3.6",
45
+ "zustand": "^4.5.7"
45
46
  },
46
47
  "peerDependencies": {
47
48
  "@types/react": ">=19.0.0",
@@ -1,22 +1 @@
1
- /**
2
- * Mounts the iframe-side selectMode controller. This is a render-less
3
- * component — every responsibility is delegated to a dedicated hook so each
4
- * concern (parent messaging, cursor styling, pointer interactions, hover
5
- * tracking, scroll syncing) can be read in isolation:
6
- *
7
- * - `useParentMessaging` — inbound messages from the parent (set mode,
8
- * set selection type) + outbound `IFRAME_READY` handshake.
9
- * - `useCrosshairCursor` — crosshair cursor + disable text/drag selection
10
- * while in selection mode.
11
- * - `useSelectionInteractions` — pointer-down / move / up state machine
12
- * that produces `ELEMENT_SELECTED`, `AREA_SELECTED`, and live
13
- * `DRAG_RECT_UPDATED` messages.
14
- * - `useSelectionHover` — hover-element rect updates.
15
- * - `useSelectionScrollSync` — keeps the parent overlay aligned with the
16
- * selected element/area when the iframe scrolls.
17
- *
18
- * The `interactionSessionRef` is the single source of truth for the
19
- * in-progress drag/selection — every hook reads/writes it via ref so we
20
- * don't trigger React re-renders during interaction-heavy operations.
21
- */
22
- export declare function EditorSelection(): null;
1
+ export declare function EditorSelection(): import("react/jsx-runtime").JSX.Element;
@@ -1,39 +1,29 @@
1
1
  "use client";
2
- import { useCallback, useEffect } from "react";
3
- import {
4
- buildElementDescriptor,
5
- buildSelector
6
- } from "./descriptor/elementDescriptor";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect } from "react";
7
4
  import { installFastRefreshRelay } from "./fast-refresh-relay";
8
- import { useCrosshairCursor } from "./hooks/useCrosshairCursor";
9
- import { useEditModeStateMachine } from "./hooks/useEditModeStateMachine";
10
5
  import { useOptimisticOverrides } from "./hooks/useOptimisticOverrides";
11
6
  import { useParentMessaging } from "./hooks/useParentMessaging";
12
7
  import { useSelectionHover } from "./hooks/useSelectionHover";
13
8
  import { useSelectionInteractions } from "./hooks/useSelectionInteractions";
14
9
  import { useSelectionScrollSync } from "./hooks/useSelectionScrollSync";
15
- import { useSelectModeController } from "./hooks/useSelectModeController";
16
- import { getRepetitionSiblingElements } from "./hooks/useSiblingResolution";
17
- import { getWebsiteBuilderRpc } from "./iframeRpcChannel";
18
- import { getDocumentScrollOffset } from "./interactions/selectionGeometry";
19
10
  import { PREVIEW_RENDERED_EVENT } from "./previewRendered";
20
- import { showElementSelectionFrames } from "./selectionFrame";
21
- import { getConnectedSelectedElement } from "./selectModeSession";
11
+ import { SelectionFrames } from "./SelectionFrames";
22
12
  import {
23
- isUserSourceLocation,
24
- symbolicateSelectedElement
25
- } from "./symbolicate";
13
+ useDisplayMode,
14
+ useSelectModeStore,
15
+ useSelectModeStoreActions
16
+ } from "./selectModeStore";
26
17
  function EditorSelection() {
18
+ const store = useSelectModeStore();
19
+ const displayMode = useDisplayMode();
27
20
  const {
28
- displayMode,
29
- interactionSessionRef,
21
+ formatCommand,
22
+ postSelectedElementSnapshot,
23
+ revertInlineEdit,
30
24
  setDisplayMode,
31
25
  setSelectionType
32
- } = useSelectModeController();
33
- const { beginEditing, editingElementRef, formatCommand, revert } = useEditModeStateMachine({
34
- displayMode,
35
- interactionSessionRef
36
- });
26
+ } = useSelectModeStoreActions();
37
27
  const {
38
28
  applyOptimisticUpdate,
39
29
  markCommitted,
@@ -41,56 +31,6 @@ function EditorSelection() {
41
31
  resolveSessionOverrides,
42
32
  revertOverride
43
33
  } = useOptimisticOverrides();
44
- const postSelectedElementSnapshot = useCallback(
45
- ({ clearMissing = false } = {}) => {
46
- const session = interactionSessionRef.current;
47
- if (session.type !== "element") {
48
- return true;
49
- }
50
- const element = getConnectedSelectedElement(session);
51
- if (!element) {
52
- if (!clearMissing) return false;
53
- setSelectionType({ selectionType: "none" });
54
- getWebsiteBuilderRpc()?.areaSelected({
55
- rect: session.data.rect,
56
- items: []
57
- });
58
- return true;
59
- }
60
- const siblingElements = getRepetitionSiblingElements({ element });
61
- const descriptor = buildElementDescriptor({
62
- element,
63
- clickPosition: session.clickPosition ?? (session.data.kind === "react-element" ? session.data.clickPosition : { x: 0, y: 0 }),
64
- siblingSelectors: siblingElements.map(buildSelector)
65
- });
66
- if (descriptor.kind !== "react-element") {
67
- return true;
68
- }
69
- const refreshedSession = {
70
- ...session,
71
- element,
72
- siblingElements,
73
- data: session.data
74
- };
75
- interactionSessionRef.current = refreshedSession;
76
- showElementSelectionFrames({
77
- elements: [element, ...siblingElements],
78
- scrollOffset: getDocumentScrollOffset()
79
- });
80
- void symbolicateSelectedElement(descriptor).then((symbolicated) => {
81
- if (interactionSessionRef.current !== refreshedSession) {
82
- return;
83
- }
84
- if (symbolicated.sourceLocation && !isUserSourceLocation(symbolicated.sourceLocation)) {
85
- return;
86
- }
87
- refreshedSession.data = symbolicated;
88
- getWebsiteBuilderRpc()?.elementSelected({ data: symbolicated });
89
- });
90
- return true;
91
- },
92
- [interactionSessionRef, setSelectionType]
93
- );
94
34
  useEffect(() => {
95
35
  let refreshTimeout;
96
36
  let missingTimeout;
@@ -98,15 +38,15 @@ function EditorSelection() {
98
38
  const scheduleSnapshot = () => {
99
39
  clearTimeout(refreshTimeout);
100
40
  refreshTimeout = setTimeout(() => {
101
- if (editingElementRef.current) return;
41
+ if (store.getState().editingElement) return;
102
42
  if (postSelectedElementSnapshot()) {
103
43
  clearTimeout(missingTimeout);
104
44
  missingTimeout = void 0;
105
45
  } else if (!missingTimeout) {
106
- const missingSession = interactionSessionRef.current;
46
+ const missingSession = store.getState().interactionSession;
107
47
  missingTimeout = setTimeout(() => {
108
48
  missingTimeout = void 0;
109
- if (interactionSessionRef.current === missingSession) {
49
+ if (store.getState().interactionSession === missingSession) {
110
50
  postSelectedElementSnapshot({ clearMissing: true });
111
51
  }
112
52
  }, 5e3);
@@ -114,14 +54,14 @@ function EditorSelection() {
114
54
  }, 250);
115
55
  };
116
56
  const observer = new MutationObserver((records) => {
117
- const session = interactionSessionRef.current;
118
- if (editingElementRef.current || session.type !== "element") return;
57
+ const session = store.getState().interactionSession;
58
+ if (store.getState().editingElement || session.type !== "element") return;
119
59
  if (!session.element.isConnected || records.some((record) => session.element.contains(record.target))) {
120
60
  scheduleSnapshot();
121
61
  }
122
62
  });
123
63
  const watchRender = () => {
124
- if (editingElementRef.current || interactionSessionRef.current.type !== "element")
64
+ if (store.getState().editingElement || store.getState().interactionSession.type !== "element")
125
65
  return;
126
66
  clearTimeout(observationTimeout);
127
67
  observer.observe(document.body, {
@@ -133,7 +73,7 @@ function EditorSelection() {
133
73
  };
134
74
  installFastRefreshRelay({
135
75
  onComplete: () => {
136
- reapplyOverrides({ skipElement: editingElementRef.current });
76
+ reapplyOverrides({ skipElement: store.getState().editingElement });
137
77
  watchRender();
138
78
  }
139
79
  });
@@ -146,35 +86,56 @@ function EditorSelection() {
146
86
  window.removeEventListener(PREVIEW_RENDERED_EVENT, watchRender);
147
87
  installFastRefreshRelay();
148
88
  };
149
- }, [
150
- editingElementRef,
151
- interactionSessionRef,
152
- postSelectedElementSnapshot,
153
- reapplyOverrides
154
- ]);
89
+ }, [store, postSelectedElementSnapshot, reapplyOverrides]);
90
+ useEffect(() => {
91
+ const cancelOnPointerDown = (event) => {
92
+ const { editingElement, inlineEditRequest, actions } = store.getState();
93
+ if (event.button === 0 && editingElement && event.target instanceof Node && !editingElement.contains(event.target)) {
94
+ actions.commitInlineEdit();
95
+ }
96
+ if (inlineEditRequest && event.target instanceof Node && !inlineEditRequest.element.contains(event.target)) {
97
+ actions.dismissInlineEditRequest();
98
+ }
99
+ };
100
+ const cancelOnEscape = (event) => {
101
+ const { inlineEditRequest, actions } = store.getState();
102
+ if (!inlineEditRequest || event.key !== "Escape") {
103
+ return;
104
+ }
105
+ if (inlineEditRequest.status === "pending") {
106
+ event.preventDefault();
107
+ }
108
+ actions.dismissInlineEditRequest();
109
+ };
110
+ document.addEventListener("pointerdown", cancelOnPointerDown, true);
111
+ document.addEventListener("keydown", cancelOnEscape, true);
112
+ return () => {
113
+ document.removeEventListener("pointerdown", cancelOnPointerDown, true);
114
+ document.removeEventListener("keydown", cancelOnEscape, true);
115
+ store.getState().actions.revertInlineEdit();
116
+ };
117
+ }, [store]);
155
118
  useParentMessaging({
156
119
  applyOptimisticUpdate,
157
120
  markCommitted,
158
- onExitEditMode: revert,
121
+ onExitEditMode: revertInlineEdit,
159
122
  onFormatCommand: formatCommand,
160
123
  resolveSessionOverrides,
161
124
  revertOverride,
162
125
  setDisplayMode,
163
126
  setSelectionType
164
127
  });
165
- useCrosshairCursor({ displayMode });
166
- useSelectionInteractions({
167
- displayMode,
168
- editingElementRef,
169
- interactionSessionRef,
170
- onBeginEditing: beginEditing
171
- });
172
- useSelectionHover({ displayMode, interactionSessionRef });
173
- useSelectionScrollSync({
174
- displayMode,
175
- interactionSessionRef
176
- });
177
- return null;
128
+ useSelectionInteractions();
129
+ useSelectionHover();
130
+ useSelectionScrollSync();
131
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
132
+ displayMode === "selection" && /* @__PURE__ */ jsx("style", { children: `
133
+ * { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }
134
+ [data-select-mode-inline-editor][contenteditable="true"],
135
+ [data-select-mode-inline-editor][contenteditable="true"] * { cursor: text !important; }
136
+ ` }),
137
+ /* @__PURE__ */ jsx(SelectionFrames, {})
138
+ ] });
178
139
  }
179
140
  export {
180
141
  EditorSelection
@@ -0,0 +1 @@
1
+ export declare function SelectionFrames(): import("react").ReactPortal | null;
@@ -0,0 +1,58 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { createPortal } from "react-dom";
4
+ import {
5
+ SELECTION_FRAME_BORDER_HEX,
6
+ SELECTION_FRAME_EDITING_BORDER_HEX,
7
+ SELECTION_FRAMES_CONTAINER_ID
8
+ } from "./selectionFrame";
9
+ import {
10
+ useIsSelectionFrameEditing,
11
+ useSelectionFrames
12
+ } from "./selectModeStore";
13
+ const CONTAINER_STYLE = {
14
+ position: "absolute",
15
+ top: 0,
16
+ left: 0,
17
+ width: 0,
18
+ height: 0,
19
+ margin: 0,
20
+ padding: 0,
21
+ pointerEvents: "none",
22
+ zIndex: 2147483646
23
+ };
24
+ function SelectionFrames() {
25
+ const frames = useSelectionFrames();
26
+ const isEditing = useIsSelectionFrameEditing();
27
+ if (typeof document === "undefined" || !document.body || frames.length === 0) {
28
+ return null;
29
+ }
30
+ const borderColor = isEditing ? SELECTION_FRAME_EDITING_BORDER_HEX : SELECTION_FRAME_BORDER_HEX;
31
+ return createPortal(
32
+ /* @__PURE__ */ jsx("div", { id: SELECTION_FRAMES_CONTAINER_ID, "aria-hidden": true, style: CONTAINER_STYLE, children: frames.map((frame, index) => /* @__PURE__ */ jsx(
33
+ "div",
34
+ {
35
+ "aria-hidden": true,
36
+ style: {
37
+ position: "absolute",
38
+ boxSizing: "border-box",
39
+ borderRadius: 2,
40
+ pointerEvents: "none",
41
+ margin: 0,
42
+ padding: 0,
43
+ left: frame.pageRect.x,
44
+ top: frame.pageRect.y,
45
+ width: frame.pageRect.width,
46
+ height: frame.pageRect.height,
47
+ border: `2px solid ${borderColor}`,
48
+ background: frame.variant === "area" ? "rgba(192, 38, 211, 0.08)" : "transparent"
49
+ }
50
+ },
51
+ `${frame.variant}-${index}`
52
+ )) }),
53
+ document.body
54
+ );
55
+ }
56
+ export {
57
+ SelectionFrames
58
+ };
@@ -1,3 +1,16 @@
1
+ /**
2
+ * In-iframe annotation frame renderer — `selectionFrame`'s sibling surface.
3
+ *
4
+ * Same architecture and reasons as selection frames: page-coordinate
5
+ * `position: absolute` nodes in a body-level container, so document scroll
6
+ * translates them with zero JavaScript per scroll frame, and styling is
7
+ * vanilla inline CSS because customer pages have no tailwind.
8
+ *
9
+ * An annotation frame is an invisible positioning box over the annotated
10
+ * element carrying a numbered badge dot — the dot is the entire visual.
11
+ * Unlike selection frames these rebuild wholesale on each call: re-renders
12
+ * happen on set/re-measure (rare), never per scroll, so pooling buys nothing.
13
+ */
1
14
  export declare const ANNOTATION_CONTAINER_ID = "replo-annotation-frames";
2
15
  interface AnnotationRect {
3
16
  x: number;
@@ -1,5 +1,35 @@
1
- import { getOrCreateFramesLayerContainer } from "./selectionFrame";
2
1
  const ANNOTATION_CONTAINER_ID = "replo-annotation-frames";
2
+ function getOrCreateFramesLayerContainer({
3
+ id,
4
+ zIndex
5
+ }) {
6
+ if (typeof document === "undefined") {
7
+ return null;
8
+ }
9
+ const existing = document.getElementById(id);
10
+ if (existing instanceof HTMLDivElement) {
11
+ return existing;
12
+ }
13
+ if (!document.body) {
14
+ return null;
15
+ }
16
+ const container = document.createElement("div");
17
+ container.id = id;
18
+ container.setAttribute("aria-hidden", "true");
19
+ container.style.cssText = [
20
+ "position: absolute",
21
+ "top: 0",
22
+ "left: 0",
23
+ "width: 0",
24
+ "height: 0",
25
+ "margin: 0",
26
+ "padding: 0",
27
+ "pointer-events: none",
28
+ `z-index: ${zIndex}`
29
+ ].join(";");
30
+ document.body.append(container);
31
+ return container;
32
+ }
3
33
  const ANNOTATION_FONT_STYLE_ID = "replo-annotation-font";
4
34
  const BADGE_SIZE_PX = 30;
5
35
  const EDITORIAL_ITALIC_FONT_URL = "https://assets.replocdn.com/PPEditorialNew-Italic.woff2";
@@ -110,7 +110,7 @@ function buildSelector(element) {
110
110
  const tag = currentElement.tagName.toLowerCase();
111
111
  const id = currentElement.id;
112
112
  if (id) {
113
- parts.unshift(`#${id}`);
113
+ parts.unshift(`#${CSS.escape(id)}`);
114
114
  break;
115
115
  }
116
116
  const parentElement = currentElement.parentElement;
@@ -4,7 +4,5 @@ export declare const displayModeStore: {
4
4
  getServerSnapshot(): DisplayMode;
5
5
  subscribe(listener: () => void): () => void;
6
6
  setDisplayMode(nextDisplayMode: DisplayMode): void;
7
- /** Test-only: reset back to the default so specs don't leak state. */
8
7
  reset(): void;
9
8
  };
10
- export declare function useDisplayMode(): DisplayMode;