@upstart.gg/vite-plugins 0.1.84 → 0.1.85

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/dist/upstart-editor-api.d.ts +58 -1
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +224 -1
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts +1 -1
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +65 -2
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +2 -24
  10. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +7 -1
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  13. package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js +33 -0
  14. package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js.map +1 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  16. package/dist/vite-plugin-upstart-editor/runtime/index.js +23 -0
  17. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  18. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +5 -0
  19. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/labels.js +5 -0
  21. package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/move-controls.js +434 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/move-controls.js.map +1 -0
  24. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +6 -3
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  26. package/package.json +3 -3
  27. package/src/tests/move-controls.test.ts +86 -0
  28. package/src/tests/upstart-editor-api-move-element.test.ts +220 -0
  29. package/src/tests/vite-plugin-upstart-attrs.test.ts +165 -4
  30. package/src/upstart-editor-api.ts +263 -0
  31. package/src/vite-plugin-upstart-attrs.ts +93 -2
  32. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +2 -40
  33. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +17 -3
  34. package/src/vite-plugin-upstart-editor/runtime/controls-ui.ts +48 -0
  35. package/src/vite-plugin-upstart-editor/runtime/index.ts +31 -0
  36. package/src/vite-plugin-upstart-editor/runtime/labels.ts +10 -0
  37. package/src/vite-plugin-upstart-editor/runtime/move-controls.ts +536 -0
  38. package/src/vite-plugin-upstart-editor/runtime/types.ts +7 -2
@@ -0,0 +1,434 @@
1
+ import { getCurrentMode } from "./state.js";
2
+ import { sendToParent } from "./utils.js";
3
+ import { hideOverlays } from "./hover-overlay.js";
4
+ import { getLabels } from "./labels.js";
5
+ import { baseButton, createControlsLayer, cssAttr, setIcon } from "./controls-ui.js";
6
+ //#region src/vite-plugin-upstart-editor/runtime/move-controls.ts
7
+ const MOVE_ID_ATTR = "data-upstart-move-id";
8
+ const MOVE_SCOPE_ATTR = "data-upstart-move-scope";
9
+ const PREVIEW_ATTR = "data-upstart-move-preview";
10
+ const MAX_HANDLES = 3;
11
+ const HANDLE_SIZE_FINE = 16;
12
+ const HANDLE_SIZE_COARSE = 28;
13
+ function isCoarsePointer() {
14
+ return typeof matchMedia === "function" && matchMedia("(pointer: coarse)").matches;
15
+ }
16
+ function handleSize() {
17
+ return isCoarsePointer() ? HANDLE_SIZE_COARSE : HANDLE_SIZE_FINE;
18
+ }
19
+ const SNAP_DISTANCE = 48;
20
+ const AUTO_SCROLL_EDGE = 40;
21
+ const AUTO_SCROLL_STEP = 12;
22
+ let layer = null;
23
+ let isInitialized = false;
24
+ let rafId = null;
25
+ let hoveredItem = null;
26
+ let lastPointerType = "mouse";
27
+ let indicator = null;
28
+ const buttonOwner = /* @__PURE__ */ new WeakMap();
29
+ let drag = null;
30
+ const staged = [];
31
+ let bar = null;
32
+ let applying = false;
33
+ function initMoveControls() {
34
+ if (typeof document === "undefined" || isInitialized) return;
35
+ isInitialized = true;
36
+ window.addEventListener("scroll", scheduleRefresh, {
37
+ passive: true,
38
+ capture: true
39
+ });
40
+ window.addEventListener("resize", scheduleRefresh, { passive: true });
41
+ document.addEventListener("mouseover", onHoverMove, { passive: true });
42
+ document.addEventListener("pointerdown", onTapSelect, {
43
+ capture: true,
44
+ passive: true
45
+ });
46
+ document.addEventListener("keydown", onUndoKey);
47
+ installStyles();
48
+ }
49
+ function installStyles() {
50
+ if (document.getElementById("upstart-move-styles")) return;
51
+ const style = document.createElement("style");
52
+ style.id = "upstart-move-styles";
53
+ style.textContent = `
54
+ [${PREVIEW_ATTR}] { outline: 2px dashed #7270c6 !important; outline-offset: 2px; }
55
+ #upstart-move-bar {
56
+ position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2147483647;
57
+ display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 999px;
58
+ background: #1f1d3a; color: #fff; box-shadow: 0 4px 16px rgba(0,0,0,0.3);
59
+ font: 500 13px/1.2 system-ui, -apple-system, sans-serif; white-space: nowrap; pointer-events: auto;
60
+ box-sizing: border-box;
61
+ }
62
+ #upstart-move-bar button {
63
+ padding: 6px 12px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.35);
64
+ background: transparent; color: #fff; font: inherit; cursor: pointer; box-sizing: border-box;
65
+ }
66
+ #upstart-move-bar button[data-primary] { background: #16a34a; border-color: #16a34a; }
67
+ #upstart-move-bar button:disabled { opacity: 0.6; cursor: wait; }
68
+ @media (max-width: 560px) {
69
+ #upstart-move-bar {
70
+ flex-direction: column; align-items: stretch; width: calc(100% - 32px); max-width: 360px;
71
+ padding: 12px; gap: 8px; border-radius: 16px; white-space: normal; text-align: center;
72
+ }
73
+ #upstart-move-bar button { width: 100%; padding: 10px 12px; }
74
+ }
75
+ `;
76
+ document.head.appendChild(style);
77
+ }
78
+ function refreshMoveControls() {
79
+ if (typeof document === "undefined") return;
80
+ if (getCurrentMode() !== "move") {
81
+ hideMoveControls();
82
+ return;
83
+ }
84
+ if (drag) return;
85
+ renderBar();
86
+ const root = ensureLayer();
87
+ root.replaceChildren();
88
+ clearPreview();
89
+ if (!hoveredItem || !document.contains(hoveredItem)) return;
90
+ if (lastPointerType !== "mouse") hoveredItem.setAttribute(PREVIEW_ATTR, "");
91
+ const size = handleSize();
92
+ const placed = [];
93
+ for (const el of handleChain(hoveredItem).reverse()) {
94
+ const rect = el.getBoundingClientRect();
95
+ const top = rect.top + window.scrollY - size / 2;
96
+ let left = rect.left + window.scrollX - size / 2;
97
+ while (placed.some((p) => Math.abs(p.top - top) < size + 2 && Math.abs(p.left - left) < size + 2)) left += size + 4;
98
+ placed.push({
99
+ top,
100
+ left
101
+ });
102
+ const btn = makeHandle(el, top, left, size);
103
+ buttonOwner.set(btn, hoveredItem);
104
+ root.appendChild(btn);
105
+ }
106
+ }
107
+ /**
108
+ * The elements offered for moving when `item` is hovered: itself and its closest movable
109
+ * ancestors, so a whole block can be grabbed from any of its children. Elements alone in
110
+ * their component (nowhere to go) are skipped.
111
+ */
112
+ function handleChain(item) {
113
+ const chain = [];
114
+ for (let el = item; el && chain.length < MAX_HANDLES; el = el.parentElement?.closest(`[${MOVE_ID_ATTR}]`) ?? null) if (candidatesFor(el).length > 0) chain.push(el);
115
+ return chain;
116
+ }
117
+ function clearPreview() {
118
+ for (const el of document.querySelectorAll(`[${PREVIEW_ATTR}]`)) el.removeAttribute(PREVIEW_ATTR);
119
+ }
120
+ function hideMoveControls() {
121
+ if (drag) endDrag(false);
122
+ hoveredItem = null;
123
+ if (layer) layer.replaceChildren();
124
+ clearPreview();
125
+ bar?.remove();
126
+ bar = null;
127
+ }
128
+ function ensureLayer() {
129
+ if (!layer || !document.contains(layer)) layer = createControlsLayer("upstart-move-controls");
130
+ return layer;
131
+ }
132
+ function onTapSelect(e) {
133
+ lastPointerType = e.pointerType;
134
+ if (e.pointerType === "mouse" || getCurrentMode() !== "move" || drag) return;
135
+ const target = e.target;
136
+ if (!target || layer?.contains(target) || bar?.contains(target)) return;
137
+ hoveredItem = target.closest(`[${MOVE_ID_ATTR}]`);
138
+ scheduleRefresh();
139
+ }
140
+ function onHoverMove(e) {
141
+ if (getCurrentMode() !== "move" || drag || lastPointerType !== "mouse") return;
142
+ const owner = hoverOwner(e.target);
143
+ if (owner !== hoveredItem) {
144
+ hoveredItem = owner;
145
+ scheduleRefresh();
146
+ }
147
+ }
148
+ function hoverOwner(target) {
149
+ if (!target) return null;
150
+ if (layer?.contains(target)) {
151
+ const owner = buttonOwner.get(target);
152
+ return owner && document.contains(owner) ? owner : null;
153
+ }
154
+ return target.closest(`[${MOVE_ID_ATTR}]`);
155
+ }
156
+ /**
157
+ * True when `el` is at the same level as `source`: a sibling (same container), or a cousin
158
+ * (a child of a container next to the source's own — the other column, the next card).
159
+ */
160
+ function isSameLevel(source, el) {
161
+ if (el === source) return false;
162
+ const parent = source.parentElement;
163
+ const elParent = el.parentElement;
164
+ if (!parent || !elParent) return false;
165
+ if (elParent === parent) return true;
166
+ return elParent !== source && elParent.parentElement !== null && elParent.parentElement === parent.parentElement;
167
+ }
168
+ /**
169
+ * Elements `source` can be dropped next to: movable elements of the same component that are
170
+ * at the same level (siblings and cousins, see `isSameLevel`).
171
+ */
172
+ function candidatesFor(source) {
173
+ const scope = source.getAttribute(MOVE_SCOPE_ATTR);
174
+ if (!scope) return [];
175
+ return [...document.querySelectorAll(`[${MOVE_SCOPE_ATTR}="${cssAttr(scope)}"]`)].filter((el) => isSameLevel(source, el));
176
+ }
177
+ function axisOf(el) {
178
+ if (getComputedStyle(el).display.startsWith("inline")) return "horizontal";
179
+ const parent = el.parentElement;
180
+ if (!parent) return "vertical";
181
+ const style = getComputedStyle(parent);
182
+ if (style.display.includes("grid")) return "horizontal";
183
+ if (style.display.includes("flex") && !style.flexDirection.startsWith("column")) return "horizontal";
184
+ return "vertical";
185
+ }
186
+ /**
187
+ * The item a drop at (x, y) lands next to: the one under the pointer (the smallest, should
188
+ * several overlap), or else the nearest one within SNAP_DISTANCE.
189
+ */
190
+ function pickNearest(items, x, y) {
191
+ let best = null;
192
+ for (const { item, rect } of items) {
193
+ if (rect.width === 0 && rect.height === 0) continue;
194
+ const dx = Math.max(rect.left - x, 0, x - rect.right);
195
+ const dy = Math.max(rect.top - y, 0, y - rect.bottom);
196
+ const distance = Math.hypot(dx, dy);
197
+ if (distance > SNAP_DISTANCE) continue;
198
+ const score = distance === 0 ? -1 / Math.max(rect.width * rect.height, 1) : distance;
199
+ if (!best || score < best.score) best = {
200
+ item,
201
+ rect,
202
+ score
203
+ };
204
+ }
205
+ return best && {
206
+ item: best.item,
207
+ rect: best.rect
208
+ };
209
+ }
210
+ /** Before or after an element, from the pointer vs the element's middle along the axis. */
211
+ function dropPosition(rect, axis, x, y) {
212
+ return (axis === "horizontal" ? x > rect.left + rect.width / 2 : y > rect.top + rect.height / 2) ? "after" : "before";
213
+ }
214
+ function findDropTarget(source, x, y) {
215
+ const best = pickNearest(candidatesFor(source).map((item) => ({
216
+ item,
217
+ rect: item.getBoundingClientRect()
218
+ })), x, y);
219
+ if (!best) return null;
220
+ const axis = axisOf(best.item);
221
+ return {
222
+ element: best.item,
223
+ position: dropPosition(best.rect, axis, x, y),
224
+ axis
225
+ };
226
+ }
227
+ function makeHandle(item, top, left, size) {
228
+ const btn = baseButton(top, left, size, "#7270c6");
229
+ setIcon(btn, "grip", Math.round(size * .7));
230
+ btn.title = `${getLabels().dragToMove} (<${item.tagName.toLowerCase()}>)`;
231
+ btn.style.cursor = "grab";
232
+ btn.style.touchAction = "none";
233
+ btn.addEventListener("pointerenter", () => {
234
+ clearPreview();
235
+ item.setAttribute(PREVIEW_ATTR, "");
236
+ });
237
+ btn.addEventListener("pointerleave", () => item.removeAttribute(PREVIEW_ATTR));
238
+ btn.addEventListener("pointerdown", (e) => {
239
+ e.preventDefault();
240
+ e.stopPropagation();
241
+ btn.setPointerCapture?.(e.pointerId);
242
+ startDrag(item, btn);
243
+ });
244
+ btn.addEventListener("click", (e) => {
245
+ e.preventDefault();
246
+ e.stopPropagation();
247
+ });
248
+ return btn;
249
+ }
250
+ function startDrag(source, handle) {
251
+ drag = {
252
+ source,
253
+ target: null
254
+ };
255
+ hideOverlays();
256
+ clearPreview();
257
+ for (const child of [...layer?.children ?? []]) if (child !== handle) child.remove();
258
+ source.style.opacity = "0.5";
259
+ document.body.style.userSelect = "none";
260
+ document.documentElement.style.cursor = "grabbing";
261
+ document.addEventListener("pointermove", onDragMove);
262
+ document.addEventListener("pointerup", onDragDrop, { once: true });
263
+ document.addEventListener("pointercancel", onDragCancel, { once: true });
264
+ document.addEventListener("keydown", onDragKey, true);
265
+ }
266
+ function onDragCancel() {
267
+ endDrag(false);
268
+ }
269
+ function onDragMove(e) {
270
+ if (!drag) return;
271
+ if (e.clientY < AUTO_SCROLL_EDGE) window.scrollBy(0, -AUTO_SCROLL_STEP);
272
+ else if (e.clientY > window.innerHeight - AUTO_SCROLL_EDGE) window.scrollBy(0, AUTO_SCROLL_STEP);
273
+ drag.target = findDropTarget(drag.source, e.clientX, e.clientY);
274
+ drawIndicator(drag.target);
275
+ }
276
+ function onDragDrop() {
277
+ endDrag(true);
278
+ }
279
+ function onDragKey(e) {
280
+ if (e.key !== "Escape") return;
281
+ e.preventDefault();
282
+ e.stopPropagation();
283
+ endDrag(false);
284
+ }
285
+ function endDrag(commit) {
286
+ document.removeEventListener("pointermove", onDragMove);
287
+ document.removeEventListener("pointerup", onDragDrop);
288
+ document.removeEventListener("pointercancel", onDragCancel);
289
+ document.removeEventListener("keydown", onDragKey, true);
290
+ document.body.style.removeProperty("user-select");
291
+ document.documentElement.style.removeProperty("cursor");
292
+ const current = drag;
293
+ drag = null;
294
+ if (!current) return;
295
+ current.source.style.removeProperty("opacity");
296
+ layer?.replaceChildren();
297
+ indicator = null;
298
+ if (commit && current.target) stageMove(current.source, current.target);
299
+ scheduleRefresh();
300
+ }
301
+ function isNoOp(source, target) {
302
+ const el = target.element;
303
+ return target.position === "after" ? el.nextElementSibling === source : el.previousElementSibling === source;
304
+ }
305
+ function stageMove(source, target) {
306
+ const id = source.getAttribute(MOVE_ID_ATTR);
307
+ const targetId = target.element.getAttribute(MOVE_ID_ATTR);
308
+ const fromParent = source.parentNode;
309
+ if (!id || !targetId || !fromParent || isNoOp(source, target)) return;
310
+ staged.push({
311
+ node: source,
312
+ fromParent,
313
+ fromNext: source.nextSibling,
314
+ id,
315
+ targetId,
316
+ position: target.position
317
+ });
318
+ if (target.position === "after") target.element.after(source);
319
+ else target.element.before(source);
320
+ }
321
+ /** Undo the last staged move (the DOM goes back to where it was before that drop). */
322
+ function undoLastMove() {
323
+ const move = staged.pop();
324
+ if (!move) return;
325
+ const { node, fromParent, fromNext } = move;
326
+ fromParent.insertBefore(node, fromNext && fromNext.parentNode === fromParent ? fromNext : null);
327
+ scheduleRefresh();
328
+ }
329
+ function discardMoves() {
330
+ while (staged.length > 0) undoLastMove();
331
+ }
332
+ /** The staged moves, as sent to the editor on apply. */
333
+ function getStagedMoves() {
334
+ return staged.map(({ id, targetId, position }) => ({
335
+ id,
336
+ targetId,
337
+ position
338
+ }));
339
+ }
340
+ function applyMoves() {
341
+ if (staged.length === 0 || applying) return;
342
+ applying = true;
343
+ sendToParent({
344
+ type: "text-edit",
345
+ payload: {
346
+ action: "moveElements",
347
+ moves: getStagedMoves()
348
+ }
349
+ });
350
+ renderBar();
351
+ }
352
+ /** The editor could not apply the moves: keep them staged and let the user retry or undo. */
353
+ function onMovesApplyFailed() {
354
+ applying = false;
355
+ renderBar();
356
+ }
357
+ function onUndoKey(e) {
358
+ if (staged.length === 0 || applying || getCurrentMode() !== "move") return;
359
+ if (e.key.toLowerCase() !== "z" || !(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return;
360
+ const active = document.activeElement;
361
+ if (active?.isContentEditable || active?.closest("input, textarea, select")) return;
362
+ e.preventDefault();
363
+ undoLastMove();
364
+ }
365
+ function renderBar() {
366
+ if (staged.some((m) => !document.contains(m.node))) {
367
+ staged.length = 0;
368
+ applying = false;
369
+ }
370
+ if (staged.length === 0) {
371
+ bar?.remove();
372
+ bar = null;
373
+ return;
374
+ }
375
+ if (!bar || !document.contains(bar)) {
376
+ bar = document.createElement("div");
377
+ bar.id = "upstart-move-bar";
378
+ document.body.appendChild(bar);
379
+ }
380
+ const labels = getLabels();
381
+ const text = document.createElement("span");
382
+ text.textContent = `${labels.pendingMoves} ${staged.length}`;
383
+ bar.replaceChildren(text, barButton(labels.undoMove, false, undoLastMove), barButton(labels.discardMoves, false, discardMoves), barButton(labels.applyMoves, true, applyMoves));
384
+ }
385
+ function barButton(label, primary, action) {
386
+ const btn = document.createElement("button");
387
+ btn.type = "button";
388
+ btn.textContent = label;
389
+ btn.disabled = applying;
390
+ if (primary) btn.setAttribute("data-primary", "");
391
+ btn.addEventListener("click", (e) => {
392
+ e.preventDefault();
393
+ e.stopPropagation();
394
+ action();
395
+ });
396
+ return btn;
397
+ }
398
+ function drawIndicator(target) {
399
+ indicator?.remove();
400
+ indicator = null;
401
+ if (!target) return;
402
+ const root = ensureLayer();
403
+ const r = target.element.getBoundingClientRect();
404
+ const line = document.createElement("div");
405
+ const thickness = 3;
406
+ const x = window.scrollX;
407
+ const y = window.scrollY;
408
+ const box = target.axis === "vertical" ? {
409
+ top: (target.position === "before" ? r.top - 4 : r.bottom + 4) + y - thickness / 2,
410
+ left: r.left + x,
411
+ width: r.width,
412
+ height: thickness
413
+ } : {
414
+ top: r.top + y,
415
+ left: (target.position === "before" ? r.left - 4 : r.right + 4) + x - thickness / 2,
416
+ width: thickness,
417
+ height: r.height
418
+ };
419
+ line.setAttribute("data-upstart-drop-indicator", target.position);
420
+ line.style.cssText = `position: absolute; top: ${box.top}px; left: ${box.left}px; width: ${box.width}px; height: ${box.height}px; background: #7270c6; border-radius: 2px; pointer-events: none;`;
421
+ root.appendChild(line);
422
+ indicator = line;
423
+ }
424
+ function scheduleRefresh() {
425
+ if (rafId !== null) return;
426
+ rafId = requestAnimationFrame(() => {
427
+ rafId = null;
428
+ refreshMoveControls();
429
+ });
430
+ }
431
+ //#endregion
432
+ export { hideMoveControls, initMoveControls, onMovesApplyFailed, refreshMoveControls };
433
+
434
+ //# sourceMappingURL=move-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"move-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/move-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\nimport { getLabels } from \"./labels.js\";\nimport { hideOverlays } from \"./hover-overlay.js\";\nimport { baseButton, createControlsLayer, cssAttr, setIcon } from \"./controls-ui.js\";\n\n// Editor-only drag & drop of simple elements (texts, images). Movable elements carry\n// `data-upstart-move-id` (their source range in the registry) and `data-upstart-move-scope`\n// (the component they belong to), both injected by vite-plugin-upstart-attrs. A grip shows on\n// the hovered element; dragging it shows an insertion bar before/after another element of the\n// same component at the same level (sibling or cousin). The drop moves the element in the preview DOM only and stages the move: a bar\n// lets the user undo the last move, discard them all, or apply them as a single\n// `moveElements` source edit (then the editor rebuilds and reloads the preview). React keeps\n// working on the moved nodes thanks to the DOM guard.\n\nconst MOVE_ID_ATTR = \"data-upstart-move-id\";\nconst MOVE_SCOPE_ATTR = \"data-upstart-move-scope\";\n// Set on the element a hovered handle would move, to outline it.\nconst PREVIEW_ATTR = \"data-upstart-move-preview\";\n// Handles shown at once: the hovered element and its closest movable ancestors (whole blocks).\nconst MAX_HANDLES = 3;\n// Handles are larger on touch screens, so a finger can grab them.\nconst HANDLE_SIZE_FINE = 16;\nconst HANDLE_SIZE_COARSE = 28;\n\nfunction isCoarsePointer(): boolean {\n return typeof matchMedia === \"function\" && matchMedia(\"(pointer: coarse)\").matches;\n}\n\nfunction handleSize(): number {\n return isCoarsePointer() ? HANDLE_SIZE_COARSE : HANDLE_SIZE_FINE;\n}\n\n// How far (px) from a target the pointer may be and still drop next to it.\nconst SNAP_DISTANCE = 48;\n// Distance (px) from the viewport edge at which the page scrolls while dragging.\nconst AUTO_SCROLL_EDGE = 40;\nconst AUTO_SCROLL_STEP = 12;\n\nexport type DropPosition = \"before\" | \"after\";\n\nexport interface DropTarget {\n element: HTMLElement;\n position: DropPosition;\n /** Insertion axis: \"vertical\" for stacked blocks, \"horizontal\" for side-by-side elements. */\n axis: \"vertical\" | \"horizontal\";\n}\n\nlet layer: HTMLDivElement | null = null;\nlet isInitialized = false;\nlet rafId: number | null = null;\nlet hoveredItem: HTMLElement | null = null;\n// Touch screens have no hover: a tap selects the element instead, and it stays outlined.\nlet lastPointerType = \"mouse\";\nlet indicator: HTMLDivElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nlet drag: { source: HTMLElement; target: DropTarget | null } | null = null;\n\n// Staged moves, oldest first. Each one remembers where the node was, so undoing them in\n// reverse order restores the original DOM exactly.\ninterface StagedMove {\n node: HTMLElement;\n fromParent: Node;\n fromNext: Node | null;\n id: string;\n targetId: string;\n position: DropPosition;\n}\nconst staged: StagedMove[] = [];\nlet bar: HTMLDivElement | null = null;\nlet applying = false;\n\nexport function initMoveControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n document.addEventListener(\"pointerdown\", onTapSelect, { capture: true, passive: true });\n document.addEventListener(\"keydown\", onUndoKey);\n installStyles();\n}\n\n// Stylesheet (not inline styles) so the pending-moves bar can switch to a column layout on\n// narrow viewports.\nfunction installStyles(): void {\n if (document.getElementById(\"upstart-move-styles\")) return;\n const style = document.createElement(\"style\");\n style.id = \"upstart-move-styles\";\n style.textContent = `\n[${PREVIEW_ATTR}] { outline: 2px dashed #7270c6 !important; outline-offset: 2px; }\n#upstart-move-bar {\n position: fixed; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2147483647;\n display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 14px; border-radius: 999px;\n background: #1f1d3a; color: #fff; box-shadow: 0 4px 16px rgba(0,0,0,0.3);\n font: 500 13px/1.2 system-ui, -apple-system, sans-serif; white-space: nowrap; pointer-events: auto;\n box-sizing: border-box;\n}\n#upstart-move-bar button {\n padding: 6px 12px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.35);\n background: transparent; color: #fff; font: inherit; cursor: pointer; box-sizing: border-box;\n}\n#upstart-move-bar button[data-primary] { background: #16a34a; border-color: #16a34a; }\n#upstart-move-bar button:disabled { opacity: 0.6; cursor: wait; }\n@media (max-width: 560px) {\n #upstart-move-bar {\n flex-direction: column; align-items: stretch; width: calc(100% - 32px); max-width: 360px;\n padding: 12px; gap: 8px; border-radius: 16px; white-space: normal; text-align: center;\n }\n #upstart-move-bar button { width: 100%; padding: 10px 12px; }\n}\n`;\n document.head.appendChild(style);\n}\n\nexport function refreshMoveControls(): void {\n if (typeof document === \"undefined\") return;\n if (getCurrentMode() !== \"move\") {\n hideMoveControls();\n return;\n }\n // The layer holds the insertion bar while dragging: leave it to the drag handlers.\n if (drag) return;\n renderBar();\n\n const root = ensureLayer();\n root.replaceChildren();\n clearPreview();\n if (!hoveredItem || !document.contains(hoveredItem)) return;\n if (lastPointerType !== \"mouse\") hoveredItem.setAttribute(PREVIEW_ATTR, \"\");\n\n // One handle per element of the chain, at its top-left corner, nudged to the right when\n // it would cover the handle of an element sharing the same corner. Placed from the largest\n // element to the smallest, so handles read outer block → inner element, left to right.\n const size = handleSize();\n const placed: { top: number; left: number }[] = [];\n for (const el of handleChain(hoveredItem).reverse()) {\n const rect = el.getBoundingClientRect();\n const top = rect.top + window.scrollY - size / 2;\n let left = rect.left + window.scrollX - size / 2;\n while (placed.some((p) => Math.abs(p.top - top) < size + 2 && Math.abs(p.left - left) < size + 2)) {\n left += size + 4;\n }\n placed.push({ top, left });\n const btn = makeHandle(el, top, left, size);\n buttonOwner.set(btn, hoveredItem);\n root.appendChild(btn);\n }\n}\n\n/**\n * The elements offered for moving when `item` is hovered: itself and its closest movable\n * ancestors, so a whole block can be grabbed from any of its children. Elements alone in\n * their component (nowhere to go) are skipped.\n */\nfunction handleChain(item: HTMLElement): HTMLElement[] {\n const chain: HTMLElement[] = [];\n for (\n let el: HTMLElement | null = item;\n el && chain.length < MAX_HANDLES;\n el = el.parentElement?.closest<HTMLElement>(`[${MOVE_ID_ATTR}]`) ?? null\n ) {\n if (candidatesFor(el).length > 0) chain.push(el);\n }\n return chain;\n}\n\nfunction clearPreview(): void {\n for (const el of document.querySelectorAll(`[${PREVIEW_ATTR}]`)) el.removeAttribute(PREVIEW_ATTR);\n}\n\nexport function hideMoveControls(): void {\n if (drag) endDrag(false);\n hoveredItem = null;\n if (layer) layer.replaceChildren();\n clearPreview();\n bar?.remove();\n bar = null;\n}\n\nfunction ensureLayer(): HTMLDivElement {\n if (!layer || !document.contains(layer)) layer = createControlsLayer(\"upstart-move-controls\");\n return layer;\n}\n\n// Touch: tapping the page selects the movable element under the finger (or clears the\n// selection), since there is no hover to reveal its handles.\nfunction onTapSelect(e: PointerEvent): void {\n lastPointerType = e.pointerType;\n if (e.pointerType === \"mouse\" || getCurrentMode() !== \"move\" || drag) return;\n const target = e.target as HTMLElement | null;\n if (!target || layer?.contains(target) || bar?.contains(target)) return;\n hoveredItem = target.closest<HTMLElement>(`[${MOVE_ID_ATTR}]`);\n scheduleRefresh();\n}\n\nfunction onHoverMove(e: MouseEvent): void {\n // Compatibility mouse events follow a tap: the tap already chose the element.\n if (getCurrentMode() !== \"move\" || drag || lastPointerType !== \"mouse\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The movable element under the pointer, or the one owning the hovered handle.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n return owner && document.contains(owner) ? owner : null;\n }\n return target.closest<HTMLElement>(`[${MOVE_ID_ATTR}]`);\n}\n\ninterface TreeNode {\n parentElement: TreeNode | null;\n}\n\n/**\n * True when `el` is at the same level as `source`: a sibling (same container), or a cousin\n * (a child of a container next to the source's own — the other column, the next card).\n */\nexport function isSameLevel(source: TreeNode, el: TreeNode): boolean {\n if (el === source) return false;\n const parent = source.parentElement;\n const elParent = el.parentElement;\n if (!parent || !elParent) return false;\n if (elParent === parent) return true;\n // A cousin's container must not be the source itself (that would be a child of it).\n return (\n elParent !== source && elParent.parentElement !== null && elParent.parentElement === parent.parentElement\n );\n}\n\n/**\n * Elements `source` can be dropped next to: movable elements of the same component that are\n * at the same level (siblings and cousins, see `isSameLevel`).\n */\nexport function candidatesFor(source: HTMLElement): HTMLElement[] {\n const scope = source.getAttribute(MOVE_SCOPE_ATTR);\n if (!scope) return [];\n return [...document.querySelectorAll<HTMLElement>(`[${MOVE_SCOPE_ATTR}=\"${cssAttr(scope)}\"]`)].filter(\n (el) => isSameLevel(source, el),\n );\n}\n\nfunction axisOf(el: HTMLElement): DropTarget[\"axis\"] {\n const display = getComputedStyle(el).display;\n if (display.startsWith(\"inline\")) return \"horizontal\";\n const parent = el.parentElement;\n if (!parent) return \"vertical\";\n const style = getComputedStyle(parent);\n if (style.display.includes(\"grid\")) return \"horizontal\";\n if (style.display.includes(\"flex\") && !style.flexDirection.startsWith(\"column\")) return \"horizontal\";\n return \"vertical\";\n}\n\ninterface Rect {\n left: number;\n top: number;\n right: number;\n bottom: number;\n width: number;\n height: number;\n}\n\n/**\n * The item a drop at (x, y) lands next to: the one under the pointer (the smallest, should\n * several overlap), or else the nearest one within SNAP_DISTANCE.\n */\nexport function pickNearest<T>(\n items: { item: T; rect: Rect }[],\n x: number,\n y: number,\n): { item: T; rect: Rect } | null {\n let best: { item: T; rect: Rect; score: number } | null = null;\n for (const { item, rect } of items) {\n if (rect.width === 0 && rect.height === 0) continue;\n const dx = Math.max(rect.left - x, 0, x - rect.right);\n const dy = Math.max(rect.top - y, 0, y - rect.bottom);\n const distance = Math.hypot(dx, dy);\n if (distance > SNAP_DISTANCE) continue;\n const score = distance === 0 ? -1 / Math.max(rect.width * rect.height, 1) : distance;\n if (!best || score < best.score) best = { item, rect, score };\n }\n return best && { item: best.item, rect: best.rect };\n}\n\n/** Before or after an element, from the pointer vs the element's middle along the axis. */\nexport function dropPosition(rect: Rect, axis: DropTarget[\"axis\"], x: number, y: number): DropPosition {\n const after = axis === \"horizontal\" ? x > rect.left + rect.width / 2 : y > rect.top + rect.height / 2;\n return after ? \"after\" : \"before\";\n}\n\nfunction findDropTarget(source: HTMLElement, x: number, y: number): DropTarget | null {\n const items = candidatesFor(source).map((item) => ({ item, rect: item.getBoundingClientRect() }));\n const best = pickNearest(items, x, y);\n if (!best) return null;\n const axis = axisOf(best.item);\n return { element: best.item, position: dropPosition(best.rect, axis, x, y), axis };\n}\n\nfunction makeHandle(item: HTMLElement, top: number, left: number, size: number): HTMLButtonElement {\n const btn = baseButton(top, left, size, \"#7270c6\");\n setIcon(btn, \"grip\", Math.round(size * 0.7));\n btn.title = `${getLabels().dragToMove} (<${item.tagName.toLowerCase()}>)`;\n btn.style.cursor = \"grab\";\n // Without it, a touch drag is taken for a page scroll: the browser cancels the pointer\n // events, so neither the insertion bar nor the drop would happen.\n btn.style.touchAction = \"none\";\n btn.addEventListener(\"pointerenter\", () => {\n clearPreview();\n item.setAttribute(PREVIEW_ATTR, \"\");\n });\n btn.addEventListener(\"pointerleave\", () => item.removeAttribute(PREVIEW_ATTR));\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n // Capture keeps the pointerup (and so the click) on the handle, so releasing the\n // button over the page never reaches the editor's click handler.\n btn.setPointerCapture?.(e.pointerId);\n startDrag(item, btn);\n });\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n });\n return btn;\n}\n\nfunction startDrag(source: HTMLElement, handle: HTMLElement): void {\n drag = { source, target: null };\n hideOverlays();\n clearPreview();\n // Keep only the grabbed handle: it holds the pointer capture, so it must stay in the DOM.\n for (const child of [...(layer?.children ?? [])]) if (child !== handle) child.remove();\n source.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n document.documentElement.style.cursor = \"grabbing\";\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragDrop, { once: true });\n document.addEventListener(\"pointercancel\", onDragCancel, { once: true });\n document.addEventListener(\"keydown\", onDragKey, true);\n}\n\nfunction onDragCancel(): void {\n endDrag(false);\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n if (e.clientY < AUTO_SCROLL_EDGE) window.scrollBy(0, -AUTO_SCROLL_STEP);\n else if (e.clientY > window.innerHeight - AUTO_SCROLL_EDGE) window.scrollBy(0, AUTO_SCROLL_STEP);\n drag.target = findDropTarget(drag.source, e.clientX, e.clientY);\n drawIndicator(drag.target);\n}\n\nfunction onDragDrop(): void {\n endDrag(true);\n}\n\nfunction onDragKey(e: KeyboardEvent): void {\n if (e.key !== \"Escape\") return;\n e.preventDefault();\n e.stopPropagation();\n endDrag(false);\n}\n\nfunction endDrag(commit: boolean): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.removeEventListener(\"pointerup\", onDragDrop);\n document.removeEventListener(\"pointercancel\", onDragCancel);\n document.removeEventListener(\"keydown\", onDragKey, true);\n document.body.style.removeProperty(\"user-select\");\n document.documentElement.style.removeProperty(\"cursor\");\n const current = drag;\n drag = null;\n if (!current) return;\n current.source.style.removeProperty(\"opacity\");\n layer?.replaceChildren();\n indicator = null;\n if (commit && current.target) stageMove(current.source, current.target);\n scheduleRefresh();\n}\n\n// Dropping right where the element already is (next to its own neighbour) changes nothing.\nfunction isNoOp(source: HTMLElement, target: DropTarget): boolean {\n const el = target.element;\n return target.position === \"after\"\n ? el.nextElementSibling === source\n : el.previousElementSibling === source;\n}\n\nfunction stageMove(source: HTMLElement, target: DropTarget): void {\n const id = source.getAttribute(MOVE_ID_ATTR);\n const targetId = target.element.getAttribute(MOVE_ID_ATTR);\n const fromParent = source.parentNode;\n if (!id || !targetId || !fromParent || isNoOp(source, target)) return;\n staged.push({\n node: source,\n fromParent,\n fromNext: source.nextSibling,\n id,\n targetId,\n position: target.position,\n });\n if (target.position === \"after\") target.element.after(source);\n else target.element.before(source);\n}\n\n/** Undo the last staged move (the DOM goes back to where it was before that drop). */\nexport function undoLastMove(): void {\n const move = staged.pop();\n if (!move) return;\n const { node, fromParent, fromNext } = move;\n fromParent.insertBefore(node, fromNext && fromNext.parentNode === fromParent ? fromNext : null);\n scheduleRefresh();\n}\n\nexport function discardMoves(): void {\n while (staged.length > 0) undoLastMove();\n}\n\n/** The staged moves, as sent to the editor on apply. */\nexport function getStagedMoves(): { id: string; targetId: string; position: DropPosition }[] {\n return staged.map(({ id, targetId, position }) => ({ id, targetId, position }));\n}\n\nfunction applyMoves(): void {\n if (staged.length === 0 || applying) return;\n // The editor rebuilds then reloads the preview, which resets this runtime. Until then the\n // staged moves are kept, so a failed apply can still be undone.\n applying = true;\n sendToParent({ type: \"text-edit\", payload: { action: \"moveElements\", moves: getStagedMoves() } });\n renderBar();\n}\n\n/** The editor could not apply the moves: keep them staged and let the user retry or undo. */\nexport function onMovesApplyFailed(): void {\n applying = false;\n renderBar();\n}\n\n// Cmd/Ctrl+Z undoes the last move, unless the focus is in a form field.\nfunction onUndoKey(e: KeyboardEvent): void {\n if (staged.length === 0 || applying || getCurrentMode() !== \"move\") return;\n if (e.key.toLowerCase() !== \"z\" || !(e.metaKey || e.ctrlKey) || e.shiftKey || e.altKey) return;\n const active = document.activeElement as HTMLElement | null;\n if (active?.isContentEditable || active?.closest(\"input, textarea, select\")) return;\n e.preventDefault();\n undoLastMove();\n}\n\nfunction renderBar(): void {\n // A navigation replaced the page: the staged nodes are gone along with their moves.\n if (staged.some((m) => !document.contains(m.node))) {\n staged.length = 0;\n applying = false;\n }\n if (staged.length === 0) {\n bar?.remove();\n bar = null;\n return;\n }\n if (!bar || !document.contains(bar)) {\n bar = document.createElement(\"div\");\n bar.id = \"upstart-move-bar\";\n document.body.appendChild(bar);\n }\n const labels = getLabels();\n const text = document.createElement(\"span\");\n text.textContent = `${labels.pendingMoves} ${staged.length}`;\n bar.replaceChildren(\n text,\n barButton(labels.undoMove, false, undoLastMove),\n barButton(labels.discardMoves, false, discardMoves),\n barButton(labels.applyMoves, true, applyMoves),\n );\n}\n\nfunction barButton(label: string, primary: boolean, action: () => void): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.textContent = label;\n btn.disabled = applying;\n if (primary) btn.setAttribute(\"data-primary\", \"\");\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n action();\n });\n return btn;\n}\n\nfunction drawIndicator(target: DropTarget | null): void {\n indicator?.remove();\n indicator = null;\n if (!target) return;\n const root = ensureLayer();\n const r = target.element.getBoundingClientRect();\n const line = document.createElement(\"div\");\n const thickness = 3;\n const x = window.scrollX;\n const y = window.scrollY;\n const box =\n target.axis === \"vertical\"\n ? {\n top: (target.position === \"before\" ? r.top - 4 : r.bottom + 4) + y - thickness / 2,\n left: r.left + x,\n width: r.width,\n height: thickness,\n }\n : {\n top: r.top + y,\n left: (target.position === \"before\" ? r.left - 4 : r.right + 4) + x - thickness / 2,\n width: thickness,\n height: r.height,\n };\n line.setAttribute(\"data-upstart-drop-indicator\", target.position);\n line.style.cssText =\n `position: absolute; top: ${box.top}px; left: ${box.left}px; width: ${box.width}px; ` +\n `height: ${box.height}px; background: #7270c6; border-radius: 2px; pointer-events: none;`;\n root.appendChild(line);\n indicator = line;\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshMoveControls();\n });\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { PayloadArrayItemAdd, PayloadArrayItemDelete, PayloadArraySet, PayloadEditText, PayloadEditTextDirect } from "../../upstart-editor-api.js";
1
+ import { PayloadArrayItemAdd, PayloadArrayItemDelete, PayloadArraySet, PayloadEditText, PayloadEditTextDirect, PayloadMoveElements } from "../../upstart-editor-api.js";
2
2
  import { RuntimeLabels } from "./labels.js";
3
3
  import { Editor } from "@tiptap/core";
4
4
 
@@ -8,8 +8,9 @@ import { Editor } from "@tiptap/core";
8
8
  * - preview: navigate the site, no editing
9
9
  * - edit: click an element to edit it (inspector / inline text)
10
10
  * - select: pick multiple elements to attach to the next chat message
11
+ * - move: drag & drop elements, staged in the preview until applied
11
12
  */
12
- type EditorMode = "preview" | "edit" | "select";
13
+ type EditorMode = "preview" | "edit" | "select" | "move";
13
14
  /**
14
15
  * A reference to an element picked in "select" mode. Built from the data-upstart-*
15
16
  * attributes injected by vite-plugin-upstart-attrs. `key` (e.g. `filePath:hash`) is
@@ -85,7 +86,7 @@ interface EditableImageRef {
85
86
  */
86
87
  type EditorMessage = {
87
88
  type: "text-edit";
88
- payload: PayloadEditText | PayloadEditTextDirect | PayloadArrayItemAdd | PayloadArrayItemDelete | PayloadArraySet;
89
+ payload: PayloadEditText | PayloadEditTextDirect | PayloadArrayItemAdd | PayloadArrayItemDelete | PayloadArraySet | PayloadMoveElements;
89
90
  } | {
90
91
  type: "editor-ready";
91
92
  path?: string;
@@ -168,6 +169,8 @@ type ParentMessage = {
168
169
  } | {
169
170
  type: "deselect";
170
171
  key: string;
172
+ } /** Applying the staged element moves failed: they stay staged in the preview. */ | {
173
+ type: "moves-apply-failed";
171
174
  } /** Translated labels for the controls the runtime renders (see `runtime/labels.ts`). */ | {
172
175
  type: "set-labels";
173
176
  labels: Partial<RuntimeLabels>;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;AAkBA;;;;;KAAY,UAAA;;;;;;UAOK,YAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,IAAA;EAiBU;;;;;EAXV,SAAA;AAAA;;;;;;;;;KAWU,cAAA;;AAiBZ;;UAZiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMe,cAAA;EACf,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,WAAA;EACT,IAAA;EACA,IAAA,EAAM,cAAA;AAAA;;;;;UAOS,gBAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;EAMA;EAJA,WAAA;EACA,gBAAA;EAOA;EALA,KAAA;EACA,MAAA;EACA,YAAA;EACA,aAAA;EAWU;EATV,OAAA;;EAEA,iBAAA;EACA,eAAA;AAAA;;;;KAMU,aAAA;EAEN,IAAA;EACA,OAAA,EACI,eAAA,GACA,qBAAA,GACA,mBAAA,GACA,sBAAA,GACA,eAAA;AAAA;EAEJ,IAAA;EAAsB,IAAA;AAAA;EACtB,IAAA;EAA0B,IAAA;AAAA;EAE1B,IAAA;EAAsB,OAAA;AAAA;EACtB,IAAA;EAAyB,CAAA;EAAW,CAAA;AAAA;EACpC,IAAA;EAAsB,KAAA;AAAA;;;;;;;EAMtB,IAAA;EAAuB,KAAA;AAAA;EACvB,IAAA;EAA2B,SAAA,EAAW,YAAA;AAAA;EAEtC,IAAA;EACA,IAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA,GAAc,MAAA;EACd,MAAA,EAAQ,aAAA;EACR,aAAA,WAUY;EARZ,OAAA,WAcM;EAZN,MAAA,GAAS,gBAAA;;;;;;EAMT,YAAA;AAAA;;;;KAMM,aAAA;EACN,IAAA;EAAkB,IAAA,EAAM,UAAA;AAAA;EACxB,IAAA;EAA2B,WAAA;EAAqB,SAAA;EAAmB,UAAA;AAAA;EACnE,IAAA;EAAuB,OAAA;EAAiB,GAAA;AAAA;EAExC,IAAA;EAA6B,OAAA;EAAiB,GAAA;AAAA;EAE9C,IAAA;EAAuB,GAAA;AAAA;EACvB,IAAA;AAAA;EACA,IAAA;EAAiC,CAAA;EAAW,CAAA;AAAA;EAC5C,IAAA;AAAA;EACA,IAAA;EAAkB,GAAA;AAAA;EAElB,IAAA;EAAoB,MAAA,EAAQ,OAAA,CAAQ,aAAA;AAAA;;;;UAKzB,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;;UAMc,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;;UAMc,oBAAA;EAiD0B;;;;EA5CzC,gBAAA;;;;;;EAOA,sBAAA;;;;;EAMA,iBAAA;;;;;EAMA,UAAA;;;;;EAMA,WAAA;;;;;EAMA,aAAA;;;;;;EAOA,kBAAA,IAAsB,SAAA,UAAmB,GAAA;AAAA;;;;UAM1B,0BAAA;;;;;EAKf,OAAA;;;;;EAMA,UAAA;AAAA"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/types.ts"],"mappings":";;;;;;AAoBA;;;;;AAOA;KAPY,UAAA;;;;;;UAOK,YAAA;EACf,GAAA;EACA,IAAA;EACA,QAAA;EACA,aAAA;EACA,QAAA;EACA,IAAA;EAiBwB;;;;AAK1B;EAhBE,SAAA;AAAA;;;;;;;;;KAWU,cAAA;AAiBZ;;;AAAA,UAZiB,aAAA;EACf,GAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMe,cAAA;EACf,MAAA,EAAQ,MAAA;EACR,OAAA,EAAS,WAAA;EACT,IAAA;EACA,IAAA,EAAM,cAAA;AAAA;;;;;UAOS,gBAAA;EACf,EAAA;EACA,GAAA;EACA,GAAA;EAOA;EALA,WAAA;EACA,gBAAA;EASA;EAPA,KAAA;EACA,MAAA;EACA,YAAA;EACA,aAAA;EAWuB;EATvB,OAAA;EAaQ;EAXR,iBAAA;EACA,eAAA;AAAA;;;;KAMU,aAAA;EAEN,IAAA;EACA,OAAA,EACI,eAAA,GACA,qBAAA,GACA,mBAAA,GACA,sBAAA,GACA,eAAA,GACA,mBAAA;AAAA;EAEJ,IAAA;EAAsB,IAAA;AAAA;EACtB,IAAA;EAA0B,IAAA;AAAA;EAE1B,IAAA;EAAsB,OAAA;AAAA;EACtB,IAAA;EAAyB,CAAA;EAAW,CAAA;AAAA;EACpC,IAAA;EAAsB,KAAA;AAAA;;;;;;;EAMtB,IAAA;EAAuB,KAAA;AAAA;EACvB,IAAA;EAA2B,SAAA,EAAW,YAAA;AAAA;EAEtC,IAAA;EACA,IAAA;EACA,aAAA;EACA,QAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,QAAA;EACA,WAAA,GAAc,MAAA;EACd,MAAA,EAAQ,aAAA;EACR,aAAA,WAUA;EARA,OAAA,WAQY;EANZ,MAAA,GAAS,gBAAA;EAYU;;;;;EANnB,YAAA;AAAA;;;;KAMM,aAAA;EACN,IAAA;EAAkB,IAAA,EAAM,UAAA;AAAA;EACxB,IAAA;EAA2B,WAAA;EAAqB,SAAA;EAAmB,UAAA;AAAA;EACnE,IAAA;EAAuB,OAAA;EAAiB,GAAA;AAAA;EAExC,IAAA;EAA6B,OAAA;EAAiB,GAAA;AAAA;EAE9C,IAAA;EAAuB,GAAA;AAAA;EACvB,IAAA;AAAA;EACA,IAAA;EAAiC,CAAA;EAAW,CAAA;AAAA;EAC5C,IAAA;AAAA;EACA,IAAA;EAAkB,GAAA;AAAA;EAElB,IAAA;AAAA;EAEA,IAAA;EAAoB,MAAA,EAAQ,OAAA,CAAQ,aAAA;AAAA;;;AAuB1C;UAlBiB,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;;;UAMc,oBAAA;EACf,MAAA;EACA,IAAA,EAAM,aAAA;EAAA,CACL,GAAA;AAAA;;AAuDH;;UAjDiB,oBAAA;EAsDf;;;;EAjDA,gBAAA;;;;;;EAOA,sBAAA;;;;;EAMA,iBAAA;;;;;EAMA,UAAA;;;;;EAMA,WAAA;;;;;EAMA,aAAA;;;;;;EAOA,kBAAA,IAAsB,SAAA,UAAmB,GAAA;AAAA;;;;UAM1B,0BAAA;;;;;EAKf,OAAA;;;;;EAMA,UAAA;AAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@upstart.gg/vite-plugins",
3
- "version": "0.1.84",
3
+ "version": "0.1.85",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -23,7 +23,7 @@
23
23
  "oxc-parser": "0.101.0",
24
24
  "unplugin": "^2.3.11",
25
25
  "zimmerframe": "^1.1.4",
26
- "@upstart.gg/sdk": "^0.1.84"
26
+ "@upstart.gg/sdk": "^0.1.85"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@rolldown/binding-linux-arm64-gnu": "1.0.0",
@@ -118,7 +118,7 @@
118
118
  },
119
119
  "peerDependencies": {
120
120
  "zod": "4.3.6",
121
- "@upstart.gg/sdk": "^0.1.84"
121
+ "@upstart.gg/sdk": "^0.1.85"
122
122
  },
123
123
  "author": "Upstart",
124
124
  "publishConfig": {
@@ -0,0 +1,86 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { dropPosition, isSameLevel, pickNearest } from "../vite-plugin-upstart-editor/runtime/move-controls";
3
+
4
+ const rect = (left: number, top: number, width: number, height: number) => ({
5
+ left,
6
+ top,
7
+ width,
8
+ height,
9
+ right: left + width,
10
+ bottom: top + height,
11
+ });
12
+
13
+ describe("pickNearest", () => {
14
+ const title = { item: "title", rect: rect(0, 0, 400, 40) };
15
+ const text = { item: "text", rect: rect(0, 60, 400, 100) };
16
+
17
+ test("picks the element under the pointer", () => {
18
+ expect(pickNearest([title, text], 100, 80)?.item).toBe("text");
19
+ });
20
+
21
+ test("picks the innermost of nested elements", () => {
22
+ const strong = { item: "strong", rect: rect(10, 70, 50, 20) };
23
+ expect(pickNearest([text, strong], 20, 75)?.item).toBe("strong");
24
+ });
25
+
26
+ test("snaps to the nearest element in the gap between two elements", () => {
27
+ expect(pickNearest([title, text], 100, 45)?.item).toBe("title");
28
+ expect(pickNearest([title, text], 100, 57)?.item).toBe("text");
29
+ });
30
+
31
+ test("ignores elements too far away and hidden ones", () => {
32
+ expect(pickNearest([title, text], 100, 400)).toBeNull();
33
+ expect(pickNearest([{ item: "hidden", rect: rect(0, 0, 0, 0) }], 0, 0)).toBeNull();
34
+ });
35
+ });
36
+
37
+ describe("dropPosition", () => {
38
+ const r = rect(0, 0, 200, 100);
39
+
40
+ test("uses the vertical middle for stacked elements", () => {
41
+ expect(dropPosition(r, "vertical", 190, 20)).toBe("before");
42
+ expect(dropPosition(r, "vertical", 10, 80)).toBe("after");
43
+ });
44
+
45
+ test("uses the horizontal middle for side-by-side elements", () => {
46
+ expect(dropPosition(r, "horizontal", 20, 90)).toBe("before");
47
+ expect(dropPosition(r, "horizontal", 150, 10)).toBe("after");
48
+ });
49
+ });
50
+
51
+ describe("isSameLevel", () => {
52
+ type Node = { name: string; parentElement: Node | null };
53
+ const node = (name: string, parent: Node | null): Node => ({ name, parentElement: parent });
54
+
55
+ // section > [colA > [img, title], colB > [text], aside > nested > [deep]]
56
+ const root = node("root", null);
57
+ const section = node("section", root);
58
+ const colA = node("colA", section);
59
+ const colB = node("colB", section);
60
+ const img = node("img", colA);
61
+ const title = node("title", colA);
62
+ const text = node("text", colB);
63
+ const aside = node("aside", section);
64
+ const nested = node("nested", aside);
65
+ const deep = node("deep", nested);
66
+
67
+ test("siblings are at the same level", () => {
68
+ expect(isSameLevel(img, title)).toBe(true);
69
+ expect(isSameLevel(colA, colB)).toBe(true);
70
+ });
71
+
72
+ test("cousins (children of a neighbouring container) are at the same level", () => {
73
+ expect(isSameLevel(img, text)).toBe(true);
74
+ expect(isSameLevel(text, title)).toBe(true);
75
+ expect(isSameLevel(img, nested)).toBe(true);
76
+ });
77
+
78
+ test("the element itself, its parent and deeper or higher elements are not", () => {
79
+ expect(isSameLevel(img, img)).toBe(false);
80
+ expect(isSameLevel(img, colA)).toBe(false);
81
+ expect(isSameLevel(img, section)).toBe(false);
82
+ expect(isSameLevel(img, deep)).toBe(false);
83
+ expect(isSameLevel(colA, img)).toBe(false);
84
+ expect(isSameLevel(colA, text)).toBe(false);
85
+ });
86
+ });