@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.
Files changed (38) 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/replo-utils/lib/viewportSnapshot.d.ts +1 -1
  4. package/_vendor/replo-utils/lib/viewportSnapshot.mjs +1 -1
  5. package/_vendor/replo-utils/react/use-viewport-snapshot-responder.mjs +28 -5222
  6. package/_vendor/schemas/errors.d.ts +3 -43
  7. package/_vendor/schemas/generated/selectModeIframeRpc.d.ts +43 -0
  8. package/_vendor/schemas/iframeRpcChannel.mjs +2 -0
  9. package/_vendor/schemas/reploError.d.ts +43 -0
  10. package/_vendor/schemas/selectModeIframeRpc.d.ts +146 -7
  11. package/_vendor/schemas/selectModeIframeRpc.mjs +73 -10
  12. package/buildMetadata.js +3 -3
  13. package/{chunk-3YA3QXUQ.mjs → chunk-B3PLD7SH.mjs} +29 -18
  14. package/package.json +4 -4
  15. package/rrweb-snapshot-GTQBXO2Y.mjs +5761 -0
  16. package/select-mode/EditorSelection.js +5 -1
  17. package/select-mode/fast-refresh-relay.js +82 -3
  18. package/select-mode/fiber.d.ts +6 -0
  19. package/select-mode/fiber.js +33 -0
  20. package/select-mode/hooks/useParentMessaging.js +10 -3
  21. package/select-mode/hooks/useSelectionHover.js +2 -1
  22. package/select-mode/hooks/useSelectionInteractions.js +10 -0
  23. package/select-mode/iframeRpcChannel.d.ts +45 -7
  24. package/select-mode/iframeRpcChannel.js +9 -8
  25. package/select-mode/section-canvas/SectionCanvas.d.ts +7 -0
  26. package/select-mode/section-canvas/SectionCanvas.js +599 -0
  27. package/select-mode/section-canvas/optimisticSectionMove.d.ts +39 -0
  28. package/select-mode/section-canvas/optimisticSectionMove.js +172 -0
  29. package/select-mode/section-canvas/sectionCanvasControls.d.ts +3 -0
  30. package/select-mode/section-canvas/sectionCanvasControls.js +8 -0
  31. package/select-mode/section-canvas/sectionCanvasStore.d.ts +71 -0
  32. package/select-mode/section-canvas/sectionCanvasStore.js +275 -0
  33. package/select-mode/section-canvas/sectionElements.d.ts +16 -0
  34. package/select-mode/section-canvas/sectionElements.js +94 -0
  35. package/select-mode/section-canvas/sectionGhost.d.ts +5 -0
  36. package/select-mode/section-canvas/sectionGhost.js +23 -0
  37. package/select-mode/symbolicate.d.ts +2 -0
  38. 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 {};