@replohq/sandbox-runtime 1.6.8 → 1.7.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.
- package/_vendor/replo-utils/lib/viewportSnapshot.d.ts +28 -7
- package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
- package/_vendor/replo-utils/react/use-viewport-snapshot-responder.d.ts +2 -3
- package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +18 -2
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +1 -0
- package/_vendor/schemas/selectModeIframeRpc.d.ts +2 -0
- package/_vendor/schemas/selectModeIframeRpc.mjs +11 -6
- package/buildMetadata.js +3 -3
- package/{chunk-RXYLMSNM.mjs → chunk-3YA3QXUQ.mjs} +27 -9
- package/package.json +4 -4
- package/select-mode/hooks/useEditModeStateMachine.js +14 -22
- package/select-mode/hooks/useEditModeStateMachine.js.map +2 -2
- package/select-mode/hooks/useParentMessaging.d.ts +4 -1
- package/select-mode/hooks/useParentMessaging.js +2 -2
- package/select-mode/hooks/useParentMessaging.js.map +2 -2
- package/select-mode/hooks/useSelectModeController.d.ts +4 -1
- package/select-mode/hooks/useSelectModeController.js +27 -2
- package/select-mode/hooks/useSelectModeController.js.map +2 -2
- package/select-mode/hooks/useSelectionInteractions.js +153 -53
- package/select-mode/hooks/useSelectionInteractions.js.map +2 -2
- package/select-mode/interactions/selectionGeometry.d.ts +25 -0
- package/select-mode/interactions/selectionGeometry.js +83 -1
- package/select-mode/interactions/selectionGeometry.js.map +2 -2
- package/select-mode/selectModeSession.d.ts +9 -1
- package/select-mode/selectModeSession.js +15 -1
- package/select-mode/selectModeSession.js.map +2 -2
- package/select-mode/selectionFrame.d.ts +7 -0
- package/select-mode/selectionFrame.js +20 -0
- package/select-mode/selectionFrame.js.map +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export declare const VIEWPORT_SNAPSHOT_REQUEST = "replo:viewport-snapshot:request";
|
|
2
2
|
export declare const VIEWPORT_SNAPSHOT_RESPONSE = "replo:viewport-snapshot:response";
|
|
3
|
+
export declare const VIEWPORT_CONTEXT_REQUEST = "replo:viewport-context:request";
|
|
4
|
+
export declare const VIEWPORT_CONTEXT_RESPONSE = "replo:viewport-context:response";
|
|
3
5
|
/**
|
|
4
6
|
* Levels of iframe nesting a capture request fans out to below the requester.
|
|
5
7
|
* 2 covers the deepest product surface: agent-ui → miniapp → builder preview.
|
|
6
8
|
*/
|
|
7
9
|
export declare const VIEWPORT_SNAPSHOT_MAX_DEPTH = 2;
|
|
8
10
|
export interface ViewportSnapshotRequestMessage {
|
|
9
|
-
type: typeof VIEWPORT_SNAPSHOT_REQUEST;
|
|
11
|
+
type: typeof VIEWPORT_SNAPSHOT_REQUEST | typeof VIEWPORT_CONTEXT_REQUEST;
|
|
10
12
|
requestId: string;
|
|
11
13
|
frameIndex: number;
|
|
12
14
|
remainingDepth: number;
|
|
@@ -21,6 +23,19 @@ export interface ViewportFrameRect {
|
|
|
21
23
|
export interface ViewportFrameCapture {
|
|
22
24
|
snapshot: unknown;
|
|
23
25
|
childFrames: ViewportChildFrame[];
|
|
26
|
+
context?: ViewportFrameContext;
|
|
27
|
+
}
|
|
28
|
+
export interface ViewportFrameContext {
|
|
29
|
+
viewport: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
};
|
|
33
|
+
scroll: {
|
|
34
|
+
x: number;
|
|
35
|
+
y: number;
|
|
36
|
+
maximumX: number;
|
|
37
|
+
maximumY: number;
|
|
38
|
+
};
|
|
24
39
|
}
|
|
25
40
|
export interface ViewportChildFrame {
|
|
26
41
|
frameIndex: number;
|
|
@@ -28,7 +43,7 @@ export interface ViewportChildFrame {
|
|
|
28
43
|
capture: ViewportFrameCapture;
|
|
29
44
|
}
|
|
30
45
|
export interface ViewportSnapshotResponseMessage {
|
|
31
|
-
type: typeof VIEWPORT_SNAPSHOT_RESPONSE;
|
|
46
|
+
type: typeof VIEWPORT_SNAPSHOT_RESPONSE | typeof VIEWPORT_CONTEXT_RESPONSE;
|
|
32
47
|
requestId: string;
|
|
33
48
|
frameIndex: number;
|
|
34
49
|
capture: ViewportFrameCapture | null;
|
|
@@ -62,24 +77,30 @@ export declare function isTrustedViewportSnapshotRequester(origin: string): bool
|
|
|
62
77
|
* at the timeout with whatever arrived — a frame running an older runtime
|
|
63
78
|
* simply never answers and is omitted.
|
|
64
79
|
*/
|
|
65
|
-
export declare function requestChildViewportSnapshots({ targetWindow, requestId, timeoutMs, remainingDepth, }: {
|
|
80
|
+
export declare function requestChildViewportSnapshots({ targetWindow, requestId, timeoutMs, remainingDepth, requestType, }: {
|
|
66
81
|
targetWindow: RelayWindow;
|
|
67
82
|
requestId: string;
|
|
68
83
|
timeoutMs: number;
|
|
69
84
|
remainingDepth?: number;
|
|
85
|
+
requestType?: ViewportSnapshotRequestMessage["type"];
|
|
70
86
|
}): Promise<{
|
|
71
87
|
frameIndex: number;
|
|
72
88
|
capture: ViewportFrameCapture;
|
|
73
89
|
}[]>;
|
|
90
|
+
export declare function requestChildViewportContexts(input: Parameters<typeof requestChildViewportSnapshots>[0]): Promise<{
|
|
91
|
+
frameIndex: number;
|
|
92
|
+
capture: ViewportFrameCapture;
|
|
93
|
+
}[]>;
|
|
74
94
|
/**
|
|
75
|
-
* Child-side responder: on a trusted parent request,
|
|
76
|
-
*
|
|
77
|
-
*
|
|
95
|
+
* Child-side responder: on a trusted parent request, capture this frame's DOM
|
|
96
|
+
* or lightweight viewing context, cascade to child frames, and answer only the
|
|
97
|
+
* requesting origin.
|
|
78
98
|
*/
|
|
79
|
-
export declare function startViewportSnapshotResponder({ targetWindow, takeSnapshot, measureFrameRect, isTrustedRequester, cascadeTimeoutMs, }: {
|
|
99
|
+
export declare function startViewportSnapshotResponder({ targetWindow, takeSnapshot, measureFrameRect, measureViewportContext, isTrustedRequester, cascadeTimeoutMs, }: {
|
|
80
100
|
targetWindow: RelayWindow;
|
|
81
101
|
takeSnapshot: () => unknown;
|
|
82
102
|
measureFrameRect: (frameIndex: number) => ViewportFrameRect | null;
|
|
103
|
+
measureViewportContext?: () => ViewportFrameContext;
|
|
83
104
|
isTrustedRequester?: (origin: string) => boolean;
|
|
84
105
|
cascadeTimeoutMs?: number;
|
|
85
106
|
}): () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportSnapshots, startViewportSnapshotResponder } from '../../../chunk-
|
|
1
|
+
export { VIEWPORT_CONTEXT_REQUEST, VIEWPORT_CONTEXT_RESPONSE, VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportContexts, requestChildViewportSnapshots, startViewportSnapshotResponder } from '../../../chunk-3YA3QXUQ.mjs';
|
|
@@ -2,8 +2,7 @@ import type { ViewportFrameRect } from "../lib/viewportSnapshot";
|
|
|
2
2
|
/** Relay frameIndex → on-screen rect; window.frames order is iframe document order. */
|
|
3
3
|
export declare function measureViewportFrameRect(frameIndex: number): ViewportFrameRect | null;
|
|
4
4
|
/**
|
|
5
|
-
* Answers
|
|
6
|
-
*
|
|
7
|
-
* iframes. Passive until asked; see docs/agent-session-viewport-filmstrip-prd.md.
|
|
5
|
+
* Answers trusted parent requests with either an rrweb DOM capture or compact
|
|
6
|
+
* viewport measurements, cascading into this frame's own iframes.
|
|
8
7
|
*/
|
|
9
8
|
export declare function useViewportSnapshotResponder(): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { startViewportSnapshotResponder } from '../../../chunk-
|
|
1
|
+
import { startViewportSnapshotResponder } from '../../../chunk-3YA3QXUQ.mjs';
|
|
2
2
|
import { useEffect } from 'react';
|
|
3
3
|
|
|
4
4
|
// ../../node_modules/.pnpm/rrweb-snapshot@2.1.1/node_modules/rrweb-snapshot/dist/rrweb-snapshot.js
|
|
@@ -5244,6 +5244,21 @@ function measureViewportFrameRect(frameIndex) {
|
|
|
5244
5244
|
const { x, y, width, height } = element.getBoundingClientRect();
|
|
5245
5245
|
return { x, y, width, height };
|
|
5246
5246
|
}
|
|
5247
|
+
function measureViewportFrameContext() {
|
|
5248
|
+
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
|
5249
|
+
const scrollingElement = document.scrollingElement ?? document.documentElement;
|
|
5250
|
+
const maximumX = Math.max(0, scrollingElement.scrollWidth - viewport.width);
|
|
5251
|
+
const maximumY = Math.max(0, scrollingElement.scrollHeight - viewport.height);
|
|
5252
|
+
return {
|
|
5253
|
+
viewport,
|
|
5254
|
+
scroll: {
|
|
5255
|
+
x: Math.min(maximumX, Math.max(0, Math.round(window.scrollX))),
|
|
5256
|
+
y: Math.min(maximumY, Math.max(0, Math.round(window.scrollY))),
|
|
5257
|
+
maximumX,
|
|
5258
|
+
maximumY
|
|
5259
|
+
}
|
|
5260
|
+
};
|
|
5261
|
+
}
|
|
5247
5262
|
function useViewportSnapshotResponder() {
|
|
5248
5263
|
useEffect(() => {
|
|
5249
5264
|
if (typeof window === "undefined") {
|
|
@@ -5254,7 +5269,8 @@ function useViewportSnapshotResponder() {
|
|
|
5254
5269
|
takeSnapshot: () => {
|
|
5255
5270
|
return snapshot(document, { maskAllInputs: true });
|
|
5256
5271
|
},
|
|
5257
|
-
measureFrameRect: measureViewportFrameRect
|
|
5272
|
+
measureFrameRect: measureViewportFrameRect,
|
|
5273
|
+
measureViewportContext: measureViewportFrameContext
|
|
5258
5274
|
});
|
|
5259
5275
|
}, []);
|
|
5260
5276
|
}
|
|
@@ -216,6 +216,7 @@ export declare const websiteBuilderToCanopyMethodConfig: {
|
|
|
216
216
|
drag: "drag";
|
|
217
217
|
none: "none";
|
|
218
218
|
}>;
|
|
219
|
+
selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
219
220
|
}, z.core.$strip>;
|
|
220
221
|
outputSchema: z.ZodVoid;
|
|
221
222
|
};
|
|
@@ -714,6 +715,7 @@ export declare const selectModeParentMessageSchema: z.ZodDiscriminatedUnion<[z.Z
|
|
|
714
715
|
drag: "drag";
|
|
715
716
|
none: "none";
|
|
716
717
|
}>;
|
|
718
|
+
selectors: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
717
719
|
}, z.core.$strip>;
|
|
718
720
|
}, z.core.$strip>, z.ZodObject<{
|
|
719
721
|
method: z.ZodLiteral<"applyOptimisticUpdate">;
|
|
@@ -33,13 +33,18 @@ var websiteBuilderToCanopyMethodConfig = {
|
|
|
33
33
|
inputSchema: z.object({ displayMode: displayModeSchema }),
|
|
34
34
|
outputSchema: z.void()
|
|
35
35
|
},
|
|
36
|
-
// Declarative push of the parent's current selection
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
36
|
+
// Declarative push of the parent's current selection into the canopy
|
|
37
|
+
// iframe. Transitioning to `none` is canopy's signal to hide its
|
|
38
|
+
// in-iframe selection frame and drop any in-progress drag — there's no
|
|
39
|
+
// separate "clear" message because selection state always flows through
|
|
40
|
+
// agentUiState. For an `area` selection the parent may also send the
|
|
41
|
+
// selectors of the members it still holds, so an item removed in the
|
|
42
|
+
// editor is dropped from the in-iframe selection too.
|
|
41
43
|
setSelectionType: {
|
|
42
|
-
inputSchema: z.object({
|
|
44
|
+
inputSchema: z.object({
|
|
45
|
+
selectionType: selectModeSelectionTypeSchema,
|
|
46
|
+
selectors: z.array(z.string()).optional()
|
|
47
|
+
}),
|
|
43
48
|
outputSchema: z.void()
|
|
44
49
|
},
|
|
45
50
|
applyOptimisticUpdate: {
|
package/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-09-
|
|
5
|
+
"commit": "a518bd507469206a903941ac3dde233c966036de",
|
|
6
|
+
"builtAt": "2026-09-15T05:05:19.645Z"
|
|
7
7
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// ../replo-utils/lib/viewportSnapshot.ts
|
|
2
2
|
var VIEWPORT_SNAPSHOT_REQUEST = "replo:viewport-snapshot:request";
|
|
3
3
|
var VIEWPORT_SNAPSHOT_RESPONSE = "replo:viewport-snapshot:response";
|
|
4
|
+
var VIEWPORT_CONTEXT_REQUEST = "replo:viewport-context:request";
|
|
5
|
+
var VIEWPORT_CONTEXT_RESPONSE = "replo:viewport-context:response";
|
|
4
6
|
var VIEWPORT_SNAPSHOT_MAX_DEPTH = 2;
|
|
5
7
|
function isTrustedViewportSnapshotRequester(origin) {
|
|
6
8
|
let hostname;
|
|
@@ -21,16 +23,17 @@ function canReceiveMessages(value) {
|
|
|
21
23
|
return typeof value === "object" && value !== null && "postMessage" in value && typeof value.postMessage === "function";
|
|
22
24
|
}
|
|
23
25
|
function isResponseMessage(data) {
|
|
24
|
-
return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_RESPONSE && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number";
|
|
26
|
+
return typeof data === "object" && data !== null && "type" in data && (data.type === VIEWPORT_SNAPSHOT_RESPONSE || data.type === VIEWPORT_CONTEXT_RESPONSE) && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number";
|
|
25
27
|
}
|
|
26
28
|
function isRequestMessage(data) {
|
|
27
|
-
return typeof data === "object" && data !== null && "type" in data && data.type === VIEWPORT_SNAPSHOT_REQUEST && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number" && "remainingDepth" in data && typeof data.remainingDepth === "number";
|
|
29
|
+
return typeof data === "object" && data !== null && "type" in data && (data.type === VIEWPORT_SNAPSHOT_REQUEST || data.type === VIEWPORT_CONTEXT_REQUEST) && "requestId" in data && typeof data.requestId === "string" && "frameIndex" in data && typeof data.frameIndex === "number" && "remainingDepth" in data && typeof data.remainingDepth === "number";
|
|
28
30
|
}
|
|
29
31
|
function requestChildViewportSnapshots({
|
|
30
32
|
targetWindow,
|
|
31
33
|
requestId,
|
|
32
34
|
timeoutMs,
|
|
33
|
-
remainingDepth = VIEWPORT_SNAPSHOT_MAX_DEPTH
|
|
35
|
+
remainingDepth = VIEWPORT_SNAPSHOT_MAX_DEPTH,
|
|
36
|
+
requestType = VIEWPORT_SNAPSHOT_REQUEST
|
|
34
37
|
}) {
|
|
35
38
|
const frameCount = targetWindow.frames.length;
|
|
36
39
|
if (frameCount === 0 || remainingDepth <= 0) {
|
|
@@ -64,7 +67,7 @@ function requestChildViewportSnapshots({
|
|
|
64
67
|
const timeoutId = targetWindow.setTimeout(settle, timeoutMs);
|
|
65
68
|
for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
|
|
66
69
|
const message = {
|
|
67
|
-
type:
|
|
70
|
+
type: requestType,
|
|
68
71
|
requestId,
|
|
69
72
|
frameIndex,
|
|
70
73
|
remainingDepth
|
|
@@ -73,10 +76,17 @@ function requestChildViewportSnapshots({
|
|
|
73
76
|
}
|
|
74
77
|
});
|
|
75
78
|
}
|
|
79
|
+
function requestChildViewportContexts(input) {
|
|
80
|
+
return requestChildViewportSnapshots({
|
|
81
|
+
...input,
|
|
82
|
+
requestType: VIEWPORT_CONTEXT_REQUEST
|
|
83
|
+
});
|
|
84
|
+
}
|
|
76
85
|
function startViewportSnapshotResponder({
|
|
77
86
|
targetWindow,
|
|
78
87
|
takeSnapshot,
|
|
79
88
|
measureFrameRect,
|
|
89
|
+
measureViewportContext,
|
|
80
90
|
isTrustedRequester = isTrustedViewportSnapshotRequester,
|
|
81
91
|
cascadeTimeoutMs = 1500
|
|
82
92
|
}) {
|
|
@@ -89,11 +99,12 @@ function startViewportSnapshotResponder({
|
|
|
89
99
|
return;
|
|
90
100
|
}
|
|
91
101
|
const { requestId, frameIndex, remainingDepth } = event.data;
|
|
102
|
+
const captureDom = event.data.type === VIEWPORT_SNAPSHOT_REQUEST;
|
|
92
103
|
const requester = event.source;
|
|
93
104
|
const requesterOrigin = event.origin ?? "*";
|
|
94
105
|
const respond = (capture) => {
|
|
95
106
|
const response = {
|
|
96
|
-
type: VIEWPORT_SNAPSHOT_RESPONSE,
|
|
107
|
+
type: captureDom ? VIEWPORT_SNAPSHOT_RESPONSE : VIEWPORT_CONTEXT_RESPONSE,
|
|
97
108
|
requestId,
|
|
98
109
|
frameIndex,
|
|
99
110
|
capture
|
|
@@ -101,11 +112,16 @@ function startViewportSnapshotResponder({
|
|
|
101
112
|
requester.postMessage(response, requesterOrigin);
|
|
102
113
|
};
|
|
103
114
|
let snapshot = null;
|
|
115
|
+
let context;
|
|
104
116
|
try {
|
|
105
|
-
|
|
117
|
+
if (!captureDom) {
|
|
118
|
+
context = measureViewportContext?.();
|
|
119
|
+
} else {
|
|
120
|
+
snapshot = takeSnapshot();
|
|
121
|
+
}
|
|
106
122
|
} catch {
|
|
107
123
|
}
|
|
108
|
-
if (snapshot === null) {
|
|
124
|
+
if (captureDom ? snapshot === null : !context) {
|
|
109
125
|
respond(null);
|
|
110
126
|
return;
|
|
111
127
|
}
|
|
@@ -114,10 +130,12 @@ function startViewportSnapshotResponder({
|
|
|
114
130
|
// Derived per level so a child's own fan-out can't collide with ours.
|
|
115
131
|
requestId: `${requestId}:${frameIndex}`,
|
|
116
132
|
timeoutMs: cascadeTimeoutMs,
|
|
117
|
-
remainingDepth: remainingDepth - 1
|
|
133
|
+
remainingDepth: remainingDepth - 1,
|
|
134
|
+
requestType: event.data.type
|
|
118
135
|
}).then((childCaptures) => {
|
|
119
136
|
respond({
|
|
120
137
|
snapshot,
|
|
138
|
+
...context ? { context } : {},
|
|
121
139
|
childFrames: childCaptures.flatMap((child) => {
|
|
122
140
|
const rect = measureFrameRect(child.frameIndex);
|
|
123
141
|
return rect ? [{ frameIndex: child.frameIndex, rect, capture: child.capture }] : [];
|
|
@@ -131,4 +149,4 @@ function startViewportSnapshotResponder({
|
|
|
131
149
|
};
|
|
132
150
|
}
|
|
133
151
|
|
|
134
|
-
export { VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportSnapshots, startViewportSnapshotResponder };
|
|
152
|
+
export { VIEWPORT_CONTEXT_REQUEST, VIEWPORT_CONTEXT_RESPONSE, VIEWPORT_SNAPSHOT_MAX_DEPTH, VIEWPORT_SNAPSHOT_REQUEST, VIEWPORT_SNAPSHOT_RESPONSE, isTrustedViewportSnapshotRequester, requestChildViewportContexts, requestChildViewportSnapshots, startViewportSnapshotResponder };
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sandbox-runtime",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.7.1",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-09-
|
|
8
|
+
"commit": "a518bd507469206a903941ac3dde233c966036de",
|
|
9
|
+
"builtAt": "2026-09-15T05:05:19.645Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "../iframeRpcChannel";
|
|
6
6
|
import { setSelectionFrameEditing } from "../selectionFrame";
|
|
7
7
|
import { symbolicateSelectedElement } from "../symbolicate";
|
|
8
|
+
import { createInlineTiptapEditor } from "../tiptapInlineEditor";
|
|
8
9
|
import { getRepetitionSiblingElements } from "./useSiblingResolution";
|
|
9
10
|
const EMPTY_ACTIVE_MARKS = {
|
|
10
11
|
bold: false,
|
|
@@ -133,30 +134,21 @@ function useEditModeStateMachine({
|
|
|
133
134
|
editingElementRef.current = element;
|
|
134
135
|
editIdRef.current = editId;
|
|
135
136
|
originalInnerHTMLRef.current = originalInnerHTML;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
onEscape: revert,
|
|
145
|
-
onBlur: handleBlur,
|
|
146
|
-
onSelectionUpdate: (activeMarks) => {
|
|
147
|
-
postEditorState({ activeMarks, isEditing: true });
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
initialHTMLRef.current = editorRef.current.getHTML();
|
|
151
|
-
beginEditingAtRef.current = Date.now();
|
|
152
|
-
editorRef.current.focusAndSelectAll();
|
|
153
|
-
setSelectionFrameEditing(true);
|
|
154
|
-
postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });
|
|
155
|
-
}).catch(() => {
|
|
156
|
-
if (editingElementRef.current === element && editIdRef.current === editId) {
|
|
157
|
-
revert();
|
|
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 });
|
|
158
145
|
}
|
|
159
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 });
|
|
160
152
|
},
|
|
161
153
|
[commit, handleBlur, revert]
|
|
162
154
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../select-mode/hooks/useEditModeStateMachine.ts"],
|
|
4
|
-
"sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n void import(\"../tiptapInlineEditor\")\n .then(({ createInlineTiptapEditor }) => {\n if (\n editingElementRef.current !== element ||\n editIdRef.current !== editId\n ) {\n return;\n }\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n })\n .catch(() => {\n if (\n editingElementRef.current === element &&\n editIdRef.current === editId\n ) {\n revert();\n }\n });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n siblingElements: getRepetitionSiblingElements({ element }),\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
|
|
5
|
-
"mappings": "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
|
|
4
|
+
"sourcesContent": ["import type { MutableRefObject } from \"react\";\nimport type {\n DisplayMode,\n SelectableData,\n SelectModeFormatCommand,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\nimport type { InlineTiptapEditor } from \"../tiptapInlineEditor\";\n\nimport { useCallback, useEffect, useRef } from \"react\";\n\nimport {\n getWebsiteBuilderRpc,\n requestInlineTextEdit,\n} from \"../iframeRpcChannel\";\nimport { setSelectionFrameEditing } from \"../selectionFrame\";\nimport { symbolicateSelectedElement } from \"../symbolicate\";\nimport { createInlineTiptapEditor } from \"../tiptapInlineEditor\";\nimport { getRepetitionSiblingElements } from \"./useSiblingResolution\";\n\ntype ActiveMarks = {\n bold: boolean;\n italic: boolean;\n underline: boolean;\n strike: boolean;\n};\n\nconst EMPTY_ACTIVE_MARKS: ActiveMarks = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n};\n\n// Notify the parent of the inline editor's state so it can show the inline\n// formatting controls (and live mark state) only while editing.\nfunction postEditorState({\n activeMarks,\n isEditing,\n}: {\n activeMarks: ActiveMarks;\n isEditing: boolean;\n}) {\n getWebsiteBuilderRpc()?.editorStateUpdate({ activeMarks, isEditing });\n}\n\nexport function isTextEditableElement(\n element: Element,\n): element is HTMLElement {\n const tagName = element.tagName.toLowerCase();\n return (\n element instanceof HTMLElement &&\n Boolean(element.textContent?.trim()) &&\n ![\n \"input\",\n \"textarea\",\n \"select\",\n \"option\",\n \"img\",\n \"video\",\n \"canvas\",\n ].includes(tagName)\n );\n}\n\n// Window after entering an edit during which a focus loss to the parent frame\n// is treated as the select-mode panel's composer auto-focus stealing focus\n// (and is reclaimed) rather than a deliberate move into the editor UI.\nconst FOCUS_STEAL_GUARD_MS = 1500;\n\nexport function useEditModeStateMachine({\n displayMode,\n interactionSessionRef,\n}: {\n displayMode: DisplayMode;\n interactionSessionRef: MutableRefObject<SelectModeInteractionSession>;\n}) {\n const editorRef = useRef<InlineTiptapEditor | null>(null);\n const editingElementRef = useRef<HTMLElement | null>(null);\n const editIdRef = useRef<string | null>(null);\n const pendingEditRef = useRef<{\n editId: string;\n element: HTMLElement;\n timeout: number;\n } | null>(null);\n const originalInnerHTMLRef = useRef<string | null>(null);\n // The editor's HTML right after it parsed the original content. tiptap\n // re-serializes the markup, so this \u2014 not the raw `originalInnerHTML` \u2014 is the\n // apples-to-apples baseline for detecting whether a commit actually changed\n // anything.\n const initialHTMLRef = useRef<string | null>(null);\n const isCommittingRef = useRef(false);\n const beginEditingAtRef = useRef(0);\n\n const destroyEditor = useCallback(() => {\n editorRef.current?.destroy();\n editorRef.current = null;\n editingElementRef.current = null;\n editIdRef.current = null;\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }\n originalInnerHTMLRef.current = null;\n initialHTMLRef.current = null;\n isCommittingRef.current = false;\n setSelectionFrameEditing(false);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: false });\n }, []);\n\n const commit = useCallback(() => {\n const editor = editorRef.current;\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n const editId = editIdRef.current;\n if (\n !editor ||\n !editId ||\n originalInnerHTML === null ||\n isCommittingRef.current\n ) {\n return;\n }\n\n isCommittingRef.current = true;\n const committedHTML = editor.getHTML();\n\n // A blur or Shift-Enter with no actual change must not spawn an edit\n // session. When the content is untouched, restore the original markup and\n // tear down without notifying the parent.\n if (committedHTML === initialHTMLRef.current) {\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = originalInnerHTML;\n }\n return;\n }\n\n const committedJSON = editor.getJSON();\n getWebsiteBuilderRpc()?.textCommitted({\n editId,\n html: committedHTML,\n json: committedJSON,\n originalInnerHTML,\n });\n // NOTE (Noah Gilmore, 2026-07-16): A directly mounted ProseMirror view\n // clears its root during destroy, so final HTML must be restored afterward.\n destroyEditor();\n if (editingElement) {\n editingElement.innerHTML = committedHTML;\n }\n }, [destroyEditor]);\n\n const revert = useCallback(() => {\n const editingElement = editingElementRef.current;\n const originalInnerHTML = originalInnerHTMLRef.current;\n destroyEditor();\n if (editingElement && originalInnerHTML !== null) {\n editingElement.innerHTML = originalInnerHTML;\n }\n }, [destroyEditor]);\n\n const handleBlur = useCallback(() => {\n if (isCommittingRef.current) {\n return;\n }\n // Focus stayed inside the iframe \u2014 the user clicked elsewhere on the page,\n // so this is a real \"click away\": commit.\n if (typeof document !== \"undefined\" && document.hasFocus()) {\n commit();\n return;\n }\n // Focus left the iframe to the parent editor. When it happens right as the\n // edit opens, the select-mode panel's composer auto-focus stole it \u2014 pull\n // it back so a double-click keeps the caret in the text. A deliberate move\n // into the editor UI later is left alone, and never commits/destroys.\n if (Date.now() - beginEditingAtRef.current < FOCUS_STEAL_GUARD_MS) {\n editorRef.current?.focus();\n }\n }, [commit]);\n\n const formatCommand = useCallback((command: SelectModeFormatCommand) => {\n editorRef.current?.runFormatCommand(command);\n }, []);\n\n const clearPendingEdit = useCallback((editId: string) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n clearTimeout(pendingEditRef.current.timeout);\n pendingEditRef.current = null;\n }, []);\n\n const resolveEditRequest = useCallback(\n ({ editId, allowed }: { editId: string; allowed: boolean }) => {\n const pendingEdit = pendingEditRef.current;\n if (!pendingEdit || pendingEdit.editId !== editId) {\n return;\n }\n clearTimeout(pendingEdit.timeout);\n pendingEditRef.current = null;\n if (!allowed || !pendingEdit.element.isConnected) {\n return;\n }\n\n const element = pendingEdit.element;\n const originalInnerHTML = element.innerHTML;\n editingElementRef.current = element;\n editIdRef.current = editId;\n originalInnerHTMLRef.current = originalInnerHTML;\n\n editorRef.current = createInlineTiptapEditor({\n element,\n content: originalInnerHTML,\n onCommit: commit,\n onEscape: revert,\n onBlur: handleBlur,\n onSelectionUpdate: (activeMarks) => {\n postEditorState({ activeMarks, isEditing: true });\n },\n });\n initialHTMLRef.current = editorRef.current.getHTML();\n beginEditingAtRef.current = Date.now();\n editorRef.current.focusAndSelectAll();\n setSelectionFrameEditing(true);\n postEditorState({ activeMarks: EMPTY_ACTIVE_MARKS, isEditing: true });\n },\n [commit, handleBlur, revert],\n );\n\n const beginEditing = useCallback(\n ({\n element,\n descriptorPromise,\n }: {\n element: Element;\n descriptorPromise: Promise<SelectableData>;\n }) => {\n // Already editing this element \u2014 re-entering would re-read the mid-edit\n // DOM as the \"original\" and rebuild the editor over itself, wiping the\n // content. The pointer-up guard prevents the usual trigger; this is the\n // backstop for any other path.\n if (editingElementRef.current === element) {\n return;\n }\n if (!isTextEditableElement(element)) {\n return;\n }\n // Same element already awaiting symbolicate / parent allow \u2014 ignore.\n // A different element supersedes the in-flight attempt below.\n if (pendingEditRef.current?.element === element) {\n return;\n }\n\n void descriptorPromise.then((descriptor) => {\n if (descriptor.kind !== \"react-element\" || !element.isConnected) {\n return;\n }\n interactionSessionRef.current = {\n type: \"element\",\n element,\n siblingElements: getRepetitionSiblingElements({ element }),\n clickPosition: null,\n data: descriptor,\n };\n const editId = crypto.randomUUID();\n if (pendingEditRef.current) {\n clearTimeout(pendingEditRef.current.timeout);\n }\n // Symbolicate has no AbortSignal; a wedged sandbox can hang forever.\n // Self-clear so a hung attempt can't permanently block inline edit.\n const timeout = window.setTimeout(() => {\n clearPendingEdit(editId);\n }, 5000);\n pendingEditRef.current = { editId, element, timeout };\n void symbolicateSelectedElement(descriptor).then(\n (symbolicated) => {\n if (pendingEditRef.current?.editId !== editId) {\n return;\n }\n void requestInlineTextEdit({\n editId,\n element: symbolicated,\n }).then(\n ({ allowed }) => {\n resolveEditRequest({ editId, allowed });\n },\n () => {\n resolveEditRequest({ editId, allowed: false });\n },\n );\n },\n () => {\n clearPendingEdit(editId);\n },\n );\n });\n },\n [clearPendingEdit, interactionSessionRef, resolveEditRequest],\n );\n\n // eslint-disable-next-line replo/no-use-effect -- Tears down the inline editor when leaving selection mode.\n useEffect(() => {\n if (displayMode !== \"selection\") {\n revert();\n }\n }, [displayMode, revert]);\n\n return {\n beginEditing,\n editingElementRef,\n formatCommand,\n revert,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -32,5 +32,8 @@ export declare function useParentMessaging({ applyOptimisticUpdate, markCommitte
|
|
|
32
32
|
onFormatCommand?: (command: SelectModeFormatCommand) => void;
|
|
33
33
|
revertOverride?: (editId: string) => void;
|
|
34
34
|
setDisplayMode: (displayMode: DisplayMode) => void;
|
|
35
|
-
setSelectionType: (
|
|
35
|
+
setSelectionType: (input: {
|
|
36
|
+
selectionType: SelectModeSelectionType;
|
|
37
|
+
selectors?: string[];
|
|
38
|
+
}) => void;
|
|
36
39
|
}): void;
|
|
@@ -40,8 +40,8 @@ function useParentMessaging({
|
|
|
40
40
|
setDisplayMode: ({ displayMode }) => {
|
|
41
41
|
setDisplayMode(displayMode);
|
|
42
42
|
},
|
|
43
|
-
setSelectionType: (
|
|
44
|
-
setSelectionType(
|
|
43
|
+
setSelectionType: (input) => {
|
|
44
|
+
setSelectionType(input);
|
|
45
45
|
},
|
|
46
46
|
applyOptimisticUpdate: (message) => {
|
|
47
47
|
applyOptimisticUpdate?.({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../select-mode/hooks/useParentMessaging.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\nimport { iframeRuntimeBuildMetadataSchema } from \"schemas/selectModeIframeRpc\";\nimport { z } from \"zod\";\n\nimport { BUILD_METADATA } from \"../../buildMetadata\";\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\nconst SDK_BUILD_METADATA_SPECIFIER = \"@replohq/sdk/lib/buildMetadata\";\n\nconst sdkBuildMetadataModuleSchema = z.object({\n BUILD_METADATA: iframeRuntimeBuildMetadataSchema,\n});\n\nasync function announceSdkBuildMetadata() {\n try {\n const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(\n await import(SDK_BUILD_METADATA_SPECIFIER),\n );\n if (sdkBuildMetadata.success) {\n getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);\n }\n } catch {\n // Sites that haven't installed @replohq/sdk still handshake with sandbox-runtime alone.\n }\n}\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (selectionType: SelectModeSelectionType) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: (
|
|
5
|
-
"mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,+BAA+B;AAErC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB;AAClB,CAAC;AAED,eAAe,2BAA2B;AACxC,MAAI;AACF,UAAM,mBAAmB,6BAA6B;AAAA,MACpD,MAAM,OAAO;AAAA,IACf;AACA,QAAI,iBAAiB,SAAS;AAC5B,2BAAqB,GAAG,YAAY,iBAAiB,KAAK,cAAc;AAAA,IAC1E;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAqBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,
|
|
4
|
+
"sourcesContent": ["import type {\n DisplayMode,\n SelectModeFormatCommand,\n SelectModeOptimisticUpdatePayload,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\n\nimport { useEffect, useMemo } from \"react\";\n\nimport { useIframeRpcChannel } from \"replo-utils/react/use-iframe-rpc-channel\";\nimport { iframeRuntimeBuildMetadataSchema } from \"schemas/selectModeIframeRpc\";\nimport { z } from \"zod\";\n\nimport { BUILD_METADATA } from \"../../buildMetadata\";\nimport {\n getSelectModeIframeRpcChannel,\n getWebsiteBuilderRpc,\n setSelectModeParentHandlers,\n} from \"../iframeRpcChannel\";\n\nconst SDK_BUILD_METADATA_SPECIFIER = \"@replohq/sdk/lib/buildMetadata\";\n\nconst sdkBuildMetadataModuleSchema = z.object({\n BUILD_METADATA: iframeRuntimeBuildMetadataSchema,\n});\n\nasync function announceSdkBuildMetadata() {\n try {\n const sdkBuildMetadata = sdkBuildMetadataModuleSchema.safeParse(\n await import(SDK_BUILD_METADATA_SPECIFIER),\n );\n if (sdkBuildMetadata.success) {\n getWebsiteBuilderRpc()?.iframeReady(sdkBuildMetadata.data.BUILD_METADATA);\n }\n } catch {\n // Sites that haven't installed @replohq/sdk still handshake with sandbox-runtime alone.\n }\n}\n\n/**\n * Inbound side of the iframe<->parent selectMode bridge.\n *\n * Listens for Website Builder \u2192 canopy methods and applies them locally:\n * - `setDisplayMode`: switch the iframe between `browse` and `selection`.\n * - `setSelectionType`: declarative push of the parent's current\n * selection type. The interesting transition is `* \u2192 \"none\"`, which\n * tears down any in-iframe selection frame / in-progress drag \u2014 agent-ui\n * drives it from `agentUiState.selectionType` so panel dismiss / submit\n * flow through the same path as exiting selection mode entirely. The\n * regression test in `EditorSelection.spec.tsx` covers the case where\n * losing this used to lock the iframe out of further selections.\n *\n * Also fires `iframeReady` on mount so the parent knows it's safe to push\n * the initial `setDisplayMode` \u2014 without this handshake the parent would\n * race the iframe's mount and the first message could be lost.\n *\n * @author Kevin O'Connell 2026-04-17\n */\nexport function useParentMessaging({\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n}: {\n applyOptimisticUpdate?: (input: {\n editId: string;\n payload: SelectModeOptimisticUpdatePayload;\n targetSelector: string;\n targetMode: \"primary\" | \"group\";\n }) => void;\n markCommitted?: (editId: string, sessionId: string) => void;\n resolveSessionOverrides?: (\n sessionId: string,\n outcome: \"persisted\" | \"failed\",\n ) => void;\n onEnterEditMode?: () => void;\n onExitEditMode?: () => void;\n onFormatCommand?: (command: SelectModeFormatCommand) => void;\n revertOverride?: (editId: string) => void;\n setDisplayMode: (displayMode: DisplayMode) => void;\n setSelectionType: (input: {\n selectionType: SelectModeSelectionType;\n selectors?: string[];\n }) => void;\n}) {\n useIframeRpcChannel(useMemo(() => getSelectModeIframeRpcChannel(), []));\n\n // eslint-disable-next-line replo/no-use-effect -- Legacy effect - may or may not be necessary\n useEffect(() => {\n setSelectModeParentHandlers({\n setDisplayMode: ({ displayMode }) => {\n setDisplayMode(displayMode);\n },\n setSelectionType: (input) => {\n setSelectionType(input);\n },\n applyOptimisticUpdate: (message) => {\n applyOptimisticUpdate?.({\n editId: message.editId,\n payload: message.payload,\n targetSelector: message.targetSelector,\n targetMode: message.targetMode,\n });\n },\n markCommitted: ({ editId, sessionId }) => {\n markCommitted?.(editId, sessionId);\n },\n resolveSessionOverrides: ({ sessionId, outcome }) => {\n resolveSessionOverrides?.(sessionId, outcome);\n },\n revertOverride: ({ editId }) => {\n revertOverride?.(editId);\n },\n enterEditMode: () => {\n onEnterEditMode?.();\n },\n exitEditMode: () => {\n onExitEditMode?.();\n },\n formatCommand: ({ command }) => {\n onFormatCommand?.(command);\n },\n });\n const rpc = getWebsiteBuilderRpc();\n rpc?.iframeReady(BUILD_METADATA);\n void announceSdkBuildMetadata();\n\n return () => {\n setSelectModeParentHandlers(null);\n };\n }, [\n applyOptimisticUpdate,\n markCommitted,\n resolveSessionOverrides,\n onEnterEditMode,\n onExitEditMode,\n onFormatCommand,\n revertOverride,\n setDisplayMode,\n setSelectionType,\n ]);\n}\n"],
|
|
5
|
+
"mappings": "AAOA,SAAS,WAAW,eAAe;AAEnC,SAAS,2BAA2B;AACpC,SAAS,wCAAwC;AACjD,SAAS,SAAS;AAElB,SAAS,sBAAsB;AAC/B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,MAAM,+BAA+B;AAErC,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,gBAAgB;AAClB,CAAC;AAED,eAAe,2BAA2B;AACxC,MAAI;AACF,UAAM,mBAAmB,6BAA6B;AAAA,MACpD,MAAM,OAAO;AAAA,IACf;AACA,QAAI,iBAAiB,SAAS;AAC5B,2BAAqB,GAAG,YAAY,iBAAiB,KAAK,cAAc;AAAA,IAC1E;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAqBO,SAAS,mBAAmB;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAqBG;AACD,sBAAoB,QAAQ,MAAM,8BAA8B,GAAG,CAAC,CAAC,CAAC;AAGtE,YAAU,MAAM;AACd,gCAA4B;AAAA,MAC1B,gBAAgB,CAAC,EAAE,YAAY,MAAM;AACnC,uBAAe,WAAW;AAAA,MAC5B;AAAA,MACA,kBAAkB,CAAC,UAAU;AAC3B,yBAAiB,KAAK;AAAA,MACxB;AAAA,MACA,uBAAuB,CAAC,YAAY;AAClC,gCAAwB;AAAA,UACtB,QAAQ,QAAQ;AAAA,UAChB,SAAS,QAAQ;AAAA,UACjB,gBAAgB,QAAQ;AAAA,UACxB,YAAY,QAAQ;AAAA,QACtB,CAAC;AAAA,MACH;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,UAAU,MAAM;AACxC,wBAAgB,QAAQ,SAAS;AAAA,MACnC;AAAA,MACA,yBAAyB,CAAC,EAAE,WAAW,QAAQ,MAAM;AACnD,kCAA0B,WAAW,OAAO;AAAA,MAC9C;AAAA,MACA,gBAAgB,CAAC,EAAE,OAAO,MAAM;AAC9B,yBAAiB,MAAM;AAAA,MACzB;AAAA,MACA,eAAe,MAAM;AACnB,0BAAkB;AAAA,MACpB;AAAA,MACA,cAAc,MAAM;AAClB,yBAAiB;AAAA,MACnB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,0BAAkB,OAAO;AAAA,MAC3B;AAAA,IACF,CAAC;AACD,UAAM,MAAM,qBAAqB;AACjC,SAAK,YAAY,cAAc;AAC/B,SAAK,yBAAyB;AAE9B,WAAO,MAAM;AACX,kCAA4B,IAAI;AAAA,IAClC;AAAA,EACF,GAAG;AAAA,IACD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AACH;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,5 +4,8 @@ export declare function useSelectModeController(): {
|
|
|
4
4
|
displayMode: DisplayMode;
|
|
5
5
|
interactionSessionRef: import("react").RefObject<SelectModeInteractionSession>;
|
|
6
6
|
setDisplayMode: (nextDisplayMode: DisplayMode) => void;
|
|
7
|
-
setSelectionType: (selectionType
|
|
7
|
+
setSelectionType: ({ selectionType, selectors, }: {
|
|
8
|
+
selectionType: SelectModeSelectionType;
|
|
9
|
+
selectors?: string[];
|
|
10
|
+
}) => void;
|
|
8
11
|
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback, useRef } from "react";
|
|
2
2
|
import { displayModeStore, useDisplayMode } from "../displayModeStore";
|
|
3
3
|
import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
|
|
4
|
-
import {
|
|
4
|
+
import { getDocumentScrollOffset } from "../interactions/selectionGeometry";
|
|
5
|
+
import { hideSelectionFrame, showAreaSelectionFrames } from "../selectionFrame";
|
|
5
6
|
import { IDLE_SESSION } from "../selectModeSession";
|
|
6
7
|
import { resetHoverRectDedup } from "./useSelectionHover";
|
|
7
8
|
function resetInteractionSession({
|
|
@@ -22,10 +23,34 @@ function useSelectModeController() {
|
|
|
22
23
|
const displayMode = useDisplayMode();
|
|
23
24
|
const interactionSessionRef = useRef(IDLE_SESSION);
|
|
24
25
|
const setSelectionType = useCallback(
|
|
25
|
-
(
|
|
26
|
+
({
|
|
27
|
+
selectionType,
|
|
28
|
+
selectors
|
|
29
|
+
}) => {
|
|
26
30
|
if (selectionType === "none") {
|
|
27
31
|
resetInteractionSession({ interactionSessionRef });
|
|
32
|
+
return;
|
|
28
33
|
}
|
|
34
|
+
const session = interactionSessionRef.current;
|
|
35
|
+
if (selectionType !== "area" || !selectors || session.type !== "area") {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const retainedSelectors = new Set(selectors);
|
|
39
|
+
const items = session.items.filter((item) => {
|
|
40
|
+
return item.data.kind !== "react-element" || retainedSelectors.has(item.data.selector);
|
|
41
|
+
});
|
|
42
|
+
if (items.length === session.items.length) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (items.length === 0) {
|
|
46
|
+
resetInteractionSession({ interactionSessionRef });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
interactionSessionRef.current = { ...session, items };
|
|
50
|
+
showAreaSelectionFrames({
|
|
51
|
+
elements: items.map((item) => item.element),
|
|
52
|
+
scrollOffset: getDocumentScrollOffset()
|
|
53
|
+
});
|
|
29
54
|
},
|
|
30
55
|
[]
|
|
31
56
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../select-mode/hooks/useSelectModeController.ts"],
|
|
4
|
-
"sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { hideSelectionFrame } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { resetHoverRectDedup } from \"./useSelectionHover\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n // The parent wiped its hover overlay as part of this clear; the hover\n // hook's rect dedup must forget its last post or an identical rect would\n // never re-render the overlay.\n resetHoverRectDedup();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n (selectionType: SelectModeSelectionType) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n }\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
|
|
5
|
-
"mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,
|
|
4
|
+
"sourcesContent": ["import type {\n DisplayMode,\n SelectModeSelectionType,\n} from \"schemas/generated/selectMode\";\nimport type { SelectModeInteractionSession } from \"../selectModeSession\";\n\nimport { useCallback, useRef } from \"react\";\n\nimport { displayModeStore, useDisplayMode } from \"../displayModeStore\";\nimport { getWebsiteBuilderRpc } from \"../iframeRpcChannel\";\nimport { getDocumentScrollOffset } from \"../interactions/selectionGeometry\";\nimport { hideSelectionFrame, showAreaSelectionFrames } from \"../selectionFrame\";\nimport { IDLE_SESSION } from \"../selectModeSession\";\nimport { resetHoverRectDedup } from \"./useSelectionHover\";\n\n/**\n * Tear down any in-progress drag/selection. Re-used by both the\n * display-mode flip-out path (defensive: belt-and-braces with the\n * parent's `setSelectionType(\"none\")` push) and by the explicit\n * `setSelectionType(\"none\")` path below.\n */\nfunction resetInteractionSession({\n interactionSessionRef,\n}: {\n interactionSessionRef: React.MutableRefObject<SelectModeInteractionSession>;\n}) {\n const currentSession = interactionSessionRef.current;\n if (currentSession.type === \"drag\" && currentSession.hasExceededThreshold) {\n getWebsiteBuilderRpc()?.dragRectUpdated({\n dragRect: null,\n selectionRects: [],\n });\n }\n hideSelectionFrame();\n // The parent wiped its hover overlay as part of this clear; the hover\n // hook's rect dedup must forget its last post or an identical rect would\n // never re-render the overlay.\n resetHoverRectDedup();\n interactionSessionRef.current = IDLE_SESSION;\n}\n\nexport function useSelectModeController() {\n // Display mode lives in a module singleton so the sibling `LinkInterceptor`\n // can read it (and stand down during selection) without a shared provider.\n const displayMode = useDisplayMode();\n const interactionSessionRef =\n useRef<SelectModeInteractionSession>(IDLE_SESSION);\n\n // The parent is authoritative for \"is there a current selection?\". Canopy\n // originates selections via pointer events, posts them up, agent-ui\n // mirrors `selection.type` into `agentUiState`, and the result is pushed\n // back down here. When the parent says `none` (panel dismissed / submit /\n // exit selection mode) we tear down the in-iframe frame; the matching\n // commit-side paths are in `useSelectionInteractions`.\n const setSelectionType = useCallback(\n ({\n selectionType,\n selectors,\n }: {\n selectionType: SelectModeSelectionType;\n selectors?: string[];\n }) => {\n if (selectionType === \"none\") {\n resetInteractionSession({ interactionSessionRef });\n return;\n }\n const session = interactionSessionRef.current;\n if (selectionType !== \"area\" || !selectors || session.type !== \"area\") {\n return;\n }\n // The editor can remove a single member (the tag's X) without a\n // pointer event ever reaching the iframe, so trim the session to the\n // members the parent still holds.\n const retainedSelectors = new Set(selectors);\n const items = session.items.filter((item) => {\n return (\n item.data.kind !== \"react-element\" ||\n retainedSelectors.has(item.data.selector)\n );\n });\n if (items.length === session.items.length) {\n return;\n }\n if (items.length === 0) {\n resetInteractionSession({ interactionSessionRef });\n return;\n }\n interactionSessionRef.current = { ...session, items };\n showAreaSelectionFrames({\n elements: items.map((item) => item.element),\n scrollOffset: getDocumentScrollOffset(),\n });\n },\n [],\n );\n\n const setDisplayMode = useCallback((nextDisplayMode: DisplayMode) => {\n if (\n displayModeStore.getSnapshot() === \"selection\" &&\n nextDisplayMode !== \"selection\"\n ) {\n resetInteractionSession({ interactionSessionRef });\n }\n\n displayModeStore.setDisplayMode(nextDisplayMode);\n }, []);\n\n return {\n displayMode,\n interactionSessionRef,\n setDisplayMode,\n setSelectionType,\n };\n}\n"],
|
|
5
|
+
"mappings": "AAMA,SAAS,aAAa,cAAc;AAEpC,SAAS,kBAAkB,sBAAsB;AACjD,SAAS,4BAA4B;AACrC,SAAS,+BAA+B;AACxC,SAAS,oBAAoB,+BAA+B;AAC5D,SAAS,oBAAoB;AAC7B,SAAS,2BAA2B;AAQpC,SAAS,wBAAwB;AAAA,EAC/B;AACF,GAEG;AACD,QAAM,iBAAiB,sBAAsB;AAC7C,MAAI,eAAe,SAAS,UAAU,eAAe,sBAAsB;AACzE,yBAAqB,GAAG,gBAAgB;AAAA,MACtC,UAAU;AAAA,MACV,gBAAgB,CAAC;AAAA,IACnB,CAAC;AAAA,EACH;AACA,qBAAmB;AAInB,sBAAoB;AACpB,wBAAsB,UAAU;AAClC;AAEO,SAAS,0BAA0B;AAGxC,QAAM,cAAc,eAAe;AACnC,QAAM,wBACJ,OAAqC,YAAY;AAQnD,QAAM,mBAAmB;AAAA,IACvB,CAAC;AAAA,MACC;AAAA,MACA;AAAA,IACF,MAGM;AACJ,UAAI,kBAAkB,QAAQ;AAC5B,gCAAwB,EAAE,sBAAsB,CAAC;AACjD;AAAA,MACF;AACA,YAAM,UAAU,sBAAsB;AACtC,UAAI,kBAAkB,UAAU,CAAC,aAAa,QAAQ,SAAS,QAAQ;AACrE;AAAA,MACF;AAIA,YAAM,oBAAoB,IAAI,IAAI,SAAS;AAC3C,YAAM,QAAQ,QAAQ,MAAM,OAAO,CAAC,SAAS;AAC3C,eACE,KAAK,KAAK,SAAS,mBACnB,kBAAkB,IAAI,KAAK,KAAK,QAAQ;AAAA,MAE5C,CAAC;AACD,UAAI,MAAM,WAAW,QAAQ,MAAM,QAAQ;AACzC;AAAA,MACF;AACA,UAAI,MAAM,WAAW,GAAG;AACtB,gCAAwB,EAAE,sBAAsB,CAAC;AACjD;AAAA,MACF;AACA,4BAAsB,UAAU,EAAE,GAAG,SAAS,MAAM;AACpD,8BAAwB;AAAA,QACtB,UAAU,MAAM,IAAI,CAAC,SAAS,KAAK,OAAO;AAAA,QAC1C,cAAc,wBAAwB;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,IACA,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,YAAY,CAAC,oBAAiC;AACnE,QACE,iBAAiB,YAAY,MAAM,eACnC,oBAAoB,aACpB;AACA,8BAAwB,EAAE,sBAAsB,CAAC;AAAA,IACnD;AAEA,qBAAiB,eAAe,eAAe;AAAA,EACjD,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|