@replohq/sandbox-runtime 1.16.1 → 1.17.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/_vendor/replo-utils/lib/historyKeys.d.ts +2 -0
- package/_vendor/replo-utils/lib/historyKeys.mjs +18 -0
- package/_vendor/replo-utils/lib/viewportSnapshot.d.ts +1 -1
- package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
- package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +28 -5222
- package/_vendor/schemas/errors.d.ts +3 -43
- package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +43 -0
- package/_vendor/schemas/iframeRpcChannel.mjs +2 -0
- package/_vendor/schemas/reploError.d.ts +43 -0
- package/_vendor/schemas/selectModeIframeRpc.d.ts +146 -7
- package/_vendor/schemas/selectModeIframeRpc.mjs +73 -10
- package/buildMetadata.js +3 -3
- package/{chunk-3YA3QXUQ.mjs → chunk-B3PLD7SH.mjs} +29 -18
- package/package.json +4 -4
- package/rrweb-snapshot-GTQBXO2Y.mjs +5761 -0
- 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 +45 -7
- package/select-mode/iframeRpcChannel.js +9 -8
- 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
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
import { historyStepForKey } from "../../_vendor/replo-utils/lib/historyKeys.mjs";
|
|
6
|
+
import {
|
|
7
|
+
useDisplayMode,
|
|
8
|
+
useEditingElement,
|
|
9
|
+
useInteractionSession
|
|
10
|
+
} from "../selectModeStore";
|
|
11
|
+
import { getSectionViewportBox } from "./optimisticSectionMove";
|
|
12
|
+
import {
|
|
13
|
+
isSectionCanvasControl,
|
|
14
|
+
SECTION_CANVAS_ATTRIBUTE
|
|
15
|
+
} from "./sectionCanvasControls";
|
|
16
|
+
import {
|
|
17
|
+
getPlacedSections,
|
|
18
|
+
getSectionGaps,
|
|
19
|
+
GHOST_WIDTH_PX,
|
|
20
|
+
sectionCanvasStore,
|
|
21
|
+
useSectionCanvasStore
|
|
22
|
+
} from "./sectionCanvasStore";
|
|
23
|
+
const BUTTON_SIZE = 28;
|
|
24
|
+
const TOOLBAR_INSET = 8;
|
|
25
|
+
const AUTO_SCROLL_EDGE = 56;
|
|
26
|
+
const AUTO_SCROLL_STEP = 14;
|
|
27
|
+
const ACCENT = "#2563eb";
|
|
28
|
+
const ACCENT_PRESSED = "#1d4ed8";
|
|
29
|
+
const PILL_HEIGHT = 22;
|
|
30
|
+
const PILL_SPREAD = 64;
|
|
31
|
+
const PILL_HALF_WIDTH = 52;
|
|
32
|
+
const SECTION_CANVAS_CSS = `
|
|
33
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button { all: initial; box-sizing: border-box; display: flex; align-items: center; justify-content: center; pointer-events: auto; cursor: pointer !important; color: ${ACCENT}; background: #fff !important; }
|
|
34
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button * { cursor: inherit !important; }
|
|
35
|
+
[${SECTION_CANVAS_ATTRIBUTE}] [data-shape="pill"] { position: absolute; transform: translate(-50%, -50%); white-space: nowrap; border-radius: 4px; background: ${ACCENT} !important; color: #fff !important; font-family: system-ui, -apple-system, sans-serif; font-weight: 600; box-shadow: 0 2px 6px rgba(15, 23, 42, 0.2); }
|
|
36
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="pill"]:hover, [${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="pill"]:focus-visible { background: ${ACCENT_PRESSED} !important; color: #fff !important; }
|
|
37
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="tool"] { border-radius: 4px; gap: 4px; background: transparent !important; color: #fff !important; font-family: system-ui, -apple-system, sans-serif; font-weight: 600; }
|
|
38
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="tool"][data-grip] { cursor: grab !important; }
|
|
39
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="tool"]:hover, [${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="tool"]:focus-visible { background: ${ACCENT_PRESSED} !important; }
|
|
40
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button[data-shape="tool"][data-locked] { cursor: default !important; background: transparent !important; }
|
|
41
|
+
[${SECTION_CANVAS_ATTRIBUTE}] button:focus-visible { outline: 2px solid ${ACCENT}; outline-offset: 2px; }
|
|
42
|
+
[${SECTION_CANVAS_ATTRIBUTE}][data-dragging] * { cursor: grabbing !important; }
|
|
43
|
+
`;
|
|
44
|
+
function useSectionCanvasListeners(isEnabled) {
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!isEnabled) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { actions } = sectionCanvasStore.getState();
|
|
50
|
+
let frame = 0;
|
|
51
|
+
const scheduleGeometry = () => {
|
|
52
|
+
if (frame) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
frame = requestAnimationFrame(() => {
|
|
56
|
+
frame = 0;
|
|
57
|
+
actions.bumpGeometry();
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
const trackHover = (event) => {
|
|
61
|
+
const state = sectionCanvasStore.getState();
|
|
62
|
+
if (state.drag || isSectionCanvasControl(event.target)) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const hovered = getPlacedSections(state).find(({ box }) => {
|
|
66
|
+
return event.clientY >= box.top && event.clientY <= box.bottom && event.clientX >= box.left && event.clientX <= box.right;
|
|
67
|
+
});
|
|
68
|
+
actions.setHoveredIndex(hovered?.section.index ?? null);
|
|
69
|
+
};
|
|
70
|
+
const clearHover = (event) => {
|
|
71
|
+
if (!event.relatedTarget) {
|
|
72
|
+
actions.setHoveredIndex(null);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handleKeyDown = (event) => {
|
|
76
|
+
const { drag, canvas } = sectionCanvasStore.getState();
|
|
77
|
+
if (event.key === "Escape" && drag) {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
actions.endDrag({ shouldDrop: false });
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (event.key === "Escape" && typeof canvas?.insertingAt === "number") {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
actions.request({ type: "cancelInsert" });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
let resolveTimeout;
|
|
88
|
+
const domObserver = new MutationObserver(() => {
|
|
89
|
+
const { elementsByIndex } = sectionCanvasStore.getState();
|
|
90
|
+
const isStale = elementsByIndex.size === 0 || [...elementsByIndex.values()].some((elements) => {
|
|
91
|
+
return elements.some((element) => !element.isConnected);
|
|
92
|
+
});
|
|
93
|
+
if (!isStale) {
|
|
94
|
+
scheduleGeometry();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
clearTimeout(resolveTimeout);
|
|
98
|
+
resolveTimeout = setTimeout(() => actions.handlePreviewRefresh(), 250);
|
|
99
|
+
});
|
|
100
|
+
domObserver.observe(document.body, { childList: true, subtree: true });
|
|
101
|
+
const resizeObserver = new ResizeObserver(scheduleGeometry);
|
|
102
|
+
const observeSections = () => {
|
|
103
|
+
resizeObserver.disconnect();
|
|
104
|
+
for (const elements of sectionCanvasStore.getState().elementsByIndex.values()) {
|
|
105
|
+
for (const element of elements) {
|
|
106
|
+
resizeObserver.observe(element);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
observeSections();
|
|
111
|
+
const unsubscribe = sectionCanvasStore.subscribe((state, previous) => {
|
|
112
|
+
if (state.elementsByIndex !== previous.elementsByIndex) {
|
|
113
|
+
observeSections();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
document.addEventListener("pointermove", trackHover, true);
|
|
117
|
+
document.addEventListener("pointerout", clearHover, true);
|
|
118
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
119
|
+
document.addEventListener("scroll", scheduleGeometry, true);
|
|
120
|
+
window.addEventListener("resize", scheduleGeometry);
|
|
121
|
+
return () => {
|
|
122
|
+
cancelAnimationFrame(frame);
|
|
123
|
+
clearTimeout(resolveTimeout);
|
|
124
|
+
domObserver.disconnect();
|
|
125
|
+
resizeObserver.disconnect();
|
|
126
|
+
unsubscribe();
|
|
127
|
+
document.removeEventListener("pointermove", trackHover, true);
|
|
128
|
+
document.removeEventListener("pointerout", clearHover, true);
|
|
129
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
130
|
+
document.removeEventListener("scroll", scheduleGeometry, true);
|
|
131
|
+
window.removeEventListener("resize", scheduleGeometry);
|
|
132
|
+
actions.setHoveredIndex(null);
|
|
133
|
+
};
|
|
134
|
+
}, [isEnabled]);
|
|
135
|
+
}
|
|
136
|
+
function useHistoryShortcutRelay(hasCanvas) {
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!hasCanvas) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const handleKeyDown = (event) => {
|
|
142
|
+
const step = historyStepForKey(event);
|
|
143
|
+
if (!step) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
event.preventDefault();
|
|
147
|
+
if (!event.repeat) {
|
|
148
|
+
sectionCanvasStore.getState().actions.request({ type: step });
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
152
|
+
return () => document.removeEventListener("keydown", handleKeyDown, true);
|
|
153
|
+
}, [hasCanvas]);
|
|
154
|
+
}
|
|
155
|
+
function useAutoScrollWhileDragging(isDragging) {
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (!isDragging) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
let frame = requestAnimationFrame(function step() {
|
|
161
|
+
const { drag, canvas, actions } = sectionCanvasStore.getState();
|
|
162
|
+
if (drag?.isActive) {
|
|
163
|
+
const edge = AUTO_SCROLL_EDGE / (canvas?.previewScale ?? 1);
|
|
164
|
+
const step2 = AUTO_SCROLL_STEP / (canvas?.previewScale ?? 1);
|
|
165
|
+
let delta = 0;
|
|
166
|
+
if (drag.y < edge) {
|
|
167
|
+
delta = -step2;
|
|
168
|
+
} else if (drag.y > window.innerHeight - edge) {
|
|
169
|
+
delta = step2;
|
|
170
|
+
}
|
|
171
|
+
if (delta !== 0) {
|
|
172
|
+
window.scrollBy(0, delta);
|
|
173
|
+
actions.updateDrag({ x: drag.x, y: drag.y });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
frame = requestAnimationFrame(step);
|
|
177
|
+
});
|
|
178
|
+
return () => cancelAnimationFrame(frame);
|
|
179
|
+
}, [isDragging]);
|
|
180
|
+
}
|
|
181
|
+
function useCancelDragWhenDisabled(isEnabled) {
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const { endDrag } = sectionCanvasStore.getState().actions;
|
|
184
|
+
if (!isEnabled) {
|
|
185
|
+
endDrag({ shouldDrop: false });
|
|
186
|
+
return void 0;
|
|
187
|
+
}
|
|
188
|
+
return () => endDrag({ shouldDrop: false });
|
|
189
|
+
}, [isEnabled]);
|
|
190
|
+
}
|
|
191
|
+
function SectionCanvas() {
|
|
192
|
+
const displayMode = useDisplayMode();
|
|
193
|
+
const editingElement = useEditingElement();
|
|
194
|
+
const session = useInteractionSession();
|
|
195
|
+
const { canvas, elementsByIndex, hoveredIndex, drag, isMovePending } = useSectionCanvasStore((state) => ({
|
|
196
|
+
canvas: state.canvas,
|
|
197
|
+
elementsByIndex: state.elementsByIndex,
|
|
198
|
+
hoveredIndex: state.hoveredIndex,
|
|
199
|
+
drag: state.drag,
|
|
200
|
+
isMovePending: state.pendingMove !== null,
|
|
201
|
+
geometryVersion: state.geometryVersion
|
|
202
|
+
}));
|
|
203
|
+
const isEnabled = canvas !== null && displayMode === "selection" && !editingElement && !canvas.isDisabled;
|
|
204
|
+
useSectionCanvasListeners(isEnabled);
|
|
205
|
+
useHistoryShortcutRelay(canvas !== null);
|
|
206
|
+
useAutoScrollWhileDragging(drag !== null);
|
|
207
|
+
useCancelDragWhenDisabled(isEnabled);
|
|
208
|
+
if (!isEnabled || typeof document === "undefined" || !document.body) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const unit = 1 / canvas.previewScale;
|
|
212
|
+
const visible = {
|
|
213
|
+
left: canvas.occludedInsets?.left ?? 0,
|
|
214
|
+
right: window.innerWidth - (canvas.occludedInsets?.right ?? 0)
|
|
215
|
+
};
|
|
216
|
+
const selectedIndex = session.type === "element" ? [...elementsByIndex.entries()].find(([, elements]) => {
|
|
217
|
+
return elements.some((element) => element.contains(session.element));
|
|
218
|
+
})?.[0] ?? null : null;
|
|
219
|
+
const activeIndex = drag ? drag.index : hoveredIndex ?? selectedIndex;
|
|
220
|
+
const activeSection = activeIndex === null ? void 0 : canvas.sections[activeIndex];
|
|
221
|
+
const activeBox = activeIndex === null ? null : getSectionViewportBox(elementsByIndex.get(activeIndex));
|
|
222
|
+
let overlay = null;
|
|
223
|
+
if (activeSection && activeBox && !isMovePending) {
|
|
224
|
+
overlay = /* @__PURE__ */ jsx(
|
|
225
|
+
SectionControls,
|
|
226
|
+
{
|
|
227
|
+
index: activeSection.index,
|
|
228
|
+
label: activeSection.label,
|
|
229
|
+
isLocked: activeSection.isLocked,
|
|
230
|
+
isFirst: activeSection.index === 0,
|
|
231
|
+
isLast: activeSection.index === canvas.sections.length - 1,
|
|
232
|
+
box: activeBox,
|
|
233
|
+
visible,
|
|
234
|
+
unit,
|
|
235
|
+
isDragging: drag !== null,
|
|
236
|
+
isPicking: typeof canvas.insertingAt === "number"
|
|
237
|
+
}
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
let insertMarker = null;
|
|
241
|
+
const insertingAt = canvas.insertingAt;
|
|
242
|
+
if (typeof insertingAt === "number" && !drag) {
|
|
243
|
+
const placed = getPlacedSections({ canvas, elementsByIndex });
|
|
244
|
+
const gap = getSectionGaps(placed, canvas.sections.length).find(
|
|
245
|
+
(candidate) => candidate.at === insertingAt
|
|
246
|
+
);
|
|
247
|
+
if (gap) {
|
|
248
|
+
const left = Math.max(
|
|
249
|
+
visible.left,
|
|
250
|
+
Math.min(...placed.map(({ box }) => box.left))
|
|
251
|
+
);
|
|
252
|
+
const right = Math.min(
|
|
253
|
+
visible.right,
|
|
254
|
+
Math.max(...placed.map(({ box }) => box.right))
|
|
255
|
+
);
|
|
256
|
+
const y = Math.min(
|
|
257
|
+
Math.max(gap.y, PILL_HEIGHT * unit),
|
|
258
|
+
window.innerHeight - PILL_HEIGHT * unit
|
|
259
|
+
);
|
|
260
|
+
insertMarker = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
261
|
+
/* @__PURE__ */ jsx(
|
|
262
|
+
"div",
|
|
263
|
+
{
|
|
264
|
+
style: {
|
|
265
|
+
position: "fixed",
|
|
266
|
+
left,
|
|
267
|
+
width: right - left,
|
|
268
|
+
top: y - 1.5 * unit,
|
|
269
|
+
height: 3 * unit,
|
|
270
|
+
background: ACCENT
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
),
|
|
274
|
+
/* @__PURE__ */ jsx(
|
|
275
|
+
"div",
|
|
276
|
+
{
|
|
277
|
+
"data-shape": "pill",
|
|
278
|
+
role: "status",
|
|
279
|
+
style: {
|
|
280
|
+
left: (left + right) / 2,
|
|
281
|
+
top: y,
|
|
282
|
+
height: PILL_HEIGHT * unit,
|
|
283
|
+
padding: `0 ${10 * unit}px`,
|
|
284
|
+
fontSize: 11 * unit,
|
|
285
|
+
display: "flex",
|
|
286
|
+
alignItems: "center"
|
|
287
|
+
},
|
|
288
|
+
children: "New Section Goes Here"
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
] });
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
let dragLayer = null;
|
|
295
|
+
if (drag?.isActive && activeBox) {
|
|
296
|
+
const placed = getPlacedSections({ canvas, elementsByIndex });
|
|
297
|
+
const gap = drag.gap === null ? null : getSectionGaps(placed, canvas.sections.length).find((candidate) => {
|
|
298
|
+
return candidate.at === drag.gap;
|
|
299
|
+
});
|
|
300
|
+
const lineLeft = Math.min(...placed.map(({ box }) => box.left));
|
|
301
|
+
const lineRight = Math.max(...placed.map(({ box }) => box.right));
|
|
302
|
+
const ghostWidth = GHOST_WIDTH_PX * unit;
|
|
303
|
+
const ghostHeight = Math.min(
|
|
304
|
+
(activeBox.bottom - activeBox.top) / (activeBox.right - activeBox.left) * ghostWidth,
|
|
305
|
+
ghostWidth * 0.75
|
|
306
|
+
);
|
|
307
|
+
dragLayer = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
308
|
+
/* @__PURE__ */ jsx(
|
|
309
|
+
"div",
|
|
310
|
+
{
|
|
311
|
+
style: {
|
|
312
|
+
position: "fixed",
|
|
313
|
+
left: activeBox.left,
|
|
314
|
+
top: activeBox.top,
|
|
315
|
+
width: activeBox.right - activeBox.left,
|
|
316
|
+
height: activeBox.bottom - activeBox.top,
|
|
317
|
+
background: "rgba(255, 255, 255, 0.65)",
|
|
318
|
+
outline: `${2 * unit}px dashed ${ACCENT}`,
|
|
319
|
+
outlineOffset: -unit
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
),
|
|
323
|
+
gap && /* @__PURE__ */ jsx(
|
|
324
|
+
"div",
|
|
325
|
+
{
|
|
326
|
+
"data-replo-section-drop-line": true,
|
|
327
|
+
style: {
|
|
328
|
+
position: "fixed",
|
|
329
|
+
left: lineLeft,
|
|
330
|
+
width: lineRight - lineLeft,
|
|
331
|
+
top: Math.min(
|
|
332
|
+
Math.max(gap.y, 3 * unit),
|
|
333
|
+
window.innerHeight - 3 * unit
|
|
334
|
+
) - 1.5 * unit,
|
|
335
|
+
height: 3 * unit,
|
|
336
|
+
borderRadius: 3 * unit,
|
|
337
|
+
background: ACCENT,
|
|
338
|
+
boxShadow: `0 0 0 ${unit}px #fff`
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
),
|
|
342
|
+
/* @__PURE__ */ jsx(
|
|
343
|
+
"div",
|
|
344
|
+
{
|
|
345
|
+
style: {
|
|
346
|
+
position: "fixed",
|
|
347
|
+
left: drag.x + 14 * unit + ghostWidth > window.innerWidth ? drag.x - 14 * unit - ghostWidth : drag.x + 14 * unit,
|
|
348
|
+
top: Math.min(
|
|
349
|
+
drag.y + 14 * unit,
|
|
350
|
+
window.innerHeight - ghostHeight - 8 * unit
|
|
351
|
+
),
|
|
352
|
+
width: ghostWidth,
|
|
353
|
+
height: ghostHeight,
|
|
354
|
+
overflow: "hidden",
|
|
355
|
+
borderRadius: 6 * unit,
|
|
356
|
+
background: "#fff",
|
|
357
|
+
boxShadow: `0 ${8 * unit}px ${24 * unit}px rgba(15, 23, 42, 0.25), 0 0 0 ${unit}px rgba(15, 23, 42, 0.12)`,
|
|
358
|
+
opacity: 0.95,
|
|
359
|
+
display: "flex",
|
|
360
|
+
alignItems: "center",
|
|
361
|
+
justifyContent: "center",
|
|
362
|
+
font: `500 ${12 * unit}px/1.3 system-ui, sans-serif`,
|
|
363
|
+
color: "#3f3f46"
|
|
364
|
+
},
|
|
365
|
+
children: drag.ghostSrc ? /* @__PURE__ */ jsx(
|
|
366
|
+
"img",
|
|
367
|
+
{
|
|
368
|
+
src: drag.ghostSrc,
|
|
369
|
+
alt: "",
|
|
370
|
+
style: {
|
|
371
|
+
display: "block",
|
|
372
|
+
width: "100%",
|
|
373
|
+
height: "auto",
|
|
374
|
+
alignSelf: "flex-start"
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
) : activeSection?.label ?? "Section"
|
|
378
|
+
}
|
|
379
|
+
)
|
|
380
|
+
] });
|
|
381
|
+
}
|
|
382
|
+
return createPortal(
|
|
383
|
+
/* @__PURE__ */ jsxs(
|
|
384
|
+
"div",
|
|
385
|
+
{
|
|
386
|
+
...{ [SECTION_CANVAS_ATTRIBUTE]: "" },
|
|
387
|
+
"data-dragging": drag?.isActive ? "" : void 0,
|
|
388
|
+
role: "group",
|
|
389
|
+
"aria-label": "Page sections",
|
|
390
|
+
style: {
|
|
391
|
+
all: "initial",
|
|
392
|
+
position: "fixed",
|
|
393
|
+
inset: 0,
|
|
394
|
+
zIndex: 2147483647,
|
|
395
|
+
pointerEvents: "none"
|
|
396
|
+
},
|
|
397
|
+
onPointerDown: (event) => event.stopPropagation(),
|
|
398
|
+
children: [
|
|
399
|
+
/* @__PURE__ */ jsx("style", { children: SECTION_CANVAS_CSS }),
|
|
400
|
+
insertMarker,
|
|
401
|
+
overlay,
|
|
402
|
+
dragLayer
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
document.body
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function SectionControls({
|
|
410
|
+
index,
|
|
411
|
+
label,
|
|
412
|
+
isLocked,
|
|
413
|
+
isFirst,
|
|
414
|
+
isLast,
|
|
415
|
+
box,
|
|
416
|
+
visible,
|
|
417
|
+
unit,
|
|
418
|
+
isDragging,
|
|
419
|
+
isPicking
|
|
420
|
+
}) {
|
|
421
|
+
const { actions } = sectionCanvasStore.getState();
|
|
422
|
+
const size = BUTTON_SIZE * unit;
|
|
423
|
+
const pillHeight = PILL_HEIGHT * unit;
|
|
424
|
+
const inset = pillHeight / 2 + 4 * unit;
|
|
425
|
+
const clampY = (y) => {
|
|
426
|
+
return Math.max(inset, Math.min(window.innerHeight - inset, y));
|
|
427
|
+
};
|
|
428
|
+
const topY = clampY(box.top);
|
|
429
|
+
const bottomY = clampY(box.bottom);
|
|
430
|
+
const spread = bottomY - topY < pillHeight * 2 ? PILL_SPREAD * unit : 0;
|
|
431
|
+
const centerX = Math.min(
|
|
432
|
+
Math.max(
|
|
433
|
+
(box.left + box.right) / 2,
|
|
434
|
+
visible.left + PILL_HALF_WIDTH * unit + spread
|
|
435
|
+
),
|
|
436
|
+
visible.right - PILL_HALF_WIDTH * unit - spread
|
|
437
|
+
);
|
|
438
|
+
const toolbarTop = Math.max(box.top, 0) + TOOLBAR_INSET * unit;
|
|
439
|
+
const textButton = {
|
|
440
|
+
height: size,
|
|
441
|
+
padding: `0 ${8 * unit}px`,
|
|
442
|
+
fontSize: 12 * unit
|
|
443
|
+
};
|
|
444
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
445
|
+
/* @__PURE__ */ jsx(
|
|
446
|
+
"div",
|
|
447
|
+
{
|
|
448
|
+
style: {
|
|
449
|
+
position: "fixed",
|
|
450
|
+
left: box.left,
|
|
451
|
+
top: box.top,
|
|
452
|
+
width: box.right - box.left,
|
|
453
|
+
height: box.bottom - box.top,
|
|
454
|
+
boxSizing: "border-box",
|
|
455
|
+
border: `${2 * unit}px solid ${ACCENT}`,
|
|
456
|
+
pointerEvents: "none"
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
),
|
|
460
|
+
!isDragging && !isPicking && [
|
|
461
|
+
{ edge: "above", at: index, top: topY, left: centerX - spread },
|
|
462
|
+
{
|
|
463
|
+
edge: "below",
|
|
464
|
+
at: index + 1,
|
|
465
|
+
top: bottomY,
|
|
466
|
+
left: centerX + spread
|
|
467
|
+
}
|
|
468
|
+
].map(({ edge, at, top, left }) => /* @__PURE__ */ jsx(
|
|
469
|
+
"button",
|
|
470
|
+
{
|
|
471
|
+
type: "button",
|
|
472
|
+
"data-shape": "pill",
|
|
473
|
+
"aria-label": `Add Section ${edge} ${label}`,
|
|
474
|
+
style: {
|
|
475
|
+
left,
|
|
476
|
+
top,
|
|
477
|
+
height: pillHeight,
|
|
478
|
+
padding: `0 ${10 * unit}px`,
|
|
479
|
+
fontSize: 11 * unit
|
|
480
|
+
},
|
|
481
|
+
onClick: () => actions.request({ type: "insert", at }),
|
|
482
|
+
children: "Add Section"
|
|
483
|
+
},
|
|
484
|
+
edge
|
|
485
|
+
)),
|
|
486
|
+
/* @__PURE__ */ jsx(
|
|
487
|
+
"div",
|
|
488
|
+
{
|
|
489
|
+
style: {
|
|
490
|
+
position: "fixed",
|
|
491
|
+
top: toolbarTop,
|
|
492
|
+
right: window.innerWidth - Math.min(box.right, visible.right) + TOOLBAR_INSET * unit,
|
|
493
|
+
display: "flex",
|
|
494
|
+
gap: 2 * unit,
|
|
495
|
+
padding: 2 * unit,
|
|
496
|
+
background: ACCENT,
|
|
497
|
+
borderRadius: 6 * unit,
|
|
498
|
+
boxShadow: "0 2px 6px rgba(15, 23, 42, 0.2)",
|
|
499
|
+
pointerEvents: "auto",
|
|
500
|
+
opacity: isDragging ? 0 : 1
|
|
501
|
+
},
|
|
502
|
+
children: isLocked ? /* @__PURE__ */ jsx(
|
|
503
|
+
"button",
|
|
504
|
+
{
|
|
505
|
+
type: "button",
|
|
506
|
+
"data-shape": "tool",
|
|
507
|
+
"aria-label": `${label} is shown conditionally in code; change it in chat`,
|
|
508
|
+
title: "Shown conditionally or repeated in code, so change it in chat",
|
|
509
|
+
"data-locked": "",
|
|
510
|
+
style: textButton,
|
|
511
|
+
children: "Locked"
|
|
512
|
+
}
|
|
513
|
+
) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
514
|
+
/* @__PURE__ */ jsxs(
|
|
515
|
+
"button",
|
|
516
|
+
{
|
|
517
|
+
type: "button",
|
|
518
|
+
"data-shape": "tool",
|
|
519
|
+
"data-grip": "",
|
|
520
|
+
"aria-label": `Move ${label}. Drag, or use the arrow keys`,
|
|
521
|
+
title: "Drag to move",
|
|
522
|
+
style: { ...textButton, touchAction: "none" },
|
|
523
|
+
onPointerDown: (event) => {
|
|
524
|
+
if (event.button !== 0) {
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
event.preventDefault();
|
|
528
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
529
|
+
actions.beginDrag({
|
|
530
|
+
index,
|
|
531
|
+
pointerId: event.pointerId,
|
|
532
|
+
x: event.clientX,
|
|
533
|
+
y: event.clientY
|
|
534
|
+
});
|
|
535
|
+
},
|
|
536
|
+
onPointerMove: (event) => {
|
|
537
|
+
if (sectionCanvasStore.getState().drag?.pointerId === event.pointerId) {
|
|
538
|
+
actions.updateDrag({ x: event.clientX, y: event.clientY });
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
onPointerUp: () => actions.endDrag({ shouldDrop: true }),
|
|
542
|
+
onPointerCancel: () => actions.endDrag({ shouldDrop: false }),
|
|
543
|
+
onKeyDown: (event) => {
|
|
544
|
+
if (event.key === "ArrowUp" && !isFirst) {
|
|
545
|
+
event.preventDefault();
|
|
546
|
+
actions.move({ index, to: index - 1 });
|
|
547
|
+
} else if (event.key === "ArrowDown" && !isLast) {
|
|
548
|
+
event.preventDefault();
|
|
549
|
+
actions.move({ index, to: index + 1 });
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
children: [
|
|
553
|
+
/* @__PURE__ */ jsx(GripIcon, { size: 14 * unit }),
|
|
554
|
+
"Move"
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
),
|
|
558
|
+
/* @__PURE__ */ jsx(
|
|
559
|
+
"button",
|
|
560
|
+
{
|
|
561
|
+
type: "button",
|
|
562
|
+
"data-shape": "tool",
|
|
563
|
+
"aria-label": `Remove ${label}`,
|
|
564
|
+
title: "Remove section",
|
|
565
|
+
style: textButton,
|
|
566
|
+
onClick: () => actions.request({ type: "remove", index }),
|
|
567
|
+
children: "Remove"
|
|
568
|
+
}
|
|
569
|
+
)
|
|
570
|
+
] })
|
|
571
|
+
}
|
|
572
|
+
)
|
|
573
|
+
] });
|
|
574
|
+
}
|
|
575
|
+
const GRIP_DOTS = [
|
|
576
|
+
[5, 3],
|
|
577
|
+
[5, 8],
|
|
578
|
+
[5, 13],
|
|
579
|
+
[11, 3],
|
|
580
|
+
[11, 8],
|
|
581
|
+
[11, 13]
|
|
582
|
+
];
|
|
583
|
+
function GripIcon({ size }) {
|
|
584
|
+
return /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 16 16", "aria-hidden": "true", children: GRIP_DOTS.map(([cx, cy]) => {
|
|
585
|
+
return /* @__PURE__ */ jsx(
|
|
586
|
+
"circle",
|
|
587
|
+
{
|
|
588
|
+
cx,
|
|
589
|
+
cy,
|
|
590
|
+
r: 1.5,
|
|
591
|
+
fill: "currentColor"
|
|
592
|
+
},
|
|
593
|
+
`${cx}-${cy}`
|
|
594
|
+
);
|
|
595
|
+
}) });
|
|
596
|
+
}
|
|
597
|
+
export {
|
|
598
|
+
SectionCanvas
|
|
599
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
type SectionBox = {
|
|
2
|
+
index: number;
|
|
3
|
+
elements: HTMLElement[];
|
|
4
|
+
};
|
|
5
|
+
/** How far each section shifts once `from` moves to `to`, keeping the original gaps between slots. */
|
|
6
|
+
export declare function getMovedSectionOffsets({ sections, from, to, }: {
|
|
7
|
+
sections: readonly {
|
|
8
|
+
index: number;
|
|
9
|
+
top: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
}[];
|
|
12
|
+
from: number;
|
|
13
|
+
to: number;
|
|
14
|
+
}): Map<number, number>;
|
|
15
|
+
export type ViewportBox = {
|
|
16
|
+
top: number;
|
|
17
|
+
bottom: number;
|
|
18
|
+
left: number;
|
|
19
|
+
right: number;
|
|
20
|
+
};
|
|
21
|
+
/** A section's on-screen box: the union of its connected elements, or null when it renders nothing visible. */
|
|
22
|
+
export declare function getSectionViewportBox(elements: readonly HTMLElement[] | undefined): ViewportBox | null;
|
|
23
|
+
/**
|
|
24
|
+
* Shows a dropped section in its new place before the source change reaches the preview, by
|
|
25
|
+
* shifting sections with transforms instead of moving DOM nodes: React keeps positional children,
|
|
26
|
+
* so nodes moved behind its back would get the wrong content when the refresh commits. The shift
|
|
27
|
+
* clears itself the moment the preview's own DOM reflects the move (checked before paint), so the
|
|
28
|
+
* page never shows the old order in between.
|
|
29
|
+
*/
|
|
30
|
+
export declare function startOptimisticSectionMove({ sections, from, to, onLanded, }: {
|
|
31
|
+
sections: readonly SectionBox[];
|
|
32
|
+
from: number;
|
|
33
|
+
to: number;
|
|
34
|
+
onLanded: () => void;
|
|
35
|
+
}): {
|
|
36
|
+
revert: () => void;
|
|
37
|
+
settle: () => void;
|
|
38
|
+
};
|
|
39
|
+
export {};
|