@replohq/sandbox-runtime 1.16.2 → 1.18.0
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/ClientErrorBoundary.js +26 -0
- package/DocumentReadyBeacon.js +5 -1
- package/_vendor/replo-utils/lib/historyKeys.d.ts +2 -0
- package/_vendor/replo-utils/lib/historyKeys.mjs +18 -0
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +43 -0
- package/_vendor/schemas/iframeRpcChannel.mjs +3 -541
- package/_vendor/schemas/previewHealthIframeRpc.d.ts +159 -0
- package/_vendor/schemas/previewHealthIframeRpc.mjs +123 -0
- package/_vendor/schemas/selectModeIframeRpc.d.ts +149 -3
- package/_vendor/schemas/selectModeIframeRpc.mjs +72 -2
- package/buildMetadata.js +3 -3
- package/chunk-KRA4LUIR.mjs +541 -0
- package/nextErrorFeed.d.ts +30 -0
- package/nextErrorFeed.js +64 -0
- package/package.json +5 -12
- package/previewHealth.d.ts +25 -0
- package/previewHealth.js +431 -0
- package/sandbox-runtime.js +0 -2
- package/select-mode/EditorSelection.js +5 -1
- package/select-mode/fast-refresh-relay.js +82 -3
- package/select-mode/fiber.d.ts +6 -0
- package/select-mode/fiber.js +33 -0
- package/select-mode/hooks/useParentMessaging.js +10 -3
- package/select-mode/hooks/useSelectionHover.js +2 -1
- package/select-mode/hooks/useSelectionInteractions.js +10 -0
- package/select-mode/iframeRpcChannel.d.ts +46 -2
- package/select-mode/iframeRpcChannel.js +7 -0
- package/select-mode/section-canvas/SectionCanvas.d.ts +7 -0
- package/select-mode/section-canvas/SectionCanvas.js +599 -0
- package/select-mode/section-canvas/optimisticSectionMove.d.ts +39 -0
- package/select-mode/section-canvas/optimisticSectionMove.js +172 -0
- package/select-mode/section-canvas/sectionCanvasControls.d.ts +3 -0
- package/select-mode/section-canvas/sectionCanvasControls.js +8 -0
- package/select-mode/section-canvas/sectionCanvasStore.d.ts +71 -0
- package/select-mode/section-canvas/sectionCanvasStore.js +275 -0
- package/select-mode/section-canvas/sectionElements.d.ts +16 -0
- package/select-mode/section-canvas/sectionElements.js +94 -0
- package/select-mode/section-canvas/sectionGhost.d.ts +5 -0
- package/select-mode/section-canvas/sectionGhost.js +23 -0
- package/select-mode/symbolicate.d.ts +2 -0
- package/select-mode/symbolicate.js +18 -1
- package/NextErrorReporter.d.ts +0 -1
- package/NextErrorReporter.js +0 -131
- package/_vendor/schemas/componentPreview.d.ts +0 -43
- package/_vendor/schemas/componentPreview.mjs +0 -79
- package/_vendor/schemas/generated/componentPreview.d.ts +0 -5
- package/_vendor/schemas/generated/componentPreview.mjs +0 -1
- package/_vendor/schemas/loaderKeys.d.ts +0 -69
- package/next/component-preview-client.d.ts +0 -8
- package/next/component-preview-client.js +0 -227
- package/next/component-preview-href.d.ts +0 -5
- package/next/component-preview-href.js +0 -13
- package/next/component-preview-props.d.ts +0 -8
- package/next/component-preview-props.js +0 -35
- package/next/component-preview.d.ts +0 -11
- package/next/component-preview.js +0 -83
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const SETTLE_TRANSITION = "transform 180ms cubic-bezier(0.2, 0, 0, 1)";
|
|
2
|
+
function getTargetPosition({
|
|
3
|
+
indices,
|
|
4
|
+
from,
|
|
5
|
+
to
|
|
6
|
+
}) {
|
|
7
|
+
return indices.filter((index) => {
|
|
8
|
+
return index !== from && (index < from ? index : index - 1) < to;
|
|
9
|
+
}).length;
|
|
10
|
+
}
|
|
11
|
+
function getMovedSectionOffsets({
|
|
12
|
+
sections,
|
|
13
|
+
from,
|
|
14
|
+
to
|
|
15
|
+
}) {
|
|
16
|
+
const fromPosition = sections.findIndex((section) => section.index === from);
|
|
17
|
+
const toPosition = getTargetPosition({
|
|
18
|
+
indices: sections.map((section) => section.index),
|
|
19
|
+
from,
|
|
20
|
+
to
|
|
21
|
+
});
|
|
22
|
+
const offsets = /* @__PURE__ */ new Map();
|
|
23
|
+
if (fromPosition === -1 || fromPosition === toPosition) {
|
|
24
|
+
return offsets;
|
|
25
|
+
}
|
|
26
|
+
const low = Math.min(fromPosition, toPosition);
|
|
27
|
+
const high = Math.max(fromPosition, toPosition);
|
|
28
|
+
const original = sections.slice(low, high + 1);
|
|
29
|
+
const reordered = [...original];
|
|
30
|
+
const [moved] = reordered.splice(fromPosition - low, 1);
|
|
31
|
+
reordered.splice(toPosition - low, 0, moved);
|
|
32
|
+
let cursor = original[0].top;
|
|
33
|
+
reordered.forEach((section, slot) => {
|
|
34
|
+
offsets.set(section.index, cursor - section.top);
|
|
35
|
+
const gapAfterSlot = slot < original.length - 1 ? original[slot + 1].top - original[slot].bottom : 0;
|
|
36
|
+
cursor += section.bottom - section.top + gapAfterSlot;
|
|
37
|
+
});
|
|
38
|
+
return offsets;
|
|
39
|
+
}
|
|
40
|
+
function getSectionViewportBox(elements) {
|
|
41
|
+
const rects = (elements ?? []).filter((element) => element.isConnected).map((element) => element.getBoundingClientRect()).filter((rect) => rect.width !== 0 || rect.height !== 0);
|
|
42
|
+
if (rects.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
top: Math.min(...rects.map((rect) => rect.top)),
|
|
47
|
+
bottom: Math.max(...rects.map((rect) => rect.bottom)),
|
|
48
|
+
left: Math.min(...rects.map((rect) => rect.left)),
|
|
49
|
+
right: Math.max(...rects.map((rect) => rect.right))
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function startOptimisticSectionMove({
|
|
53
|
+
sections,
|
|
54
|
+
from,
|
|
55
|
+
to,
|
|
56
|
+
onLanded
|
|
57
|
+
}) {
|
|
58
|
+
const placed = sections.flatMap((section) => {
|
|
59
|
+
const box = getSectionViewportBox(section.elements);
|
|
60
|
+
return box ? [{ ...section, ...box }] : [];
|
|
61
|
+
});
|
|
62
|
+
const offsets = getMovedSectionOffsets({ sections: placed, from, to });
|
|
63
|
+
const shifted = [];
|
|
64
|
+
for (const section of placed) {
|
|
65
|
+
const offset = offsets.get(section.index);
|
|
66
|
+
if (!offset) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
for (const element of section.elements) {
|
|
70
|
+
const { transform, transition, position, zIndex } = element.style;
|
|
71
|
+
shifted.push({
|
|
72
|
+
element,
|
|
73
|
+
saved: { transform, transition, position, zIndex }
|
|
74
|
+
});
|
|
75
|
+
element.style.transition = SETTLE_TRANSITION;
|
|
76
|
+
element.style.transform = `${transform} translateY(${offset}px)`.trim();
|
|
77
|
+
if (section.index === from) {
|
|
78
|
+
element.style.position = position || "relative";
|
|
79
|
+
element.style.zIndex = "1";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const order = placed.map((section) => section.index);
|
|
84
|
+
const fromPosition = order.indexOf(from);
|
|
85
|
+
const reordered = [...order];
|
|
86
|
+
reordered.splice(fromPosition, 1);
|
|
87
|
+
reordered.splice(getTargetPosition({ indices: order, from, to }), 0, from);
|
|
88
|
+
const textBySection = new Map(
|
|
89
|
+
placed.map((section) => [
|
|
90
|
+
section.index,
|
|
91
|
+
section.elements.map((element) => element.textContent ?? "").join("\n")
|
|
92
|
+
])
|
|
93
|
+
);
|
|
94
|
+
const watchedSlots = placed.flatMap((section, slot) => {
|
|
95
|
+
const expected = textBySection.get(reordered[slot]);
|
|
96
|
+
return expected === textBySection.get(section.index) ? [] : [{ elements: section.elements, expected }];
|
|
97
|
+
});
|
|
98
|
+
const containers = new Set(
|
|
99
|
+
placed.flatMap((section) => {
|
|
100
|
+
return section.elements.flatMap((element) => {
|
|
101
|
+
return element.parentNode ? [element.parentNode] : [];
|
|
102
|
+
});
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
let isDone = false;
|
|
106
|
+
let fallbackTimeout;
|
|
107
|
+
const finish = ({ animate }) => {
|
|
108
|
+
if (isDone) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
isDone = true;
|
|
112
|
+
observer.disconnect();
|
|
113
|
+
clearTimeout(fallbackTimeout);
|
|
114
|
+
for (const { element, saved } of shifted) {
|
|
115
|
+
element.style.transition = animate ? SETTLE_TRANSITION : "none";
|
|
116
|
+
element.style.transform = saved.transform;
|
|
117
|
+
element.style.position = saved.position;
|
|
118
|
+
element.style.zIndex = saved.zIndex;
|
|
119
|
+
}
|
|
120
|
+
const restoreTransitions = () => {
|
|
121
|
+
for (const { element, saved } of shifted) {
|
|
122
|
+
element.style.transition = saved.transition;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
if (animate) {
|
|
126
|
+
setTimeout(restoreTransitions, 200);
|
|
127
|
+
} else {
|
|
128
|
+
restoreTransitions();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const hasLanded = (records) => {
|
|
132
|
+
if (records.some((record) => {
|
|
133
|
+
return record.type === "childList" && containers.has(record.target);
|
|
134
|
+
})) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return watchedSlots.length > 0 && watchedSlots.every(({ elements, expected }) => {
|
|
138
|
+
return elements.some((element) => !element.isConnected) || elements.map((element) => element.textContent ?? "").join("\n") === expected;
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
const observer = new MutationObserver((records) => {
|
|
142
|
+
if (hasLanded(records)) {
|
|
143
|
+
finish({ animate: false });
|
|
144
|
+
onLanded();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
for (const container of containers) {
|
|
148
|
+
observer.observe(container, {
|
|
149
|
+
childList: true,
|
|
150
|
+
subtree: true,
|
|
151
|
+
characterData: true
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
revert: () => finish({ animate: true }),
|
|
156
|
+
settle: () => {
|
|
157
|
+
if (isDone) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
clearTimeout(fallbackTimeout);
|
|
161
|
+
fallbackTimeout = setTimeout(() => {
|
|
162
|
+
finish({ animate: false });
|
|
163
|
+
onLanded();
|
|
164
|
+
}, 8e3);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export {
|
|
169
|
+
getMovedSectionOffsets,
|
|
170
|
+
getSectionViewportBox,
|
|
171
|
+
startOptimisticSectionMove
|
|
172
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
const SECTION_CANVAS_ATTRIBUTE = "data-replo-section-canvas";
|
|
2
|
+
function isSectionCanvasControl(target) {
|
|
3
|
+
return target instanceof Element && Boolean(target.closest(`[${SECTION_CANVAS_ATTRIBUTE}]`));
|
|
4
|
+
}
|
|
5
|
+
export {
|
|
6
|
+
SECTION_CANVAS_ATTRIBUTE,
|
|
7
|
+
isSectionCanvasControl
|
|
8
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SectionCanvasRequest, SectionCanvasState } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
2
|
+
export type SectionDrag = {
|
|
3
|
+
index: number;
|
|
4
|
+
pointerId: number;
|
|
5
|
+
startX: number;
|
|
6
|
+
startY: number;
|
|
7
|
+
x: number;
|
|
8
|
+
y: number;
|
|
9
|
+
isActive: boolean;
|
|
10
|
+
ghostSrc: string | null;
|
|
11
|
+
gap: number | null;
|
|
12
|
+
};
|
|
13
|
+
type PendingMove = {
|
|
14
|
+
moveId: string;
|
|
15
|
+
settle: () => void;
|
|
16
|
+
revert: () => void;
|
|
17
|
+
};
|
|
18
|
+
type SectionCanvasStoreState = {
|
|
19
|
+
canvas: SectionCanvasState | null;
|
|
20
|
+
elementsByIndex: ReadonlyMap<number, readonly HTMLElement[]>;
|
|
21
|
+
hoveredIndex: number | null;
|
|
22
|
+
drag: SectionDrag | null;
|
|
23
|
+
pendingMove: PendingMove | null;
|
|
24
|
+
geometryVersion: number;
|
|
25
|
+
actions: {
|
|
26
|
+
setCanvas: (canvas: SectionCanvasState | null) => void;
|
|
27
|
+
resolveSections: () => Promise<void>;
|
|
28
|
+
handlePreviewRefresh: () => void;
|
|
29
|
+
bumpGeometry: () => void;
|
|
30
|
+
setHoveredIndex: (index: number | null) => void;
|
|
31
|
+
request: (request: SectionCanvasRequest) => void;
|
|
32
|
+
beginDrag: (input: {
|
|
33
|
+
index: number;
|
|
34
|
+
pointerId: number;
|
|
35
|
+
x: number;
|
|
36
|
+
y: number;
|
|
37
|
+
}) => void;
|
|
38
|
+
updateDrag: (input: {
|
|
39
|
+
x: number;
|
|
40
|
+
y: number;
|
|
41
|
+
}) => void;
|
|
42
|
+
endDrag: (input: {
|
|
43
|
+
shouldDrop: boolean;
|
|
44
|
+
}) => void;
|
|
45
|
+
move: (input: {
|
|
46
|
+
index: number;
|
|
47
|
+
to: number;
|
|
48
|
+
}) => void;
|
|
49
|
+
settleMove: (input: {
|
|
50
|
+
moveId: string;
|
|
51
|
+
outcome: "persisted" | "failed";
|
|
52
|
+
}) => void;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export declare const GHOST_WIDTH_PX = 240;
|
|
56
|
+
/** On-screen sections in page order, with their boxes. */
|
|
57
|
+
export declare function getPlacedSections(state: Pick<SectionCanvasStoreState, "canvas" | "elementsByIndex">): {
|
|
58
|
+
section: import("../../_vendor/schemas/generated/selectModeIframeRpc").SectionCanvasSection;
|
|
59
|
+
box: import("./optimisticSectionMove").ViewportBox;
|
|
60
|
+
}[];
|
|
61
|
+
/**
|
|
62
|
+
* The insertion points between on-screen sections: `at` is the index a new or moved section goes
|
|
63
|
+
* before in the page's list, `y` the viewport line drawn for it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getSectionGaps(placed: ReturnType<typeof getPlacedSections>, sectionCount: number): {
|
|
66
|
+
at: number;
|
|
67
|
+
y: number;
|
|
68
|
+
}[];
|
|
69
|
+
export declare const sectionCanvasStore: import("zustand/traditional").UseBoundStoreWithEqualityFn<import("zustand").StoreApi<SectionCanvasStoreState>>;
|
|
70
|
+
export declare function useSectionCanvasStore<Selected>(selector: (state: SectionCanvasStoreState) => Selected): Selected;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createShallowStore } from "../../_vendor/replo-utils/react/zustand.mjs";
|
|
3
|
+
import { getWebsiteBuilderRpc } from "../iframeRpcChannel";
|
|
4
|
+
import { getDocumentScrollOffset } from "../interactions/selectionGeometry";
|
|
5
|
+
import { useShallowStore } from "../useShallowStore";
|
|
6
|
+
import {
|
|
7
|
+
getSectionViewportBox,
|
|
8
|
+
startOptimisticSectionMove
|
|
9
|
+
} from "./optimisticSectionMove";
|
|
10
|
+
import { findSectionElements } from "./sectionElements";
|
|
11
|
+
import { captureSectionGhost } from "./sectionGhost";
|
|
12
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
13
|
+
const GHOST_WIDTH_PX = 240;
|
|
14
|
+
let resolveToken = 0;
|
|
15
|
+
let moveCounter = 0;
|
|
16
|
+
function sameSections(previous, next) {
|
|
17
|
+
if (!previous || !next || previous.pageFilePath !== next.pageFilePath) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return previous.sections.length === next.sections.length && previous.sections.every((section, position) => {
|
|
21
|
+
const other = next.sections[position];
|
|
22
|
+
return section.index === other.index && section.startLine === other.startLine && section.endLine === other.endLine;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function getPlacedSections(state) {
|
|
26
|
+
return (state.canvas?.sections ?? []).flatMap((section) => {
|
|
27
|
+
const box = getSectionViewportBox(state.elementsByIndex.get(section.index));
|
|
28
|
+
return box ? [{ section, box }] : [];
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function getSectionGaps(placed, sectionCount) {
|
|
32
|
+
const gaps = placed.map(({ section, box }, position) => {
|
|
33
|
+
const previous = placed[position - 1];
|
|
34
|
+
return {
|
|
35
|
+
at: section.index,
|
|
36
|
+
y: previous ? (previous.box.bottom + box.top) / 2 : box.top
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const last = placed.at(-1);
|
|
40
|
+
if (last) {
|
|
41
|
+
gaps.push({
|
|
42
|
+
at: Math.max(last.section.index + 1, sectionCount),
|
|
43
|
+
y: last.box.bottom
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return gaps;
|
|
47
|
+
}
|
|
48
|
+
function getDropGap({
|
|
49
|
+
state,
|
|
50
|
+
index,
|
|
51
|
+
y
|
|
52
|
+
}) {
|
|
53
|
+
const gaps = getSectionGaps(
|
|
54
|
+
getPlacedSections(state),
|
|
55
|
+
state.canvas?.sections.length ?? 0
|
|
56
|
+
);
|
|
57
|
+
let nearest = null;
|
|
58
|
+
for (const gap of gaps) {
|
|
59
|
+
const distance = Math.abs(gap.y - y);
|
|
60
|
+
if (!nearest || distance < nearest.distance) {
|
|
61
|
+
nearest = { at: gap.at, distance };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!nearest || nearest.at === index || nearest.at === index + 1) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return nearest.at;
|
|
68
|
+
}
|
|
69
|
+
function reportLayout(state) {
|
|
70
|
+
if (!state.canvas) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const scroll = getDocumentScrollOffset();
|
|
74
|
+
const sections = getPlacedSections(state).map(
|
|
75
|
+
({ section, box }) => {
|
|
76
|
+
const left = box.left + scroll.left;
|
|
77
|
+
const top = box.top + scroll.top;
|
|
78
|
+
const width = box.right - box.left;
|
|
79
|
+
const height = box.bottom - box.top;
|
|
80
|
+
return {
|
|
81
|
+
index: section.index,
|
|
82
|
+
startLine: section.startLine,
|
|
83
|
+
endLine: section.endLine,
|
|
84
|
+
rect: {
|
|
85
|
+
x: left,
|
|
86
|
+
y: top,
|
|
87
|
+
left,
|
|
88
|
+
top,
|
|
89
|
+
right: left + width,
|
|
90
|
+
bottom: top + height,
|
|
91
|
+
width,
|
|
92
|
+
height
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
getWebsiteBuilderRpc()?.sectionCanvasLayoutChanged({ sections });
|
|
98
|
+
}
|
|
99
|
+
const sectionCanvasStore = createShallowStore(
|
|
100
|
+
(set, get) => ({
|
|
101
|
+
canvas: null,
|
|
102
|
+
elementsByIndex: /* @__PURE__ */ new Map(),
|
|
103
|
+
hoveredIndex: null,
|
|
104
|
+
drag: null,
|
|
105
|
+
pendingMove: null,
|
|
106
|
+
geometryVersion: 0,
|
|
107
|
+
actions: {
|
|
108
|
+
setCanvas: (canvas) => {
|
|
109
|
+
const previous = get().canvas;
|
|
110
|
+
set({ canvas });
|
|
111
|
+
if (!canvas) {
|
|
112
|
+
resolveToken += 1;
|
|
113
|
+
get().pendingMove?.revert();
|
|
114
|
+
set({
|
|
115
|
+
elementsByIndex: /* @__PURE__ */ new Map(),
|
|
116
|
+
hoveredIndex: null,
|
|
117
|
+
drag: null,
|
|
118
|
+
pendingMove: null
|
|
119
|
+
});
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (canvas.isDisabled && get().drag) {
|
|
123
|
+
set({ drag: null });
|
|
124
|
+
}
|
|
125
|
+
if (!sameSections(previous, canvas) && !get().pendingMove) {
|
|
126
|
+
void get().actions.resolveSections();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
resolveSections: async () => {
|
|
130
|
+
const canvas = get().canvas;
|
|
131
|
+
if (!canvas || typeof document === "undefined" || !document.body) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const token = ++resolveToken;
|
|
135
|
+
const elementsByIndex = await findSectionElements({
|
|
136
|
+
root: document.body,
|
|
137
|
+
pageFilePath: canvas.pageFilePath,
|
|
138
|
+
sections: canvas.sections
|
|
139
|
+
}).catch(() => /* @__PURE__ */ new Map());
|
|
140
|
+
if (token !== resolveToken) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
set((state) => ({
|
|
144
|
+
elementsByIndex,
|
|
145
|
+
geometryVersion: state.geometryVersion + 1
|
|
146
|
+
}));
|
|
147
|
+
reportLayout(get());
|
|
148
|
+
},
|
|
149
|
+
handlePreviewRefresh: () => {
|
|
150
|
+
if (!get().canvas || get().pendingMove) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
void get().actions.resolveSections();
|
|
154
|
+
},
|
|
155
|
+
bumpGeometry: () => {
|
|
156
|
+
set((state) => ({ geometryVersion: state.geometryVersion + 1 }));
|
|
157
|
+
},
|
|
158
|
+
setHoveredIndex: (hoveredIndex) => {
|
|
159
|
+
if (get().hoveredIndex !== hoveredIndex) {
|
|
160
|
+
set({ hoveredIndex });
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
request: (request) => {
|
|
164
|
+
getWebsiteBuilderRpc()?.sectionCanvasRequest({ request });
|
|
165
|
+
},
|
|
166
|
+
beginDrag: ({ index, pointerId, x, y }) => {
|
|
167
|
+
const elements = get().elementsByIndex.get(index) ?? [];
|
|
168
|
+
set({
|
|
169
|
+
drag: {
|
|
170
|
+
index,
|
|
171
|
+
pointerId,
|
|
172
|
+
startX: x,
|
|
173
|
+
startY: y,
|
|
174
|
+
x,
|
|
175
|
+
y,
|
|
176
|
+
isActive: false,
|
|
177
|
+
ghostSrc: null,
|
|
178
|
+
gap: null
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const scale = get().canvas?.previewScale ?? 1;
|
|
182
|
+
void captureSectionGhost({
|
|
183
|
+
elements,
|
|
184
|
+
width: GHOST_WIDTH_PX / scale
|
|
185
|
+
}).then((ghostSrc) => {
|
|
186
|
+
const drag = get().drag;
|
|
187
|
+
if (ghostSrc && drag?.pointerId === pointerId && drag.index === index) {
|
|
188
|
+
set({ drag: { ...drag, ghostSrc } });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
updateDrag: ({ x, y }) => {
|
|
193
|
+
const state = get();
|
|
194
|
+
const drag = state.drag;
|
|
195
|
+
if (!drag) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const scale = state.canvas?.previewScale ?? 1;
|
|
199
|
+
const isActive = drag.isActive || Math.hypot(x - drag.startX, y - drag.startY) * scale > DRAG_THRESHOLD_PX;
|
|
200
|
+
set({
|
|
201
|
+
drag: {
|
|
202
|
+
...drag,
|
|
203
|
+
x,
|
|
204
|
+
y,
|
|
205
|
+
isActive,
|
|
206
|
+
gap: isActive ? getDropGap({ state, index: drag.index, y }) : null
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
endDrag: ({ shouldDrop }) => {
|
|
211
|
+
const drag = get().drag;
|
|
212
|
+
set({ drag: null });
|
|
213
|
+
if (!shouldDrop || !drag?.isActive || drag.gap === null) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
get().actions.move({
|
|
217
|
+
index: drag.index,
|
|
218
|
+
to: drag.gap > drag.index ? drag.gap - 1 : drag.gap
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
move: ({ index, to }) => {
|
|
222
|
+
const state = get();
|
|
223
|
+
const sectionCount = state.canvas?.sections.length ?? 0;
|
|
224
|
+
if (!state.canvas || state.canvas.isDisabled || state.pendingMove || state.canvas.sections[index]?.isLocked !== false || to === index || to < 0 || to >= sectionCount) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
moveCounter += 1;
|
|
228
|
+
const moveId = `section-move-${Date.now()}-${moveCounter}`;
|
|
229
|
+
const optimistic = startOptimisticSectionMove({
|
|
230
|
+
sections: state.canvas.sections.map((section) => ({
|
|
231
|
+
index: section.index,
|
|
232
|
+
elements: [...state.elementsByIndex.get(section.index) ?? []]
|
|
233
|
+
})),
|
|
234
|
+
from: index,
|
|
235
|
+
to,
|
|
236
|
+
onLanded: () => {
|
|
237
|
+
if (get().pendingMove?.moveId !== moveId) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
set({ pendingMove: null });
|
|
241
|
+
void get().actions.resolveSections();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
set({
|
|
245
|
+
pendingMove: { moveId, ...optimistic },
|
|
246
|
+
hoveredIndex: null
|
|
247
|
+
});
|
|
248
|
+
get().actions.request({ type: "move", moveId, index, to });
|
|
249
|
+
},
|
|
250
|
+
settleMove: ({ moveId, outcome }) => {
|
|
251
|
+
const pendingMove = get().pendingMove;
|
|
252
|
+
if (pendingMove?.moveId !== moveId) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (outcome === "persisted") {
|
|
256
|
+
pendingMove.settle();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
pendingMove.revert();
|
|
260
|
+
set({ pendingMove: null });
|
|
261
|
+
void get().actions.resolveSections();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
);
|
|
266
|
+
function useSectionCanvasStore(selector) {
|
|
267
|
+
return useShallowStore(sectionCanvasStore, selector);
|
|
268
|
+
}
|
|
269
|
+
export {
|
|
270
|
+
GHOST_WIDTH_PX,
|
|
271
|
+
getPlacedSections,
|
|
272
|
+
getSectionGaps,
|
|
273
|
+
sectionCanvasStore,
|
|
274
|
+
useSectionCanvasStore
|
|
275
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SelectModeSourceLocation } from "../../_vendor/schemas/generated/selectMode";
|
|
2
|
+
import type { SectionCanvasSection } from "../../_vendor/schemas/generated/selectModeIframeRpc";
|
|
3
|
+
type Resolve = (locations: readonly SelectModeSourceLocation[]) => Promise<SelectModeSourceLocation[]>;
|
|
4
|
+
/**
|
|
5
|
+
* 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 lines. Walks down from <body> a level at a time, symbolicating each level
|
|
7
|
+
* in one request and stopping at the first element that matches, so a section's descendants are
|
|
8
|
+
* never read.
|
|
9
|
+
*/
|
|
10
|
+
export declare function findSectionElements({ root, pageFilePath, sections, resolve, }: {
|
|
11
|
+
root: Element;
|
|
12
|
+
pageFilePath: string;
|
|
13
|
+
sections: readonly Pick<SectionCanvasSection, "index" | "startLine" | "endLine">[];
|
|
14
|
+
resolve?: Resolve;
|
|
15
|
+
}): Promise<Map<number, HTMLElement[]>>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isSectionCanvasPageFile } from "../../_vendor/schemas/selectModeIframeRpc.mjs";
|
|
2
|
+
import { getElementSourceChain } from "../fiber";
|
|
3
|
+
import { symbolicateSourceLocations } from "../symbolicate";
|
|
4
|
+
import { isSectionCanvasControl } from "./sectionCanvasControls";
|
|
5
|
+
const MAX_DEPTH = 14;
|
|
6
|
+
const MAX_ELEMENTS = 2e3;
|
|
7
|
+
const SKIPPED_TAGS = /* @__PURE__ */ new Set([
|
|
8
|
+
"SCRIPT",
|
|
9
|
+
"STYLE",
|
|
10
|
+
"LINK",
|
|
11
|
+
"META",
|
|
12
|
+
"TEMPLATE",
|
|
13
|
+
"NOSCRIPT"
|
|
14
|
+
]);
|
|
15
|
+
async function findSectionElements({
|
|
16
|
+
root,
|
|
17
|
+
pageFilePath,
|
|
18
|
+
sections,
|
|
19
|
+
resolve = symbolicateSourceLocations
|
|
20
|
+
}) {
|
|
21
|
+
const found = /* @__PURE__ */ new Map();
|
|
22
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
23
|
+
let level = Array.from(root.children);
|
|
24
|
+
let visited = 0;
|
|
25
|
+
for (let depth = 0; depth < MAX_DEPTH && level.length > 0; depth += 1) {
|
|
26
|
+
const candidates = level.flatMap((element) => {
|
|
27
|
+
if (!(element instanceof HTMLElement) || SKIPPED_TAGS.has(element.tagName) || isSectionCanvasControl(element)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const chain = getElementSourceChain(element).map(
|
|
31
|
+
(source) => ({
|
|
32
|
+
filePath: source.fileName,
|
|
33
|
+
lineNumber: source.lineNumber,
|
|
34
|
+
columnNumber: source.columnNumber
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
return [{ element, chain }];
|
|
38
|
+
});
|
|
39
|
+
visited += candidates.length;
|
|
40
|
+
const pending = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const { chain } of candidates) {
|
|
42
|
+
for (const location of chain) {
|
|
43
|
+
const key = locationKey(location);
|
|
44
|
+
if (!resolved.has(key)) {
|
|
45
|
+
pending.set(key, location);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (pending.size > 0) {
|
|
50
|
+
const keys = [...pending.keys()];
|
|
51
|
+
const results = await resolve([...pending.values()]);
|
|
52
|
+
keys.forEach((key, index) => {
|
|
53
|
+
resolved.set(key, results[index] ?? pending.get(key));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const next = [];
|
|
57
|
+
for (const { element, chain } of candidates) {
|
|
58
|
+
let section;
|
|
59
|
+
for (const location of chain) {
|
|
60
|
+
const original = resolved.get(locationKey(location));
|
|
61
|
+
if (!original || !isSectionCanvasPageFile({
|
|
62
|
+
filePath: original.filePath,
|
|
63
|
+
pageFilePath
|
|
64
|
+
})) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
section = sections.find((candidate) => {
|
|
68
|
+
return candidate.startLine <= original.lineNumber && original.lineNumber <= candidate.endLine;
|
|
69
|
+
});
|
|
70
|
+
if (section) {
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (section) {
|
|
75
|
+
const elements = found.get(section.index) ?? [];
|
|
76
|
+
elements.push(element);
|
|
77
|
+
found.set(section.index, elements);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
next.push(...Array.from(element.children));
|
|
81
|
+
}
|
|
82
|
+
if (visited >= MAX_ELEMENTS) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
level = next;
|
|
86
|
+
}
|
|
87
|
+
return found;
|
|
88
|
+
}
|
|
89
|
+
function locationKey(location) {
|
|
90
|
+
return `${location.filePath}:${location.lineNumber}:${location.columnNumber ?? ""}`;
|
|
91
|
+
}
|
|
92
|
+
export {
|
|
93
|
+
findSectionElements
|
|
94
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
async function captureSectionGhost({
|
|
3
|
+
elements,
|
|
4
|
+
width
|
|
5
|
+
}) {
|
|
6
|
+
const element = elements.find((candidate) => candidate.isConnected);
|
|
7
|
+
if (!element) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
const rect = element.getBoundingClientRect();
|
|
11
|
+
if (rect.width === 0 || rect.height === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
const { snapdom } = await import("@zumer/snapdom");
|
|
16
|
+
return await snapdom.toRaw(element, { width, embedFonts: false });
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
captureSectionGhost
|
|
23
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { SelectedElement, SelectModeSourceLocation } from "../_vendor/schemas/generated/selectMode";
|
|
2
2
|
export declare function invalidateSymbolicationCache(): void;
|
|
3
|
+
/** Resolves compiled locations to original files in one batched request; unresolved ones come back unchanged. */
|
|
4
|
+
export declare function symbolicateSourceLocations(locations: readonly SelectModeSourceLocation[]): Promise<SelectModeSourceLocation[]>;
|
|
3
5
|
export declare function isUserSourceLocation(location: SelectModeSourceLocation | undefined): boolean;
|
|
4
6
|
export declare function symbolicateSelectedElement(selectedElement: SelectedElement): Promise<SelectedElement>;
|