@replohq/sandbox-runtime 1.19.3 → 1.19.5
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/schemas/externalNavigationIframeRpc.d.ts +3 -1
- package/_vendor/schemas/externalNavigationIframeRpc.mjs +4 -2
- package/_vendor/schemas/iframeRpcChannel.d.ts +12 -0
- package/_vendor/schemas/iframeRpcChannel.mjs +1 -1
- package/_vendor/schemas/previewHealthIframeRpc.d.ts +3 -1
- package/_vendor/schemas/previewHealthIframeRpc.mjs +4 -2
- package/buildMetadata.js +3 -3
- package/{chunk-36IPO47A.mjs → chunk-6WREP7C2.mjs} +12 -2
- package/package.json +4 -4
- package/select-mode/descriptor/elementDescriptor.js +7 -0
- package/select-mode/interactions/embeddedFrames.d.ts +1 -5
- package/select-mode/interactions/embeddedFrames.js +1 -10
- package/select-mode/section-canvas/SectionCanvas.js +56 -70
- package/select-mode/section-canvas/optimisticSectionMove.d.ts +8 -7
- package/select-mode/section-canvas/optimisticSectionMove.js +32 -26
- package/select-mode/section-canvas/sectionCanvasStore.d.ts +3 -3
- package/select-mode/section-canvas/sectionCanvasStore.js +21 -13
- package/select-mode/section-canvas/sectionElements.d.ts +5 -7
- package/select-mode/section-canvas/sectionElements.js +4 -11
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import type { IframeRpcVersionMismatch } from "./iframeRpcChannel";
|
|
2
3
|
export declare const EXTERNAL_NAVIGATION_IFRAME_RPC_CHANNEL_ID: "replo:external-navigation-iframe-rpc";
|
|
3
4
|
export declare const EXTERNAL_NAVIGATION_IFRAME_RPC_CHANNEL_VERSION: 1;
|
|
4
5
|
export declare const sandboxToParentExternalNavigationMethodConfig: {
|
|
@@ -11,7 +12,8 @@ export declare const sandboxToParentExternalNavigationMethodConfig: {
|
|
|
11
12
|
};
|
|
12
13
|
export declare const parentToSandboxExternalNavigationMethodConfig: {};
|
|
13
14
|
/** Opens what the frame hands up in a new tab. Null `frameOrigin` accepts any origin. */
|
|
14
|
-
export declare function createExternalNavigationParentChannel({ getFrameWindow, frameOrigin, }: {
|
|
15
|
+
export declare function createExternalNavigationParentChannel({ getFrameWindow, frameOrigin, onPeerVersionMismatch, }: {
|
|
15
16
|
getFrameWindow: () => Window | null;
|
|
16
17
|
frameOrigin: string | null;
|
|
18
|
+
onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
|
|
17
19
|
}): import("./iframeRpcChannel").IframeRpcChannel<{}>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createIframeRpcChannel } from '../../chunk-
|
|
1
|
+
import { createIframeRpcChannel } from '../../chunk-6WREP7C2.mjs';
|
|
2
2
|
import '../../chunk-ZTT7SNYN.mjs';
|
|
3
3
|
import '../../chunk-THOBCJJA.mjs';
|
|
4
4
|
import '../../chunk-4ITJNSXZ.mjs';
|
|
@@ -17,7 +17,8 @@ var sandboxToParentExternalNavigationMethodConfig = {
|
|
|
17
17
|
var parentToSandboxExternalNavigationMethodConfig = {};
|
|
18
18
|
function createExternalNavigationParentChannel({
|
|
19
19
|
getFrameWindow,
|
|
20
|
-
frameOrigin
|
|
20
|
+
frameOrigin,
|
|
21
|
+
onPeerVersionMismatch
|
|
21
22
|
}) {
|
|
22
23
|
return createIframeRpcChannel({
|
|
23
24
|
subscriberWindow: window,
|
|
@@ -30,6 +31,7 @@ function createExternalNavigationParentChannel({
|
|
|
30
31
|
},
|
|
31
32
|
outgoingMethods: parentToSandboxExternalNavigationMethodConfig,
|
|
32
33
|
incomingMethods: sandboxToParentExternalNavigationMethodConfig,
|
|
34
|
+
onPeerVersionMismatch,
|
|
33
35
|
handlers: {
|
|
34
36
|
openExternal: ({ url: url2 }) => {
|
|
35
37
|
window.open(url2, "_blank", "noopener,noreferrer");
|
|
@@ -50,6 +50,11 @@ type SubscriberWindow = {
|
|
|
50
50
|
type TargetIframeWindow = {
|
|
51
51
|
postMessage: (message: unknown, targetOrigin: string) => void;
|
|
52
52
|
};
|
|
53
|
+
export type IframeRpcVersionMismatch = {
|
|
54
|
+
channelIdentifier: string;
|
|
55
|
+
ownVersion: number;
|
|
56
|
+
peerVersion: number;
|
|
57
|
+
};
|
|
53
58
|
type IframeRpcChannelOptions<OutgoingMethods extends IframeRpcChannelMethodConfig, IncomingMethods extends IframeRpcChannelMethodConfig> = {
|
|
54
59
|
subscriberWindow: SubscriberWindow;
|
|
55
60
|
channelIdentifier: string;
|
|
@@ -60,6 +65,13 @@ type IframeRpcChannelOptions<OutgoingMethods extends IframeRpcChannelMethodConfi
|
|
|
60
65
|
outgoingMethods: OutgoingMethods;
|
|
61
66
|
incomingMethods: IncomingMethods;
|
|
62
67
|
handlers: IframeRpcChannelHandlers<IncomingMethods, OutgoingMethods>;
|
|
68
|
+
/**
|
|
69
|
+
* Called for every message from the expected peer on another version of
|
|
70
|
+
* this channel. The message itself is dropped: neither side can parse the
|
|
71
|
+
* other's protocol, so recovering (e.g. reloading the older side) is the
|
|
72
|
+
* caller's job.
|
|
73
|
+
*/
|
|
74
|
+
onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
|
|
63
75
|
};
|
|
64
76
|
export type IframeRpcChannel<OutgoingMethods extends IframeRpcChannelMethodConfig = IframeRpcChannelMethodConfig> = {
|
|
65
77
|
subscribe: () => () => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createIframeRpcChannel, createIframeRpcRemote, iframeRpcCallSchema } from '../../chunk-
|
|
1
|
+
export { createIframeRpcChannel, createIframeRpcRemote, iframeRpcCallSchema } from '../../chunk-6WREP7C2.mjs';
|
|
2
2
|
import '../../chunk-ZTT7SNYN.mjs';
|
|
3
3
|
import '../../chunk-THOBCJJA.mjs';
|
|
4
4
|
import '../../chunk-4ITJNSXZ.mjs';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import type { IframeRpcVersionMismatch } from "./iframeRpcChannel";
|
|
2
3
|
export declare const PREVIEW_HEALTH_IFRAME_RPC_CHANNEL_ID: "replo:preview-health-iframe-rpc";
|
|
3
4
|
export declare const PREVIEW_HEALTH_IFRAME_RPC_CHANNEL_VERSION: 1;
|
|
4
5
|
export declare const previewErrorDetailSchema: z.ZodObject<{
|
|
@@ -147,7 +148,7 @@ export declare const editorToPreviewHealthMethodConfig: {};
|
|
|
147
148
|
* status from current runtimes and null from older runtimes, which only say
|
|
148
149
|
* ok or error.
|
|
149
150
|
*/
|
|
150
|
-
export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArrived, onStatus, onDiagnostic, }: {
|
|
151
|
+
export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArrived, onStatus, onDiagnostic, onPeerVersionMismatch, }: {
|
|
151
152
|
getFrameWindow: () => Window | null;
|
|
152
153
|
onDocumentArrived?: () => void;
|
|
153
154
|
onStatus: (update: {
|
|
@@ -155,5 +156,6 @@ export declare function subscribeToPreviewHealth({ getFrameWindow, onDocumentArr
|
|
|
155
156
|
report: PreviewStatusReport | null;
|
|
156
157
|
}) => void;
|
|
157
158
|
onDiagnostic?: (report: PreviewDiagnosticReport) => void;
|
|
159
|
+
onPeerVersionMismatch?: (mismatch: IframeRpcVersionMismatch) => void;
|
|
158
160
|
}): () => void;
|
|
159
161
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createIframeRpcChannel } from '../../chunk-
|
|
1
|
+
import { createIframeRpcChannel } from '../../chunk-6WREP7C2.mjs';
|
|
2
2
|
import '../../chunk-ZTT7SNYN.mjs';
|
|
3
3
|
import '../../chunk-THOBCJJA.mjs';
|
|
4
4
|
import '../../chunk-4ITJNSXZ.mjs';
|
|
@@ -71,7 +71,8 @@ function subscribeToPreviewHealth({
|
|
|
71
71
|
getFrameWindow,
|
|
72
72
|
onDocumentArrived,
|
|
73
73
|
onStatus,
|
|
74
|
-
onDiagnostic
|
|
74
|
+
onDiagnostic,
|
|
75
|
+
onPeerVersionMismatch
|
|
75
76
|
}) {
|
|
76
77
|
const isFromFrame = (event) => {
|
|
77
78
|
const frameWindow = getFrameWindow();
|
|
@@ -86,6 +87,7 @@ function subscribeToPreviewHealth({
|
|
|
86
87
|
isExpectedMessage: isFromFrame,
|
|
87
88
|
outgoingMethods: editorToPreviewHealthMethodConfig,
|
|
88
89
|
incomingMethods: previewHealthToEditorMethodConfig,
|
|
90
|
+
onPeerVersionMismatch,
|
|
89
91
|
handlers: {
|
|
90
92
|
documentArrived: () => {
|
|
91
93
|
onDocumentArrived?.();
|
package/buildMetadata.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export const BUILD_METADATA = {
|
|
2
2
|
"packageName": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.5",
|
|
4
4
|
"branch": "HEAD",
|
|
5
|
-
"commit": "
|
|
6
|
-
"builtAt": "2026-10-
|
|
5
|
+
"commit": "925967a151b3e53a1891d5d57b0cf956b0f64ffe",
|
|
6
|
+
"builtAt": "2026-10-28T00:16:14.622Z"
|
|
7
7
|
};
|
|
@@ -252,7 +252,8 @@ function createIframeRpcChannel(options) {
|
|
|
252
252
|
isExpectedMessage,
|
|
253
253
|
outgoingMethods,
|
|
254
254
|
incomingMethods,
|
|
255
|
-
handlers
|
|
255
|
+
handlers,
|
|
256
|
+
onPeerVersionMismatch
|
|
256
257
|
} = options;
|
|
257
258
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
258
259
|
let subscriberCount = 0;
|
|
@@ -380,7 +381,7 @@ function createIframeRpcChannel(options) {
|
|
|
380
381
|
const handleMessage = (event) => {
|
|
381
382
|
const parsedMetadata = z.object({
|
|
382
383
|
channelIdentifier: z.literal(channelIdentifier),
|
|
383
|
-
version: z.
|
|
384
|
+
version: z.number()
|
|
384
385
|
}).safeParse(event.data);
|
|
385
386
|
if (!parsedMetadata.success) {
|
|
386
387
|
return;
|
|
@@ -400,6 +401,15 @@ function createIframeRpcChannel(options) {
|
|
|
400
401
|
});
|
|
401
402
|
return;
|
|
402
403
|
}
|
|
404
|
+
const peerVersion = parsedMetadata.data.version;
|
|
405
|
+
if (peerVersion !== version) {
|
|
406
|
+
onPeerVersionMismatch?.({
|
|
407
|
+
channelIdentifier,
|
|
408
|
+
ownVersion: version,
|
|
409
|
+
peerVersion
|
|
410
|
+
});
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
403
413
|
const parsedCall = iframeRpcCallSchema.safeParse(event.data);
|
|
404
414
|
if (!parsedCall.success) {
|
|
405
415
|
return;
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@replohq/sandbox-runtime",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.5",
|
|
4
4
|
"reploBuild": {
|
|
5
5
|
"packageName": "@replohq/sandbox-runtime",
|
|
6
|
-
"version": "1.19.
|
|
6
|
+
"version": "1.19.5",
|
|
7
7
|
"branch": "HEAD",
|
|
8
|
-
"commit": "
|
|
9
|
-
"builtAt": "2026-10-
|
|
8
|
+
"commit": "925967a151b3e53a1891d5d57b0cf956b0f64ffe",
|
|
9
|
+
"builtAt": "2026-10-28T00:16:14.622Z"
|
|
10
10
|
},
|
|
11
11
|
"description": "Replo sandbox runtime — editor integration for agent-built Next.js sites.",
|
|
12
12
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -116,6 +116,13 @@ function identifyElement({
|
|
|
116
116
|
}
|
|
117
117
|
return "image";
|
|
118
118
|
}
|
|
119
|
+
if (element instanceof HTMLIFrameElement) {
|
|
120
|
+
if (element.hasAttribute("srcdoc")) {
|
|
121
|
+
return "iframe: inline HTML (srcdoc)";
|
|
122
|
+
}
|
|
123
|
+
const src = element.getAttribute("src");
|
|
124
|
+
return src ? `iframe: [${src.slice(0, 60)}]` : "iframe";
|
|
125
|
+
}
|
|
119
126
|
if (/^h[1-6]$/.test(tagName)) {
|
|
120
127
|
return trimmedText ? `heading(${tagName}): "${trimmedText.slice(0, 60)}"` : tagName;
|
|
121
128
|
}
|
|
@@ -4,11 +4,7 @@ export declare const FRAME_SHIELD_CSS = "iframe { pointer-events: none !importan
|
|
|
4
4
|
* Select mode shields iframes from the pointer so presses over an embed reach
|
|
5
5
|
* this document instead of the embed's own (often cross-origin) one. The hit
|
|
6
6
|
* element is then whatever sits behind the frame, so lift the shield for one
|
|
7
|
-
* native hit test to find the frame the user actually pointed at.
|
|
8
|
-
* injected as raw HTML has no React source, so prefer its nearest
|
|
9
|
-
* React-rendered ancestor, which the agent can edit. React 19 renders <body>
|
|
10
|
-
* too, so stop below it: a widget script appends its frame straight to the
|
|
11
|
-
* body, and selecting the frame beats selecting the whole page.
|
|
7
|
+
* native hit test to find the frame the user actually pointed at.
|
|
12
8
|
*/
|
|
13
9
|
export declare function getSelectionTargetForEmbeddedFrameHit({ hitElement, clientX, clientY, }: {
|
|
14
10
|
hitElement: Element;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getFiberFromElement } from "../fiber";
|
|
2
1
|
const FRAME_SHIELD_ATTRIBUTE = "data-select-mode-frame-shield";
|
|
3
2
|
const FRAME_SHIELD_CSS = "iframe { pointer-events: none !important; }";
|
|
4
3
|
function getSelectionTargetForEmbeddedFrameHit({
|
|
@@ -15,15 +14,7 @@ function getSelectionTargetForEmbeddedFrameHit({
|
|
|
15
14
|
shield.disabled = true;
|
|
16
15
|
const frame = document.elementFromPoint(clientX, clientY);
|
|
17
16
|
shield.disabled = false;
|
|
18
|
-
|
|
19
|
-
return hitElement;
|
|
20
|
-
}
|
|
21
|
-
for (let candidate = frame; candidate && candidate !== document.body; candidate = candidate.parentElement) {
|
|
22
|
-
if (getFiberFromElement(candidate)) {
|
|
23
|
-
return candidate;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return frame;
|
|
17
|
+
return frame instanceof HTMLIFrameElement ? frame : hitElement;
|
|
27
18
|
}
|
|
28
19
|
export {
|
|
29
20
|
FRAME_SHIELD_ATTRIBUTE,
|
|
@@ -219,24 +219,21 @@ function SectionCanvas() {
|
|
|
219
219
|
const activeIndex = drag ? drag.index : hoveredIndex ?? selectedIndex;
|
|
220
220
|
const activeSection = activeIndex === null ? void 0 : canvas.sections[activeIndex];
|
|
221
221
|
const activeBox = activeIndex === null ? null : getSectionViewportBox(elementsByIndex.get(activeIndex));
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
);
|
|
239
|
-
}
|
|
222
|
+
const overlay = activeSection && activeBox && !isMovePending ? /* @__PURE__ */ jsx(
|
|
223
|
+
SectionControls,
|
|
224
|
+
{
|
|
225
|
+
index: activeSection.index,
|
|
226
|
+
label: activeSection.label,
|
|
227
|
+
isLocked: activeSection.isLocked,
|
|
228
|
+
isFirst: activeSection.index === 0,
|
|
229
|
+
isLast: activeSection.index === canvas.sections.length - 1,
|
|
230
|
+
box: activeBox,
|
|
231
|
+
visible,
|
|
232
|
+
unit,
|
|
233
|
+
isDragging: drag !== null,
|
|
234
|
+
isPicking: typeof canvas.insertingAt === "number"
|
|
235
|
+
}
|
|
236
|
+
) : null;
|
|
240
237
|
const markedGap = drag ? drag.gap : canvas.insertingAt;
|
|
241
238
|
let gapMarker = null;
|
|
242
239
|
const placed = typeof markedGap === "number" ? getPlacedSections({ canvas, elementsByIndex }) : [];
|
|
@@ -284,48 +281,45 @@ function SectionCanvas() {
|
|
|
284
281
|
)
|
|
285
282
|
] });
|
|
286
283
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
outline: `${2 * unit}px dashed ${ACCENT}`,
|
|
301
|
-
outlineOffset: -unit
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
),
|
|
305
|
-
/* @__PURE__ */ jsx(
|
|
306
|
-
"div",
|
|
307
|
-
{
|
|
308
|
-
"data-shape": "pill",
|
|
309
|
-
style: {
|
|
310
|
-
...pillStyle({
|
|
311
|
-
unit,
|
|
312
|
-
left: Math.min(
|
|
313
|
-
Math.max(drag.x, LABEL_HALF_WIDTH * unit),
|
|
314
|
-
window.innerWidth - LABEL_HALF_WIDTH * unit
|
|
315
|
-
),
|
|
316
|
-
top: drag.y + 20 * unit + PILL_HEIGHT * unit > window.innerHeight ? drag.y - 20 * unit : drag.y + 20 * unit
|
|
317
|
-
}),
|
|
318
|
-
maxWidth: 2 * LABEL_HALF_WIDTH * unit,
|
|
319
|
-
display: "block",
|
|
320
|
-
lineHeight: `${PILL_HEIGHT * unit}px`,
|
|
321
|
-
overflow: "hidden",
|
|
322
|
-
textOverflow: "ellipsis"
|
|
323
|
-
},
|
|
324
|
-
children: activeSection?.label ?? "Section"
|
|
284
|
+
const dragLayer = drag?.isActive && activeBox ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
285
|
+
/* @__PURE__ */ jsx(
|
|
286
|
+
"div",
|
|
287
|
+
{
|
|
288
|
+
style: {
|
|
289
|
+
position: "fixed",
|
|
290
|
+
left: activeBox.left,
|
|
291
|
+
top: activeBox.top,
|
|
292
|
+
width: activeBox.right - activeBox.left,
|
|
293
|
+
height: activeBox.bottom - activeBox.top,
|
|
294
|
+
background: "rgba(255, 255, 255, 0.65)",
|
|
295
|
+
outline: `${2 * unit}px dashed ${ACCENT}`,
|
|
296
|
+
outlineOffset: -unit
|
|
325
297
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
298
|
+
}
|
|
299
|
+
),
|
|
300
|
+
/* @__PURE__ */ jsx(
|
|
301
|
+
"div",
|
|
302
|
+
{
|
|
303
|
+
"data-shape": "pill",
|
|
304
|
+
style: {
|
|
305
|
+
...pillStyle({
|
|
306
|
+
unit,
|
|
307
|
+
left: Math.min(
|
|
308
|
+
Math.max(drag.x, LABEL_HALF_WIDTH * unit),
|
|
309
|
+
window.innerWidth - LABEL_HALF_WIDTH * unit
|
|
310
|
+
),
|
|
311
|
+
top: drag.y + 20 * unit + PILL_HEIGHT * unit > window.innerHeight ? drag.y - 20 * unit : drag.y + 20 * unit
|
|
312
|
+
}),
|
|
313
|
+
maxWidth: 2 * LABEL_HALF_WIDTH * unit,
|
|
314
|
+
display: "block",
|
|
315
|
+
lineHeight: `${PILL_HEIGHT * unit}px`,
|
|
316
|
+
overflow: "hidden",
|
|
317
|
+
textOverflow: "ellipsis"
|
|
318
|
+
},
|
|
319
|
+
children: activeSection?.label ?? "Section"
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
] }) : null;
|
|
329
323
|
return createPortal(
|
|
330
324
|
/* @__PURE__ */ jsxs(
|
|
331
325
|
"div",
|
|
@@ -528,17 +522,9 @@ function SectionControls({
|
|
|
528
522
|
)
|
|
529
523
|
] });
|
|
530
524
|
}
|
|
531
|
-
const GRIP_DOTS = [
|
|
532
|
-
[5, 3],
|
|
533
|
-
[5, 8],
|
|
534
|
-
[5, 13],
|
|
535
|
-
[11, 3],
|
|
536
|
-
[11, 8],
|
|
537
|
-
[11, 13]
|
|
538
|
-
];
|
|
539
525
|
function GripIcon({ size }) {
|
|
540
|
-
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", children:
|
|
541
|
-
return /* @__PURE__ */ jsx(
|
|
526
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", children: [5, 11].flatMap((cx) => {
|
|
527
|
+
return [3, 8, 13].map((cy) => /* @__PURE__ */ jsx(
|
|
542
528
|
"circle",
|
|
543
529
|
{
|
|
544
530
|
cx,
|
|
@@ -547,7 +533,7 @@ function GripIcon({ size }) {
|
|
|
547
533
|
fill: "currentColor"
|
|
548
534
|
},
|
|
549
535
|
`${cx}-${cy}`
|
|
550
|
-
);
|
|
536
|
+
));
|
|
551
537
|
}) });
|
|
552
538
|
}
|
|
553
539
|
export {
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
type SectionBox = {
|
|
2
|
-
index: number;
|
|
3
|
-
elements: HTMLElement[];
|
|
4
|
-
};
|
|
5
1
|
/** How far each section shifts once `from` moves to `to`, keeping the original gaps between slots. */
|
|
6
2
|
export declare function getMovedSectionOffsets({ sections, from, to, }: {
|
|
7
3
|
sections: readonly {
|
|
@@ -28,12 +24,17 @@ export declare function getSectionViewportBox(elements: readonly HTMLElement[] |
|
|
|
28
24
|
* page never shows the old order in between.
|
|
29
25
|
*/
|
|
30
26
|
export declare function startOptimisticSectionMove({ sections, from, to, onLanded, }: {
|
|
31
|
-
sections: readonly
|
|
27
|
+
sections: readonly {
|
|
28
|
+
index: number;
|
|
29
|
+
elements: readonly HTMLElement[];
|
|
30
|
+
}[];
|
|
32
31
|
from: number;
|
|
33
32
|
to: number;
|
|
34
33
|
onLanded: () => void;
|
|
35
34
|
}): {
|
|
36
35
|
revert: () => void;
|
|
37
|
-
settle: (
|
|
36
|
+
settle: ({ afterMs }: {
|
|
37
|
+
afterMs: number;
|
|
38
|
+
}) => void;
|
|
39
|
+
land: () => void;
|
|
38
40
|
};
|
|
39
|
-
export {};
|
|
@@ -85,11 +85,11 @@ function startOptimisticSectionMove({
|
|
|
85
85
|
const reordered = [...order];
|
|
86
86
|
reordered.splice(fromPosition, 1);
|
|
87
87
|
reordered.splice(getTargetPosition({ indices: order, from, to }), 0, from);
|
|
88
|
+
const textOf = (elements) => {
|
|
89
|
+
return elements.map((element) => element.textContent ?? "").join("\n");
|
|
90
|
+
};
|
|
88
91
|
const textBySection = new Map(
|
|
89
|
-
placed.map((section) => [
|
|
90
|
-
section.index,
|
|
91
|
-
section.elements.map((element) => element.textContent ?? "").join("\n")
|
|
92
|
-
])
|
|
92
|
+
placed.map((section) => [section.index, textOf(section.elements)])
|
|
93
93
|
);
|
|
94
94
|
const watchedSlots = placed.flatMap((section, slot) => {
|
|
95
95
|
const expected = textBySection.get(reordered[slot]);
|
|
@@ -97,11 +97,13 @@ function startOptimisticSectionMove({
|
|
|
97
97
|
});
|
|
98
98
|
const containers = new Set(
|
|
99
99
|
placed.flatMap((section) => {
|
|
100
|
-
return section.elements.flatMap((element) =>
|
|
101
|
-
return element.parentNode ? [element.parentNode] : [];
|
|
102
|
-
});
|
|
100
|
+
return section.elements.flatMap((element) => element.parentNode ?? []);
|
|
103
101
|
})
|
|
104
102
|
);
|
|
103
|
+
const rootsInMovedOrder = reordered.flatMap((index) => {
|
|
104
|
+
const root = placed.find((section) => section.index === index)?.elements.find((element) => element.isConnected);
|
|
105
|
+
return root ? [root] : [];
|
|
106
|
+
});
|
|
105
107
|
let isDone = false;
|
|
106
108
|
let fallbackTimeout;
|
|
107
109
|
const finish = ({ animate }) => {
|
|
@@ -128,20 +130,26 @@ function startOptimisticSectionMove({
|
|
|
128
130
|
restoreTransitions();
|
|
129
131
|
}
|
|
130
132
|
};
|
|
131
|
-
const hasLanded = (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
133
|
+
const hasLanded = () => {
|
|
134
|
+
return (
|
|
135
|
+
rootsInMovedOrder.length >= 2 && rootsInMovedOrder.every((root, position) => {
|
|
136
|
+
const previous = rootsInMovedOrder[position - 1];
|
|
137
|
+
return root.isConnected && (!previous || Boolean(
|
|
138
|
+
previous.compareDocumentPosition(root) & Node.DOCUMENT_POSITION_FOLLOWING
|
|
139
|
+
));
|
|
140
|
+
}) ||
|
|
141
|
+
rootsInMovedOrder.some((root) => !root.isConnected) || watchedSlots.length > 0 && watchedSlots.every(({ elements, expected }) => {
|
|
142
|
+
return elements.some((element) => !element.isConnected) || textOf(elements) === expected;
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
const land = () => {
|
|
147
|
+
finish({ animate: false });
|
|
148
|
+
onLanded();
|
|
140
149
|
};
|
|
141
|
-
const observer = new MutationObserver((
|
|
142
|
-
if (hasLanded(
|
|
143
|
-
|
|
144
|
-
onLanded();
|
|
150
|
+
const observer = new MutationObserver(() => {
|
|
151
|
+
if (hasLanded()) {
|
|
152
|
+
land();
|
|
145
153
|
}
|
|
146
154
|
});
|
|
147
155
|
for (const container of containers) {
|
|
@@ -153,16 +161,14 @@ function startOptimisticSectionMove({
|
|
|
153
161
|
}
|
|
154
162
|
return {
|
|
155
163
|
revert: () => finish({ animate: true }),
|
|
156
|
-
settle: () => {
|
|
164
|
+
settle: ({ afterMs }) => {
|
|
157
165
|
if (isDone) {
|
|
158
166
|
return;
|
|
159
167
|
}
|
|
160
168
|
clearTimeout(fallbackTimeout);
|
|
161
|
-
fallbackTimeout = setTimeout(
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}, 8e3);
|
|
165
|
-
}
|
|
169
|
+
fallbackTimeout = setTimeout(land, afterMs);
|
|
170
|
+
},
|
|
171
|
+
land
|
|
166
172
|
};
|
|
167
173
|
}
|
|
168
174
|
export {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SectionCanvasRequest, SectionCanvasState } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
2
|
+
import { startOptimisticSectionMove } from "./optimisticSectionMove";
|
|
2
3
|
export type SectionDrag = {
|
|
3
4
|
index: number;
|
|
4
5
|
pointerId: number;
|
|
@@ -9,10 +10,9 @@ export type SectionDrag = {
|
|
|
9
10
|
isActive: boolean;
|
|
10
11
|
gap: number | null;
|
|
11
12
|
};
|
|
12
|
-
type PendingMove = {
|
|
13
|
+
type PendingMove = ReturnType<typeof startOptimisticSectionMove> & {
|
|
13
14
|
moveId: string;
|
|
14
|
-
|
|
15
|
-
revert: () => void;
|
|
15
|
+
phase: "saving" | "refreshedWhileSaving" | "saved";
|
|
16
16
|
};
|
|
17
17
|
type SectionCanvasStoreState = {
|
|
18
18
|
canvas: SectionCanvasState | null;
|
|
@@ -52,11 +52,10 @@ function getDropGap({
|
|
|
52
52
|
getPlacedSections(state),
|
|
53
53
|
state.canvas?.sections.length ?? 0
|
|
54
54
|
);
|
|
55
|
-
let nearest
|
|
55
|
+
let nearest;
|
|
56
56
|
for (const gap of gaps) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
nearest = { at: gap.at, distance };
|
|
57
|
+
if (!nearest || Math.abs(gap.y - y) < Math.abs(nearest.y - y)) {
|
|
58
|
+
nearest = gap;
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
if (!nearest || nearest.at === index || nearest.at === index + 1) {
|
|
@@ -145,10 +144,19 @@ const sectionCanvasStore = createShallowStore(
|
|
|
145
144
|
reportLayout(get());
|
|
146
145
|
},
|
|
147
146
|
handlePreviewRefresh: () => {
|
|
148
|
-
|
|
147
|
+
const { canvas, pendingMove } = get();
|
|
148
|
+
if (!canvas) {
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
if (!pendingMove) {
|
|
152
|
+
void get().actions.resolveSections();
|
|
153
|
+
} else if (pendingMove.phase === "saved") {
|
|
154
|
+
pendingMove.land();
|
|
155
|
+
} else {
|
|
156
|
+
set({
|
|
157
|
+
pendingMove: { ...pendingMove, phase: "refreshedWhileSaving" }
|
|
158
|
+
});
|
|
159
|
+
}
|
|
152
160
|
},
|
|
153
161
|
bumpGeometry: () => {
|
|
154
162
|
set((state) => ({ geometryVersion: state.geometryVersion + 1 }));
|
|
@@ -206,16 +214,14 @@ const sectionCanvasStore = createShallowStore(
|
|
|
206
214
|
},
|
|
207
215
|
move: ({ index, to }) => {
|
|
208
216
|
const state = get();
|
|
209
|
-
|
|
210
|
-
if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= sectionCount) {
|
|
217
|
+
if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= state.canvas.sections.length) {
|
|
211
218
|
return;
|
|
212
219
|
}
|
|
213
|
-
|
|
214
|
-
const moveId = `section-move-${Date.now()}-${moveCounter}`;
|
|
220
|
+
const moveId = `section-move-${Date.now()}-${++moveCounter}`;
|
|
215
221
|
const optimistic = startOptimisticSectionMove({
|
|
216
222
|
sections: state.canvas.sections.map((section) => ({
|
|
217
223
|
index: section.index,
|
|
218
|
-
elements:
|
|
224
|
+
elements: state.elementsByIndex.get(section.index) ?? []
|
|
219
225
|
})),
|
|
220
226
|
from: index,
|
|
221
227
|
to,
|
|
@@ -228,7 +234,7 @@ const sectionCanvasStore = createShallowStore(
|
|
|
228
234
|
}
|
|
229
235
|
});
|
|
230
236
|
set({
|
|
231
|
-
pendingMove: { moveId, ...optimistic },
|
|
237
|
+
pendingMove: { moveId, phase: "saving", ...optimistic },
|
|
232
238
|
hoveredIndex: null
|
|
233
239
|
});
|
|
234
240
|
get().actions.request({ type: "move", moveId, index, to });
|
|
@@ -239,7 +245,9 @@ const sectionCanvasStore = createShallowStore(
|
|
|
239
245
|
return;
|
|
240
246
|
}
|
|
241
247
|
if (outcome === "persisted") {
|
|
242
|
-
pendingMove.
|
|
248
|
+
const afterMs = pendingMove.phase === "refreshedWhileSaving" ? 8e3 : 2e4;
|
|
249
|
+
set({ pendingMove: { ...pendingMove, phase: "saved" } });
|
|
250
|
+
pendingMove.settle({ afterMs });
|
|
243
251
|
return;
|
|
244
252
|
}
|
|
245
253
|
pendingMove.revert();
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import type { SelectModeSourceLocation } from "../../_vendor/schemas/generated/selectMode";
|
|
2
1
|
import type { SectionCanvasSection } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
3
|
-
|
|
2
|
+
import { symbolicateSourceLocations } from "../symbolicate";
|
|
4
3
|
/**
|
|
5
4
|
* The DOM elements each page section renders, found from source, not layout: an element belongs to
|
|
6
|
-
* a section when a JSX call site in its owner chain lies in the page file inside that section's
|
|
7
|
-
*
|
|
8
|
-
* never read.
|
|
5
|
+
* a section when a JSX call site in its owner chain lies in the page file inside that section's
|
|
6
|
+
* lines. Walks down from <body> a level at a time, symbolicating each level in one request and
|
|
7
|
+
* stopping at the first element that matches, so a section's descendants are never read.
|
|
9
8
|
*/
|
|
10
9
|
export declare function findSectionElements({ root, pageFilePath, sections, resolve, }: {
|
|
11
10
|
root: Element;
|
|
12
11
|
pageFilePath: string;
|
|
13
12
|
sections: readonly Pick<SectionCanvasSection, "index" | "startLine" | "endLine">[];
|
|
14
|
-
resolve?:
|
|
13
|
+
resolve?: typeof symbolicateSourceLocations;
|
|
15
14
|
}): Promise<Map<number, HTMLElement[]>>;
|
|
16
|
-
export {};
|
|
@@ -47,10 +47,9 @@ async function findSectionElements({
|
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
if (pending.size > 0) {
|
|
50
|
-
const keys = [...pending.keys()];
|
|
51
50
|
const results = await resolve([...pending.values()]);
|
|
52
|
-
|
|
53
|
-
resolved.set(key, results[index] ??
|
|
51
|
+
[...pending].forEach(([key, location], index) => {
|
|
52
|
+
resolved.set(key, results[index] ?? location);
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
const next = [];
|
|
@@ -58,15 +57,9 @@ async function findSectionElements({
|
|
|
58
57
|
let section;
|
|
59
58
|
for (const location of chain) {
|
|
60
59
|
const original = resolved.get(locationKey(location));
|
|
61
|
-
|
|
62
|
-
filePath: original.filePath,
|
|
63
|
-
pageFilePath
|
|
64
|
-
})) {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
section = sections.find((candidate) => {
|
|
60
|
+
section = original && isSectionCanvasPageFile({ filePath: original.filePath, pageFilePath }) ? sections.find((candidate) => {
|
|
68
61
|
return candidate.startLine <= original.lineNumber && original.lineNumber <= candidate.endLine;
|
|
69
|
-
});
|
|
62
|
+
}) : void 0;
|
|
70
63
|
if (section) {
|
|
71
64
|
break;
|
|
72
65
|
}
|