@upstart.gg/vite-plugins 0.1.83 → 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 (44) hide show
  1. package/dist/site-theme.d.ts +55 -0
  2. package/dist/site-theme.d.ts.map +1 -0
  3. package/dist/site-theme.js +184 -0
  4. package/dist/site-theme.js.map +1 -0
  5. package/dist/upstart-editor-api.d.ts +63 -1
  6. package/dist/upstart-editor-api.d.ts.map +1 -1
  7. package/dist/upstart-editor-api.js +233 -1
  8. package/dist/upstart-editor-api.js.map +1 -1
  9. package/dist/vite-plugin-upstart-attrs.d.ts +1 -1
  10. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  11. package/dist/vite-plugin-upstart-attrs.js +65 -2
  12. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  13. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +2 -24
  14. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -1
  15. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +7 -1
  16. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js +33 -0
  18. package/dist/vite-plugin-upstart-editor/runtime/controls-ui.js.map +1 -0
  19. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js +35 -0
  21. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts +5 -0
  23. package/dist/vite-plugin-upstart-editor/runtime/labels.d.ts.map +1 -1
  24. package/dist/vite-plugin-upstart-editor/runtime/labels.js +5 -0
  25. package/dist/vite-plugin-upstart-editor/runtime/labels.js.map +1 -1
  26. package/dist/vite-plugin-upstart-editor/runtime/move-controls.js +434 -0
  27. package/dist/vite-plugin-upstart-editor/runtime/move-controls.js.map +1 -0
  28. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +9 -3
  29. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  30. package/package.json +3 -3
  31. package/src/site-theme.ts +205 -0
  32. package/src/tests/move-controls.test.ts +86 -0
  33. package/src/tests/site-theme.test.ts +122 -0
  34. package/src/tests/upstart-editor-api-move-element.test.ts +220 -0
  35. package/src/tests/vite-plugin-upstart-attrs.test.ts +165 -4
  36. package/src/upstart-editor-api.ts +294 -0
  37. package/src/vite-plugin-upstart-attrs.ts +93 -2
  38. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +2 -40
  39. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +17 -3
  40. package/src/vite-plugin-upstart-editor/runtime/controls-ui.ts +48 -0
  41. package/src/vite-plugin-upstart-editor/runtime/index.ts +46 -0
  42. package/src/vite-plugin-upstart-editor/runtime/labels.ts +10 -0
  43. package/src/vite-plugin-upstart-editor/runtime/move-controls.ts +536 -0
  44. package/src/vite-plugin-upstart-editor/runtime/types.ts +9 -2
@@ -1,6 +1,7 @@
1
1
  import { getCurrentMode } from "./state.js";
2
2
  import { sendToParent } from "./utils.js";
3
3
  import { getLabels } from "./labels.js";
4
+ import { baseButton, createControlsLayer, cssAttr, setIcon } from "./controls-ui.js";
4
5
 
5
6
  // Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY
6
7
  // (no server round-trip per action): "×" marks an item for removal, "+" inserts an
@@ -191,12 +192,7 @@ export function hideArrayControls(): void {
191
192
 
192
193
  function ensureLayer(): HTMLDivElement {
193
194
  if (layer) return layer;
194
- layer = document.createElement("div");
195
- layer.id = "upstart-array-controls";
196
- layer.style.cssText =
197
- "position: absolute; top: 0; left: 0; width: 0; height: 0; " +
198
- "pointer-events: none; z-index: 2147483646;";
199
- document.body.appendChild(layer);
195
+ layer = createControlsLayer("upstart-array-controls");
200
196
  return layer;
201
197
  }
202
198
 
@@ -237,10 +233,6 @@ function isReordered(arrayId: string): boolean {
237
233
  return false;
238
234
  }
239
235
 
240
- function cssAttr(value: string): string {
241
- return value.replace(/["\\]/g, "\\$&");
242
- }
243
-
244
236
  function textOf(el: HTMLElement): string {
245
237
  return (el.textContent ?? "").trim();
246
238
  }
@@ -261,36 +253,6 @@ function applyDeletionStyle(item: HTMLElement, marked: boolean): void {
261
253
  // Buttons
262
254
  // ---------------------------------------------------------------------------
263
255
 
264
- // Geometric SVG icons — centred by the flex container + viewBox, so they don't
265
- // depend on the host page's font metrics (which left text glyphs mis-aligned).
266
- const ICON_PATHS: Record<string, string> = {
267
- plus: '<path d="M12 5v14M5 12h14"/>',
268
- cross: '<path d="M6 6 18 18M18 6 6 18"/>',
269
- check: '<path d="M5 12.5 10 17.5 19 7"/>',
270
- restore: '<path d="M3 4v5h5"/><path d="M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9"/>',
271
- grip: '<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>',
272
- };
273
-
274
- function setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {
275
- const dots = name === "grip";
276
- btn.innerHTML =
277
- `<svg width="${px}" height="${px}" viewBox="0 0 24 24" ` +
278
- `fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" ` +
279
- 'stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" ' +
280
- `style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
281
- }
282
-
283
- function baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {
284
- const btn = document.createElement("button");
285
- btn.type = "button";
286
- btn.style.cssText =
287
- `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +
288
- "padding: 0; display: flex; align-items: center; justify-content: center; " +
289
- `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +
290
- "line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);";
291
- return btn;
292
- }
293
-
294
256
  function makeItemDeleteButton(
295
257
  arrayId: string,
296
258
  index: number,
@@ -206,6 +206,15 @@ function handleSelectClick(event: MouseEvent, target: HTMLElement): void {
206
206
 
207
207
  function handleClick(event: MouseEvent): void {
208
208
  const mode = getCurrentMode();
209
+ if (mode === "move") {
210
+ // The page is inert while moving elements: following a link or submitting a form would
211
+ // reload the page and drop the staged moves. The move controls handle their own clicks.
212
+ const target = event.target as HTMLElement | null;
213
+ if (target?.closest("#upstart-move-controls, #upstart-move-bar")) return;
214
+ event.preventDefault();
215
+ event.stopPropagation();
216
+ return;
217
+ }
209
218
  if (mode !== "edit" && mode !== "select") {
210
219
  return;
211
220
  }
@@ -225,9 +234,14 @@ function handleClick(event: MouseEvent): void {
225
234
  return;
226
235
  }
227
236
 
228
- // Editor-only chrome (the array ×/+/✓ controls) and locally-staged draft list
229
- // items handle their own clicks and must never be treated as element selection.
230
- if (target.closest("#upstart-array-controls, [data-upstart-draft-item]")) {
237
+ // Editor-only chrome (the array ×/+/✓ controls, the move handles and pending-moves bar)
238
+ // and locally-staged draft list items handle their own clicks and must never be treated
239
+ // as element selection.
240
+ if (
241
+ target.closest(
242
+ "#upstart-array-controls, #upstart-move-controls, #upstart-move-bar, [data-upstart-draft-item]",
243
+ )
244
+ ) {
231
245
  return;
232
246
  }
233
247
 
@@ -0,0 +1,48 @@
1
+ // Shared building blocks of the editor-only controls (array list buttons, move handles).
2
+
3
+ // Geometric SVG icons — centred by the flex container + viewBox, so they don't
4
+ // depend on the host page's font metrics (which left text glyphs mis-aligned).
5
+ const ICON_PATHS = {
6
+ plus: '<path d="M12 5v14M5 12h14"/>',
7
+ cross: '<path d="M6 6 18 18M18 6 6 18"/>',
8
+ check: '<path d="M5 12.5 10 17.5 19 7"/>',
9
+ restore: '<path d="M3 4v5h5"/><path d="M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9"/>',
10
+ grip: '<circle cx="9" cy="6" r="1.5"/><circle cx="15" cy="6" r="1.5"/><circle cx="9" cy="12" r="1.5"/><circle cx="15" cy="12" r="1.5"/><circle cx="9" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/>',
11
+ } as const;
12
+
13
+ export type IconName = keyof typeof ICON_PATHS;
14
+
15
+ export function setIcon(btn: HTMLElement, name: IconName, px: number): void {
16
+ const dots = name === "grip";
17
+ btn.innerHTML =
18
+ `<svg width="${px}" height="${px}" viewBox="0 0 24 24" ` +
19
+ `fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" ` +
20
+ 'stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" ' +
21
+ `style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
22
+ }
23
+
24
+ export function baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {
25
+ const btn = document.createElement("button");
26
+ btn.type = "button";
27
+ btn.style.cssText =
28
+ `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +
29
+ "padding: 0; display: flex; align-items: center; justify-content: center; " +
30
+ `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +
31
+ "line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);";
32
+ return btn;
33
+ }
34
+
35
+ /** Absolutely positioned, click-through layer appended to <body> that hosts editor controls. */
36
+ export function createControlsLayer(id: string): HTMLDivElement {
37
+ const layer = document.createElement("div");
38
+ layer.id = id;
39
+ layer.style.cssText =
40
+ "position: absolute; top: 0; left: 0; width: 0; height: 0; " +
41
+ "pointer-events: none; z-index: 2147483646;";
42
+ document.body.appendChild(layer);
43
+ return layer;
44
+ }
45
+
46
+ export function cssAttr(value: string): string {
47
+ return value.replace(/["\\]/g, "\\$&");
48
+ }
@@ -4,6 +4,12 @@ import { initErrorHandler } from "./error-handler.js";
4
4
  import { installDomGuard } from "./dom-guard.js";
5
5
  import { initTextEditor, activateAllEditors, destroyAllActiveEditors } from "./text-editor.js";
6
6
  import { initArrayControls, refreshArrayControls, hideArrayControls } from "./array-controls.js";
7
+ import {
8
+ initMoveControls,
9
+ refreshMoveControls,
10
+ hideMoveControls,
11
+ onMovesApplyFailed,
12
+ } from "./move-controls.js";
7
13
  import { initFormGuard } from "./form-guard.js";
8
14
  import { sendToParent } from "./utils.js";
9
15
  import { getCurrentMode, setCurrentMode, clearSelection, removeSelection, getSelection } from "./state.js";
@@ -22,13 +28,20 @@ export function setMode(mode: EditorMode): void {
22
28
  console.log(`[Upstart Editor] Setting mode to: ${mode}`);
23
29
  if (mode === "edit") {
24
30
  disableSelectMode();
31
+ disableMoveMode();
25
32
  enableEditMode();
26
33
  } else if (mode === "select") {
27
34
  disableEditMode();
35
+ disableMoveMode();
28
36
  enableSelectMode();
37
+ } else if (mode === "move") {
38
+ disableEditMode();
39
+ disableSelectMode();
40
+ enableMoveMode();
29
41
  } else {
30
42
  disableEditMode();
31
43
  disableSelectMode();
44
+ disableMoveMode();
32
45
  }
33
46
  }
34
47
 
@@ -156,6 +169,7 @@ export function initUpstartEditor(): void {
156
169
  initClickHandler();
157
170
  initHoverOverlay();
158
171
  initArrayControls();
172
+ initMoveControls();
159
173
  initFormGuard();
160
174
  initErrorHandler();
161
175
  initRouteReporter();
@@ -237,6 +251,21 @@ function handleParentMessage(event: MessageEvent): void {
237
251
  } else if (styleEl) {
238
252
  styleEl.remove();
239
253
  }
254
+ } else if (message.type === "preview-theme") {
255
+ // Appended last in <head> so it wins over the theme's own variables until it is cleared.
256
+ const STYLE_ID = "upstart-preview-theme";
257
+ let styleEl = document.getElementById(STYLE_ID) as HTMLStyleElement | null;
258
+ const css = typeof message.css === "string" ? message.css : "";
259
+ if (css) {
260
+ if (!styleEl) {
261
+ styleEl = document.createElement("style");
262
+ styleEl.id = STYLE_ID;
263
+ }
264
+ styleEl.textContent = css;
265
+ document.head.appendChild(styleEl);
266
+ } else if (styleEl) {
267
+ styleEl.remove();
268
+ }
240
269
  } else if (message.type === "request-scroll-position") {
241
270
  sendToParent({ type: "scroll-position", x: window.scrollX, y: window.scrollY });
242
271
  } else if (message.type === "restore-scroll-position") {
@@ -251,6 +280,8 @@ function handleParentMessage(event: MessageEvent): void {
251
280
  sendToParent({ type: "selection-changed", selection: getSelection() });
252
281
  } else if (message.type === "set-labels") {
253
282
  setLabels(message.labels);
283
+ } else if (message.type === "moves-apply-failed") {
284
+ onMovesApplyFailed();
254
285
  }
255
286
  }
256
287
 
@@ -288,6 +319,21 @@ function enableSelectMode(): void {
288
319
  document.documentElement.setAttribute("data-upstart-select-mode", "");
289
320
  }
290
321
 
322
+ function enableMoveMode(): void {
323
+ console.log("[Upstart Editor] Move mode enabled");
324
+ document.documentElement.setAttribute("data-upstart-move-mode", "");
325
+ refreshMoveControls();
326
+ }
327
+
328
+ /**
329
+ * Staged moves are kept when leaving the mode (the moved elements stay where they were
330
+ * dropped); the pending-moves bar comes back with the mode.
331
+ */
332
+ function disableMoveMode(): void {
333
+ document.documentElement.removeAttribute("data-upstart-move-mode");
334
+ hideMoveControls();
335
+ }
336
+
291
337
  function disableSelectMode(): void {
292
338
  document.documentElement.removeAttribute("data-upstart-select-mode");
293
339
  hideOverlays();
@@ -11,6 +11,11 @@ export interface RuntimeLabels {
11
11
  addItem: string;
12
12
  applyChanges: string;
13
13
  dragToReorder: string;
14
+ dragToMove: string;
15
+ pendingMoves: string;
16
+ undoMove: string;
17
+ discardMoves: string;
18
+ applyMoves: string;
14
19
  clickToEdit: string;
15
20
  startTyping: string;
16
21
  recoveryTitle: string;
@@ -34,6 +39,11 @@ export const DEFAULT_LABELS: Readonly<RuntimeLabels> = Object.freeze({
34
39
  addItem: "Add item",
35
40
  applyChanges: "Apply changes",
36
41
  dragToReorder: "Drag to reorder",
42
+ dragToMove: "Drag to move",
43
+ pendingMoves: "Moves not saved yet:",
44
+ undoMove: "Undo",
45
+ discardMoves: "Discard all",
46
+ applyMoves: "Apply",
37
47
  clickToEdit: "Click to edit...",
38
48
  startTyping: "Start typing...",
39
49
  recoveryTitle: "The preview needs a refresh",