@replohq/sandbox-runtime 1.16.2 → 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.
Files changed (30) hide show
  1. package/_vendor/replo-utils/lib/historyKeys.d.ts +2 -0
  2. package/_vendor/replo-utils/lib/historyKeys.mjs +18 -0
  3. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +43 -0
  4. package/_vendor/schemas/selectModeIframeRpc.d.ts +146 -0
  5. package/_vendor/schemas/selectModeIframeRpc.mjs +72 -2
  6. package/buildMetadata.js +3 -3
  7. package/package.json +4 -4
  8. package/select-mode/EditorSelection.js +5 -1
  9. package/select-mode/fast-refresh-relay.js +82 -3
  10. package/select-mode/fiber.d.ts +6 -0
  11. package/select-mode/fiber.js +33 -0
  12. package/select-mode/hooks/useParentMessaging.js +10 -3
  13. package/select-mode/hooks/useSelectionHover.js +2 -1
  14. package/select-mode/hooks/useSelectionInteractions.js +10 -0
  15. package/select-mode/iframeRpcChannel.d.ts +45 -1
  16. package/select-mode/iframeRpcChannel.js +7 -0
  17. package/select-mode/section-canvas/SectionCanvas.d.ts +7 -0
  18. package/select-mode/section-canvas/SectionCanvas.js +599 -0
  19. package/select-mode/section-canvas/optimisticSectionMove.d.ts +39 -0
  20. package/select-mode/section-canvas/optimisticSectionMove.js +172 -0
  21. package/select-mode/section-canvas/sectionCanvasControls.d.ts +3 -0
  22. package/select-mode/section-canvas/sectionCanvasControls.js +8 -0
  23. package/select-mode/section-canvas/sectionCanvasStore.d.ts +71 -0
  24. package/select-mode/section-canvas/sectionCanvasStore.js +275 -0
  25. package/select-mode/section-canvas/sectionElements.d.ts +16 -0
  26. package/select-mode/section-canvas/sectionElements.js +94 -0
  27. package/select-mode/section-canvas/sectionGhost.d.ts +5 -0
  28. package/select-mode/section-canvas/sectionGhost.js +23 -0
  29. package/select-mode/symbolicate.d.ts +2 -0
  30. package/select-mode/symbolicate.js +18 -1
@@ -2,11 +2,54 @@ import type { SelectedElement } from "../_vendor/schemas/generated/selectMode";
2
2
  import type { IframeRpcChannelInputHandlers, IframeRpcChannelRemote } from "../_vendor/schemas/iframeRpcChannel";
3
3
  import { previewIframeToWebsiteBuilderMethodConfig, websiteBuilderToPreviewIframeMethodConfig } from "../_vendor/schemas/selectModeIframeRpc";
4
4
  export type WebsiteBuilderRpcRemote = IframeRpcChannelRemote<typeof previewIframeToWebsiteBuilderMethodConfig>;
5
- type SelectModeParentMessagingHandlers = Omit<IframeRpcChannelInputHandlers<typeof websiteBuilderToPreviewIframeMethodConfig>, "symbolicateSelectedElement" | "navigateToRoute">;
5
+ type SelectModeParentMessagingHandlers = Omit<IframeRpcChannelInputHandlers<typeof websiteBuilderToPreviewIframeMethodConfig>, "symbolicateSelectedElement" | "navigateToRoute" | "setSectionCanvas" | "settleSectionMove">;
6
6
  export declare function setNavigateToRouteHandler(handler: ((input: {
7
7
  pathname: string;
8
8
  }) => void) | null): void;
9
9
  export declare function getSelectModeIframeRpcChannel(): import("../_vendor/schemas/iframeRpcChannel").IframeRpcChannel<{
10
+ sectionCanvasRequest: {
11
+ inputSchema: import("zod").ZodObject<{
12
+ request: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
13
+ type: import("zod").ZodLiteral<"insert">;
14
+ at: import("zod").ZodNumber;
15
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
16
+ type: import("zod").ZodLiteral<"move">;
17
+ moveId: import("zod").ZodString;
18
+ index: import("zod").ZodNumber;
19
+ to: import("zod").ZodNumber;
20
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
21
+ type: import("zod").ZodLiteral<"remove">;
22
+ index: import("zod").ZodNumber;
23
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
24
+ type: import("zod").ZodLiteral<"undo">;
25
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
26
+ type: import("zod").ZodLiteral<"redo">;
27
+ }, import("zod/v4/core").$strip>, import("zod").ZodObject<{
28
+ type: import("zod").ZodLiteral<"cancelInsert">;
29
+ }, import("zod/v4/core").$strip>], "type">;
30
+ }, import("zod/v4/core").$strip>;
31
+ outputSchema: import("zod").ZodVoid;
32
+ };
33
+ sectionCanvasLayoutChanged: {
34
+ inputSchema: import("zod").ZodObject<{
35
+ sections: import("zod").ZodArray<import("zod").ZodObject<{
36
+ index: import("zod").ZodNumber;
37
+ startLine: import("zod").ZodNumber;
38
+ endLine: import("zod").ZodNumber;
39
+ rect: import("zod").ZodObject<{
40
+ x: import("zod").ZodNumber;
41
+ y: import("zod").ZodNumber;
42
+ left: import("zod").ZodNumber;
43
+ top: import("zod").ZodNumber;
44
+ right: import("zod").ZodNumber;
45
+ bottom: import("zod").ZodNumber;
46
+ width: import("zod").ZodNumber;
47
+ height: import("zod").ZodNumber;
48
+ }, import("zod/v4/core").$strip>;
49
+ }, import("zod/v4/core").$strip>>;
50
+ }, import("zod/v4/core").$strip>;
51
+ outputSchema: import("zod").ZodVoid;
52
+ };
10
53
  requestInlineTextEdit: {
11
54
  inputSchema: import("zod").ZodObject<{
12
55
  editId: import("zod").ZodString;
@@ -105,6 +148,7 @@ export declare function getSelectModeIframeRpcChannel(): import("../_vendor/sche
105
148
  branch: import("zod").ZodNullable<import("zod").ZodString>;
106
149
  commit: import("zod").ZodNullable<import("zod").ZodString>;
107
150
  builtAt: import("zod").ZodNullable<import("zod").ZodString>;
151
+ capabilities: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString>>;
108
152
  }, import("zod/v4/core").$strip>;
109
153
  outputSchema: import("zod").ZodVoid;
110
154
  };
@@ -8,6 +8,7 @@ import {
8
8
  SELECT_MODE_IFRAME_RPC_CHANNEL_VERSION,
9
9
  websiteBuilderToPreviewIframeMethodConfig
10
10
  } from "../_vendor/schemas/selectModeIframeRpc.mjs";
11
+ import { sectionCanvasStore } from "./section-canvas/sectionCanvasStore";
11
12
  import { symbolicateSelectedElement } from "./symbolicate";
12
13
  let messagingHandlers = null;
13
14
  let navigateToRouteHandler = null;
@@ -54,6 +55,12 @@ function createSelectModeIframeRpcChannel() {
54
55
  formatCommand: (input) => {
55
56
  messagingHandlers?.formatCommand(input);
56
57
  },
58
+ setSectionCanvas: ({ canvas }) => {
59
+ sectionCanvasStore.getState().actions.setCanvas(canvas);
60
+ },
61
+ settleSectionMove: (input) => {
62
+ sectionCanvasStore.getState().actions.settleMove(input);
63
+ },
57
64
  symbolicateSelectedElement: (element) => {
58
65
  return symbolicateSelectedElement(element);
59
66
  },
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Section controls drawn on the preview, after Squarespace's editor: the hovered or selected section
3
+ * gets a bold outline with an "Add Section" pill on each edge, a grip that drags it to a new place
4
+ * (with a picture of it and a drop line), and a trash button. Every edit is a request to the
5
+ * Website Builder, which owns the source.
6
+ */
7
+ export declare function SectionCanvas(): import("react").ReactPortal | null;
@@ -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
+ };