@replohq/sandbox-runtime 1.7.10 → 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 (39) 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/displayModeStore.d.ts +0 -2
  15. package/select-mode/displayModeStore.js +11 -30
  16. package/select-mode/hooks/useParentMessaging.js +3 -0
  17. package/select-mode/hooks/useSelectionHover.d.ts +1 -19
  18. package/select-mode/hooks/useSelectionHover.js +37 -49
  19. package/select-mode/hooks/useSelectionInteractions.d.ts +1 -12
  20. package/select-mode/hooks/useSelectionInteractions.js +57 -39
  21. package/select-mode/hooks/useSelectionScrollSync.d.ts +1 -25
  22. package/select-mode/hooks/useSelectionScrollSync.js +48 -40
  23. package/select-mode/iframeRpcChannel.d.ts +24 -1
  24. package/select-mode/inlineEditFeedback.d.ts +6 -0
  25. package/select-mode/inlineEditFeedback.js +47 -0
  26. package/select-mode/isTextEditableElement.d.ts +1 -0
  27. package/select-mode/isTextEditableElement.js +15 -0
  28. package/select-mode/selectModeStore.d.ts +160 -0
  29. package/select-mode/selectModeStore.js +585 -0
  30. package/select-mode/selectionFrame.d.ts +10 -59
  31. package/select-mode/selectionFrame.js +26 -150
  32. package/select-mode/symbolicate.js +4 -0
  33. package/select-mode/tiptapInlineEditor.js +16 -1
  34. package/select-mode/hooks/useCrosshairCursor.d.ts +0 -10
  35. package/select-mode/hooks/useCrosshairCursor.js +0 -23
  36. package/select-mode/hooks/useEditModeStateMachine.d.ts +0 -16
  37. package/select-mode/hooks/useEditModeStateMachine.js +0 -228
  38. package/select-mode/hooks/useSelectModeController.d.ts +0 -11
  39. package/select-mode/hooks/useSelectModeController.js +0 -72
@@ -1,164 +1,40 @@
1
- const FUCHSIA_HEX = "#c026d3";
2
- const EDITING_FUCHSIA_HEX = "#e879f9";
3
- const FRAMES_CONTAINER_ID = "replo-select-mode-selection-frames";
4
- let isEditing = false;
5
- function getFrameBorderHex() {
6
- return isEditing ? EDITING_FUCHSIA_HEX : FUCHSIA_HEX;
7
- }
8
- function applyVariantStyle({
9
- frame,
1
+ const SELECTION_FRAMES_CONTAINER_ID = "replo-select-mode-selection-frames";
2
+ const SELECTION_FRAME_BORDER_HEX = "#c026d3";
3
+ const SELECTION_FRAME_EDITING_BORDER_HEX = "#e879f9";
4
+ function getFrameSpecs({
5
+ elements,
6
+ scrollOffset,
10
7
  variant
11
8
  }) {
12
- frame.style.border = `2px solid ${getFrameBorderHex()}`;
13
- frame.style.background = variant === "area" ? "rgba(192, 38, 211, 0.08)" : "transparent";
14
- }
15
- function setSelectionFrameEditing(editing) {
16
- if (isEditing === editing) {
17
- return;
18
- }
19
- isEditing = editing;
20
- if (typeof document === "undefined") {
21
- return;
22
- }
23
- const container = document.getElementById(FRAMES_CONTAINER_ID);
24
- if (!(container instanceof HTMLDivElement)) {
25
- return;
26
- }
27
- for (const child of container.children) {
28
- if (child instanceof HTMLDivElement) {
29
- child.style.borderColor = getFrameBorderHex();
30
- }
31
- }
32
- }
33
- function getOrCreateFramesLayerContainer({
34
- id,
35
- zIndex
36
- }) {
37
- if (typeof document === "undefined") {
38
- return null;
39
- }
40
- const existing = document.getElementById(id);
41
- if (existing instanceof HTMLDivElement) {
42
- return existing;
43
- }
44
- if (!document.body) {
45
- return null;
46
- }
47
- const container = document.createElement("div");
48
- container.id = id;
49
- container.setAttribute("aria-hidden", "true");
50
- container.style.cssText = [
51
- "position: absolute",
52
- "top: 0",
53
- "left: 0",
54
- "width: 0",
55
- "height: 0",
56
- "margin: 0",
57
- "padding: 0",
58
- "pointer-events: none",
59
- `z-index: ${zIndex}`
60
- ].join(";");
61
- document.body.append(container);
62
- return container;
63
- }
64
- function getOrCreateContainer() {
65
- return getOrCreateFramesLayerContainer({
66
- id: FRAMES_CONTAINER_ID,
67
- zIndex: 2147483646
9
+ return elements.map((element) => {
10
+ const rect = element.getBoundingClientRect();
11
+ return {
12
+ variant,
13
+ pageRect: {
14
+ x: rect.left + scrollOffset.left,
15
+ y: rect.top + scrollOffset.top,
16
+ width: rect.width,
17
+ height: rect.height
18
+ }
19
+ };
68
20
  });
69
21
  }
70
- function createFrameNode() {
71
- const frame = document.createElement("div");
72
- frame.setAttribute("aria-hidden", "true");
73
- frame.style.cssText = [
74
- "position: absolute",
75
- "top: 0",
76
- "left: 0",
77
- "box-sizing: border-box",
78
- "border-radius: 2px",
79
- "pointer-events: none",
80
- "margin: 0",
81
- "padding: 0"
82
- ].join(";");
83
- return frame;
84
- }
85
- function showSelectionFrames({
86
- frames
87
- }) {
88
- const container = getOrCreateContainer();
89
- if (!container) {
90
- return;
91
- }
92
- while (container.childElementCount > frames.length) {
93
- container.lastElementChild?.remove();
94
- }
95
- while (container.childElementCount < frames.length) {
96
- container.append(createFrameNode());
97
- }
98
- frames.forEach((spec, index) => {
99
- const frame = container.children[index];
100
- if (!(frame instanceof HTMLDivElement)) {
101
- return;
102
- }
103
- applyVariantStyle({ frame, variant: spec.variant });
104
- frame.style.left = `${spec.pageRect.x}px`;
105
- frame.style.top = `${spec.pageRect.y}px`;
106
- frame.style.width = `${spec.pageRect.width}px`;
107
- frame.style.height = `${spec.pageRect.height}px`;
108
- });
109
- }
110
- function showElementSelectionFrames({
22
+ function getElementSelectionFrameSpecs({
111
23
  elements,
112
24
  scrollOffset
113
25
  }) {
114
- showSelectionFrames({
115
- frames: elements.map((element) => {
116
- const rect = element.getBoundingClientRect();
117
- return {
118
- variant: "element",
119
- pageRect: {
120
- x: rect.left + scrollOffset.left,
121
- y: rect.top + scrollOffset.top,
122
- width: rect.width,
123
- height: rect.height
124
- }
125
- };
126
- })
127
- });
26
+ return getFrameSpecs({ elements, scrollOffset, variant: "element" });
128
27
  }
129
- function showAreaSelectionFrames({
28
+ function getAreaSelectionFrameSpecs({
130
29
  elements,
131
30
  scrollOffset
132
31
  }) {
133
- showSelectionFrames({
134
- frames: elements.map((element) => {
135
- const rect = element.getBoundingClientRect();
136
- return {
137
- variant: "area",
138
- pageRect: {
139
- x: rect.left + scrollOffset.left,
140
- y: rect.top + scrollOffset.top,
141
- width: rect.width,
142
- height: rect.height
143
- }
144
- };
145
- })
146
- });
147
- }
148
- function hideSelectionFrame() {
149
- if (typeof document === "undefined") {
150
- return;
151
- }
152
- const container = document.getElementById(FRAMES_CONTAINER_ID);
153
- if (container instanceof HTMLDivElement) {
154
- container.replaceChildren();
155
- }
32
+ return getFrameSpecs({ elements, scrollOffset, variant: "area" });
156
33
  }
157
34
  export {
158
- getOrCreateFramesLayerContainer,
159
- hideSelectionFrame,
160
- setSelectionFrameEditing,
161
- showAreaSelectionFrames,
162
- showElementSelectionFrames,
163
- showSelectionFrames
35
+ SELECTION_FRAMES_CONTAINER_ID,
36
+ SELECTION_FRAME_BORDER_HEX,
37
+ SELECTION_FRAME_EDITING_BORDER_HEX,
38
+ getAreaSelectionFrameSpecs,
39
+ getElementSelectionFrameSpecs
164
40
  };
@@ -47,6 +47,10 @@ function normalizeFilePathForSymbolicator(filePath) {
47
47
  let normalized = filePath;
48
48
  if (normalized.startsWith("about://React/Server/")) {
49
49
  normalized = normalized.slice("about://React/Server/".length);
50
+ try {
51
+ normalized = decodeURI(normalized);
52
+ } catch {
53
+ }
50
54
  }
51
55
  normalized = normalized.replace(/\?\d+$/, "");
52
56
  return rewriteBrowserChunkUrlToDistPath(normalized);
@@ -1,4 +1,4 @@
1
- import { Editor, Mark } from "@tiptap/core";
1
+ import { Editor, Mark, Node } from "@tiptap/core";
2
2
  import Bold from "@tiptap/extension-bold";
3
3
  import Document from "@tiptap/extension-document";
4
4
  import Italic from "@tiptap/extension-italic";
@@ -8,6 +8,20 @@ import Underline from "@tiptap/extension-underline";
8
8
  const InlineDocument = Document.extend({
9
9
  content: "inline*"
10
10
  });
11
+ const InlineBreak = Node.create({
12
+ name: "hardBreak",
13
+ inline: true,
14
+ group: "inline",
15
+ selectable: false,
16
+ parseHTML: () => [{ tag: "br" }],
17
+ renderHTML: () => ["br"],
18
+ renderText: () => "\n",
19
+ addKeyboardShortcuts() {
20
+ return {
21
+ Enter: () => this.editor.commands.insertContent({ type: this.name })
22
+ };
23
+ }
24
+ });
11
25
  const InlineLink = Mark.create({
12
26
  name: "link",
13
27
  inclusive: false,
@@ -51,6 +65,7 @@ function createInlineTiptapEditor({
51
65
  extensions: [
52
66
  InlineDocument,
53
67
  Text,
68
+ InlineBreak,
54
69
  Bold,
55
70
  Italic,
56
71
  Underline,
@@ -1,10 +0,0 @@
1
- import type { DisplayMode } from "../../_vendor/schemas/generated/selectMode";
2
- /**
3
- * Forces a crosshair cursor + disables text selection / drag while the iframe
4
- * is in `selection` mode. We inject a `<style>` tag into the iframe document
5
- * (rather than toggling a class on `<html>`) so that the `!important` rule
6
- * wins against any per-element cursor styling on the page being edited.
7
- */
8
- export declare function useCrosshairCursor({ displayMode, }: {
9
- displayMode: DisplayMode;
10
- }): void;
@@ -1,23 +0,0 @@
1
- import { useEffect } from "react";
2
- function useCrosshairCursor({
3
- displayMode
4
- }) {
5
- useEffect(() => {
6
- if (displayMode !== "selection") {
7
- return;
8
- }
9
- const styleElement = document.createElement("style");
10
- styleElement.textContent = `
11
- * { cursor: crosshair !important; user-select: none !important; -webkit-user-drag: none !important; }
12
- [data-select-mode-inline-editor][contenteditable="true"],
13
- [data-select-mode-inline-editor][contenteditable="true"] * { cursor: text !important; }
14
- `;
15
- document.head.append(styleElement);
16
- return () => {
17
- styleElement.remove();
18
- };
19
- }, [displayMode]);
20
- }
21
- export {
22
- useCrosshairCursor
23
- };
@@ -1,16 +0,0 @@
1
- import type { MutableRefObject } from "react";
2
- import type { DisplayMode, SelectableData, SelectModeFormatCommand } from "../../_vendor/schemas/generated/selectMode";
3
- import type { SelectModeInteractionSession } from "../selectModeSession";
4
- export declare function isTextEditableElement(element: Element): element is HTMLElement;
5
- export declare function useEditModeStateMachine({ displayMode, interactionSessionRef, }: {
6
- displayMode: DisplayMode;
7
- interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;
8
- }): {
9
- beginEditing: ({ element, descriptorPromise, }: {
10
- element: Element;
11
- descriptorPromise: Promise<SelectableData>;
12
- }) => void;
13
- editingElementRef: import("react").RefObject<HTMLElement | null>;
14
- formatCommand: (command: SelectModeFormatCommand) => void;
15
- revert: () => void;
16
- };
@@ -1,228 +0,0 @@
1
- import { useCallback, useEffect, useRef } from "react";
2
- import {
3
- getWebsiteBuilderRpc,
4
- requestInlineTextEdit
5
- } from "../iframeRpcChannel";
6
- import { setSelectionFrameEditing } from "../selectionFrame";
7
- import { symbolicateSelectedElement } from "../symbolicate";
8
- import { createInlineTiptapEditor } from "../tiptapInlineEditor";
9
- import { getRepetitionSiblingElements } from "./useSiblingResolution";
10
- const EMPTY_ACTIVE_MARKS = {
11
- bold: false,
12
- italic: false,
13
- underline: false,
14
- strike: false
15
- };
16
- function postEditorState({
17
- activeMarks,
18
- isEditing
19
- }) {
20
- getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });
21
- }
22
- function isTextEditableElement(element) {
23
- const tagName = element.tagName.toLowerCase();
24
- return element instanceof HTMLElement && Boolean(element.textContent?.trim()) && ![
25
- "input",
26
- "textarea",
27
- "select",
28
- "option",
29
- "img",
30
- "video",
31
- "canvas"
32
- ].includes(tagName);
33
- }
34
- const FOCUS_STEAL_GUARD_MS = 1500;
35
- function useEditModeStateMachine({
36
- displayMode,
37
- interactionSessionRef
38
- }) {
39
- const editorRef = useRef(null);
40
- const editingElementRef = useRef(null);
41
- const editIdRef = useRef(null);
42
- const pendingEditRef = useRef(null);
43
- const originalInnerHTMLRef = useRef(null);
44
- const initialHTMLRef = useRef(null);
45
- const isCommittingRef = useRef(false);
46
- const beginEditingAtRef = useRef(0);
47
- const destroyEditor = useCallback(() => {
48
- editorRef.current?.destroy();
49
- editorRef.current = null;
50
- editingElementRef.current = null;
51
- editIdRef.current = null;
52
- if (pendingEditRef.current) {
53
- clearTimeout(pendingEditRef.current.timeout);
54
- pendingEditRef.current = null;
55
- }
56
- originalInnerHTMLRef.current = null;
57
- initialHTMLRef.current = null;
58
- isCommittingRef.current = false;
59
- setSelectionFrameEditing(false);
60
- postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });
61
- }, []);
62
- const commit = useCallback(() => {
63
- const editor = editorRef.current;
64
- const editingElement = editingElementRef.current;
65
- const originalInnerHTML = originalInnerHTMLRef.current;
66
- const editId = editIdRef.current;
67
- if (!editor || !editId || originalInnerHTML === null || isCommittingRef.current) {
68
- return;
69
- }
70
- isCommittingRef.current = true;
71
- const committedHTML = editor.getHTML();
72
- if (committedHTML === initialHTMLRef.current) {
73
- destroyEditor();
74
- if (editingElement) {
75
- editingElement.innerHTML = originalInnerHTML;
76
- }
77
- return;
78
- }
79
- const committedJSON = editor.getJSON();
80
- getWebsiteBuilderRpc()?.textCommitted({
81
- editId,
82
- html: committedHTML,
83
- json: committedJSON,
84
- originalInnerHTML
85
- });
86
- destroyEditor();
87
- if (editingElement) {
88
- editingElement.innerHTML = committedHTML;
89
- }
90
- }, [destroyEditor]);
91
- const revert = useCallback(() => {
92
- const editingElement = editingElementRef.current;
93
- const originalInnerHTML = originalInnerHTMLRef.current;
94
- destroyEditor();
95
- if (editingElement && originalInnerHTML !== null) {
96
- editingElement.innerHTML = originalInnerHTML;
97
- }
98
- }, [destroyEditor]);
99
- const handleBlur = useCallback(() => {
100
- if (isCommittingRef.current) {
101
- return;
102
- }
103
- if (typeof document !== "undefined" && document.hasFocus()) {
104
- commit();
105
- return;
106
- }
107
- if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {
108
- editorRef.current?.focus();
109
- }
110
- }, [commit]);
111
- const formatCommand = useCallback((command) => {
112
- editorRef.current?.runFormatCommand(command);
113
- }, []);
114
- const clearPendingEdit = useCallback((editId) => {
115
- if (pendingEditRef.current?.editId !== editId) {
116
- return;
117
- }
118
- clearTimeout(pendingEditRef.current.timeout);
119
- pendingEditRef.current = null;
120
- }, []);
121
- const resolveEditRequest = useCallback(
122
- ({ editId, allowed }) => {
123
- const pendingEdit = pendingEditRef.current;
124
- if (!pendingEdit || pendingEdit.editId !== editId) {
125
- return;
126
- }
127
- clearTimeout(pendingEdit.timeout);
128
- pendingEditRef.current = null;
129
- if (!allowed || !pendingEdit.element.isConnected) {
130
- return;
131
- }
132
- const element = pendingEdit.element;
133
- const originalInnerHTML = element.innerHTML;
134
- editingElementRef.current = element;
135
- editIdRef.current = editId;
136
- originalInnerHTMLRef.current = originalInnerHTML;
137
- editorRef.current = createInlineTiptapEditor({
138
- element,
139
- content: originalInnerHTML,
140
- onCommit: commit,
141
- onEscape: revert,
142
- onBlur: handleBlur,
143
- onSelectionUpdate: (activeMarks) => {
144
- postEditorState({ activeMarks, isEditing: true });
145
- }
146
- });
147
- initialHTMLRef.current = editorRef.current.getHTML();
148
- beginEditingAtRef.current = Date.now();
149
- editorRef.current.focusAndSelectAll();
150
- setSelectionFrameEditing(true);
151
- postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });
152
- },
153
- [commit, handleBlur, revert]
154
- );
155
- const beginEditing = useCallback(
156
- ({
157
- element,
158
- descriptorPromise
159
- }) => {
160
- if (editingElementRef.current === element) {
161
- return;
162
- }
163
- if (!isTextEditableElement(element)) {
164
- return;
165
- }
166
- if (pendingEditRef.current?.element === element) {
167
- return;
168
- }
169
- void descriptorPromise.then((descriptor) => {
170
- if (descriptor.kind !== "react-element" || !element.isConnected) {
171
- return;
172
- }
173
- interactionSessionRef.current = {
174
- type: "element",
175
- element,
176
- siblingElements: getRepetitionSiblingElements({ element }),
177
- clickPosition: null,
178
- data: descriptor
179
- };
180
- const editId = crypto.randomUUID();
181
- if (pendingEditRef.current) {
182
- clearTimeout(pendingEditRef.current.timeout);
183
- }
184
- const timeout = window.setTimeout(() => {
185
- clearPendingEdit(editId);
186
- }, 5e3);
187
- pendingEditRef.current = { editId, element, timeout };
188
- void symbolicateSelectedElement(descriptor).then(
189
- (symbolicated) => {
190
- if (pendingEditRef.current?.editId !== editId) {
191
- return;
192
- }
193
- void requestInlineTextEdit({
194
- editId,
195
- element: symbolicated
196
- }).then(
197
- ({ allowed }) => {
198
- resolveEditRequest({ editId, allowed });
199
- },
200
- () => {
201
- resolveEditRequest({ editId, allowed: false });
202
- }
203
- );
204
- },
205
- () => {
206
- clearPendingEdit(editId);
207
- }
208
- );
209
- });
210
- },
211
- [clearPendingEdit, interactionSessionRef, resolveEditRequest]
212
- );
213
- useEffect(() => {
214
- if (displayMode !== "selection") {
215
- revert();
216
- }
217
- }, [displayMode, revert]);
218
- return {
219
- beginEditing,
220
- editingElementRef,
221
- formatCommand,
222
- revert
223
- };
224
- }
225
- export {
226
- isTextEditableElement,
227
- useEditModeStateMachine
228
- };
@@ -1,11 +0,0 @@
1
- import type { DisplayMode, SelectModeSelectionType } from "../../_vendor/schemas/generated/selectMode";
2
- import type { SelectModeInteractionSession } from "../selectModeSession";
3
- export declare function useSelectModeController(): {
4
- displayMode: DisplayMode;
5
- interactionSessionRef: import("react").RefObject<SelectModeInteractionSession>;
6
- setDisplayMode: (nextDisplayMode: DisplayMode) => void;
7
- setSelectionType: ({ selectionType, selectors, }: {
8
- selectionType: SelectModeSelectionType;
9
- selectors?: string[];
10
- }) => void;
11
- };
@@ -1,72 +0,0 @@
1
- import { useCallback, useRef } from "react";
2
- import { displayModeStore, useDisplayMode } from "../displayModeStore";
3
- import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
4
- import { getDocumentScrollOffset } from "../interactions/selectionGeometry";
5
- import { hideSelectionFrame, showAreaSelectionFrames } from "../selectionFrame";
6
- import { IDLE_SESSION } from "../selectModeSession";
7
- import { resetHoverRectDedup } from "./useSelectionHover";
8
- function resetInteractionSession({
9
- interactionSessionRef
10
- }) {
11
- const currentSession = interactionSessionRef.current;
12
- if (currentSession.type === "drag" && currentSession.hasExceededThreshold) {
13
- getWebsiteBuilderRpc()?.dragRectUpdated({
14
- dragRect: null,
15
- selectionRects: []
16
- });
17
- }
18
- hideSelectionFrame();
19
- resetHoverRectDedup();
20
- interactionSessionRef.current = IDLE_SESSION;
21
- }
22
- function useSelectModeController() {
23
- const displayMode = useDisplayMode();
24
- const interactionSessionRef = useRef(IDLE_SESSION);
25
- const setSelectionType = useCallback(
26
- ({
27
- selectionType,
28
- selectors
29
- }) => {
30
- if (selectionType === "none") {
31
- resetInteractionSession({ interactionSessionRef });
32
- return;
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
- });
54
- },
55
- []
56
- );
57
- const setDisplayMode = useCallback((nextDisplayMode) => {
58
- if (displayModeStore.getSnapshot() === "selection" && nextDisplayMode !== "selection") {
59
- resetInteractionSession({ interactionSessionRef });
60
- }
61
- displayModeStore.setDisplayMode(nextDisplayMode);
62
- }, []);
63
- return {
64
- displayMode,
65
- interactionSessionRef,
66
- setDisplayMode,
67
- setSelectionType
68
- };
69
- }
70
- export {
71
- useSelectModeController
72
- };