@upstart.gg/vite-plugins 0.1.41 → 0.1.42

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 (37) hide show
  1. package/dist/upstart-editor-api.d.ts +59 -1
  2. package/dist/upstart-editor-api.d.ts.map +1 -1
  3. package/dist/upstart-editor-api.js +273 -4
  4. package/dist/upstart-editor-api.js.map +1 -1
  5. package/dist/vite-plugin-upstart-attrs.d.ts +2 -1
  6. package/dist/vite-plugin-upstart-attrs.d.ts.map +1 -1
  7. package/dist/vite-plugin-upstart-attrs.js +91 -4
  8. package/dist/vite-plugin-upstart-attrs.js.map +1 -1
  9. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js +342 -0
  10. package/dist/vite-plugin-upstart-editor/runtime/array-controls.js.map +1 -0
  11. package/dist/vite-plugin-upstart-editor/runtime/click-handler.d.ts.map +1 -1
  12. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js +29 -2
  13. package/dist/vite-plugin-upstart-editor/runtime/click-handler.js.map +1 -1
  14. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js +80 -0
  15. package/dist/vite-plugin-upstart-editor/runtime/form-guard.js.map +1 -0
  16. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.d.ts.map +1 -1
  17. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js +2 -1
  18. package/dist/vite-plugin-upstart-editor/runtime/hover-overlay.js.map +1 -1
  19. package/dist/vite-plugin-upstart-editor/runtime/index.d.ts.map +1 -1
  20. package/dist/vite-plugin-upstart-editor/runtime/index.js +26 -4
  21. package/dist/vite-plugin-upstart-editor/runtime/index.js.map +1 -1
  22. package/dist/vite-plugin-upstart-editor/runtime/text-editor.d.ts.map +1 -1
  23. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js +40 -36
  24. package/dist/vite-plugin-upstart-editor/runtime/text-editor.js.map +1 -1
  25. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts +20 -4
  26. package/dist/vite-plugin-upstart-editor/runtime/types.d.ts.map +1 -1
  27. package/package.json +3 -3
  28. package/src/tests/vite-plugin-upstart-attrs.test.ts +412 -0
  29. package/src/upstart-editor-api.ts +314 -5
  30. package/src/vite-plugin-upstart-attrs.ts +154 -4
  31. package/src/vite-plugin-upstart-editor/runtime/array-controls.ts +478 -0
  32. package/src/vite-plugin-upstart-editor/runtime/click-handler.ts +43 -0
  33. package/src/vite-plugin-upstart-editor/runtime/form-guard.ts +121 -0
  34. package/src/vite-plugin-upstart-editor/runtime/hover-overlay.ts +6 -1
  35. package/src/vite-plugin-upstart-editor/runtime/index.ts +20 -4
  36. package/src/vite-plugin-upstart-editor/runtime/text-editor.ts +49 -58
  37. package/src/vite-plugin-upstart-editor/runtime/types.ts +31 -4
@@ -0,0 +1,342 @@
1
+ import { getCurrentMode } from "./state.js";
2
+ import { sendToParent } from "./utils.js";
3
+ //#region src/vite-plugin-upstart-editor/runtime/array-controls.ts
4
+ const DEFAULT_NEW_ITEM = "New item";
5
+ const DRAFT_ATTR = "data-upstart-draft-item";
6
+ const BTN_SIZE = 22;
7
+ const STACK_GAP = 6;
8
+ const BUTTON_MARGIN = 12;
9
+ let layer = null;
10
+ let observer = null;
11
+ let resizeObserver = null;
12
+ let rafId = null;
13
+ let isInitialized = false;
14
+ let isMutatingDom = false;
15
+ const observedForResize = /* @__PURE__ */ new WeakSet();
16
+ const pendingDeletes = /* @__PURE__ */ new Map();
17
+ let drag = null;
18
+ let hoveredItem = null;
19
+ const buttonOwner = /* @__PURE__ */ new WeakMap();
20
+ function initArrayControls() {
21
+ if (typeof document === "undefined" || isInitialized) return;
22
+ isInitialized = true;
23
+ window.addEventListener("scroll", scheduleRefresh, {
24
+ passive: true,
25
+ capture: true
26
+ });
27
+ window.addEventListener("resize", scheduleRefresh, { passive: true });
28
+ document.addEventListener("mouseover", onHoverMove, { passive: true });
29
+ observer = new MutationObserver((mutations) => {
30
+ if (isMutatingDom) return;
31
+ for (const m of mutations) {
32
+ const t = m.target;
33
+ if (layer && (t === layer || layer.contains(t))) continue;
34
+ scheduleRefresh();
35
+ return;
36
+ }
37
+ });
38
+ observer.observe(document.body, {
39
+ childList: true,
40
+ subtree: true
41
+ });
42
+ if (typeof ResizeObserver !== "undefined") resizeObserver = new ResizeObserver(() => scheduleRefresh());
43
+ }
44
+ function appendOwned(root, btn, owner) {
45
+ buttonOwner.set(btn, owner);
46
+ root.appendChild(btn);
47
+ }
48
+ function onHoverMove(e) {
49
+ if (getCurrentMode() !== "edit") return;
50
+ const owner = hoverOwner(e.target);
51
+ if (owner !== hoveredItem) {
52
+ hoveredItem = owner;
53
+ scheduleRefresh();
54
+ }
55
+ }
56
+ function hoverOwner(target) {
57
+ if (!target) return null;
58
+ const item = target.closest(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);
59
+ if (item) return item;
60
+ if (layer?.contains(target)) {
61
+ const owner = buttonOwner.get(target);
62
+ if (owner && document.contains(owner)) return owner;
63
+ }
64
+ return null;
65
+ }
66
+ function observeResize(el) {
67
+ if (resizeObserver && !observedForResize.has(el)) {
68
+ observedForResize.add(el);
69
+ resizeObserver.observe(el);
70
+ }
71
+ }
72
+ function refreshArrayControls() {
73
+ if (typeof document === "undefined") return;
74
+ if (getCurrentMode() !== "edit") {
75
+ hideArrayControls();
76
+ return;
77
+ }
78
+ if (drag) return;
79
+ isMutatingDom = true;
80
+ try {
81
+ const root = ensureLayer();
82
+ root.replaceChildren();
83
+ const groups = /* @__PURE__ */ new Map();
84
+ for (const el of document.querySelectorAll("[data-upstart-array-id]")) {
85
+ if (el.hasAttribute(DRAFT_ATTR)) continue;
86
+ const id = el.dataset.upstartArrayId;
87
+ if (!id) continue;
88
+ (groups.get(id) ?? groups.set(id, []).get(id)).push(el);
89
+ }
90
+ for (const [arrayId, items] of groups) {
91
+ items.sort((a, b) => itemIndex(a) - itemIndex(b));
92
+ const deletes = pendingDeletes.get(arrayId) ?? /* @__PURE__ */ new Set();
93
+ const drafts = draftPillsFor(arrayId);
94
+ const ordered = orderedItemsFor(arrayId);
95
+ const draggable = ordered.length > 1;
96
+ for (const item of items) {
97
+ observeResize(item);
98
+ const idx = itemIndex(item);
99
+ const marked = deletes.has(idx);
100
+ applyDeletionStyle(item, marked);
101
+ if (!marked && item !== hoveredItem) continue;
102
+ const rect = item.getBoundingClientRect();
103
+ if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);
104
+ appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);
105
+ }
106
+ for (const pill of drafts) {
107
+ observeResize(pill);
108
+ if (pill !== hoveredItem) continue;
109
+ const rect = pill.getBoundingClientRect();
110
+ if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);
111
+ appendOwned(root, makeDraftRemoveButton(pill, rect), pill);
112
+ }
113
+ const anchor = ordered[ordered.length - 1];
114
+ if (!anchor) continue;
115
+ const a = anchor.getBoundingClientRect();
116
+ const finalCount = items.length - deletes.size + drafts.length;
117
+ const showApply = (deletes.size > 0 || drafts.length > 0 || isReordered(arrayId)) && finalCount >= 1;
118
+ const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;
119
+ const left = a.right + window.scrollX + BUTTON_MARGIN;
120
+ const top = a.top + window.scrollY + (a.height - stackHeight) / 2;
121
+ root.appendChild(makeAddButton(arrayId, top, left));
122
+ if (showApply) root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));
123
+ }
124
+ } finally {
125
+ isMutatingDom = false;
126
+ }
127
+ }
128
+ function hideArrayControls() {
129
+ if (layer) layer.replaceChildren();
130
+ }
131
+ function ensureLayer() {
132
+ if (layer) return layer;
133
+ layer = document.createElement("div");
134
+ layer.id = "upstart-array-controls";
135
+ layer.style.cssText = "position: absolute; top: 0; left: 0; width: 0; height: 0; pointer-events: none; z-index: 2147483646;";
136
+ document.body.appendChild(layer);
137
+ return layer;
138
+ }
139
+ function itemIndex(el) {
140
+ const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : NaN;
141
+ return Number.isFinite(n) ? n : 0;
142
+ }
143
+ function originalItemsFor(arrayId) {
144
+ return [...document.querySelectorAll(`[data-upstart-array-id="${cssAttr(arrayId)}"]:not([${DRAFT_ATTR}])`)].sort((a, b) => itemIndex(a) - itemIndex(b));
145
+ }
146
+ function draftPillsFor(arrayId) {
147
+ return [...document.querySelectorAll(`[${DRAFT_ATTR}="${cssAttr(arrayId)}"]`)];
148
+ }
149
+ function orderedItemsFor(arrayId) {
150
+ const sel = `[data-upstart-array-id="${cssAttr(arrayId)}"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}="${cssAttr(arrayId)}"]`;
151
+ return [...document.querySelectorAll(sel)];
152
+ }
153
+ function isReordered(arrayId) {
154
+ let prev = -1;
155
+ for (const el of orderedItemsFor(arrayId)) {
156
+ if (el.hasAttribute(DRAFT_ATTR)) continue;
157
+ const idx = itemIndex(el);
158
+ if (idx < prev) return true;
159
+ prev = idx;
160
+ }
161
+ return false;
162
+ }
163
+ function cssAttr(value) {
164
+ return value.replace(/["\\]/g, "\\$&");
165
+ }
166
+ function textOf(el) {
167
+ return (el.textContent ?? "").trim();
168
+ }
169
+ function applyDeletionStyle(item, marked) {
170
+ if (marked) {
171
+ item.style.opacity = "0.4";
172
+ item.style.textDecoration = "line-through";
173
+ item.style.pointerEvents = "none";
174
+ } else {
175
+ item.style.removeProperty("opacity");
176
+ item.style.removeProperty("text-decoration");
177
+ item.style.removeProperty("pointer-events");
178
+ }
179
+ }
180
+ const ICON_PATHS = {
181
+ plus: "<path d=\"M12 5v14M5 12h14\"/>",
182
+ cross: "<path d=\"M6 6 18 18M18 6 6 18\"/>",
183
+ check: "<path d=\"M5 12.5 10 17.5 19 7\"/>",
184
+ restore: "<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>",
185
+ 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\"/>"
186
+ };
187
+ function setIcon(btn, name, px) {
188
+ const dots = name === "grip";
189
+ btn.innerHTML = `<svg width="${px}" height="${px}" viewBox="0 0 24 24" fill="${dots ? "currentColor" : "none"}" stroke="${dots ? "none" : "currentColor"}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;pointer-events:none">${ICON_PATHS[name]}</svg>`;
190
+ }
191
+ function baseButton(top, left, size, bg) {
192
+ const btn = document.createElement("button");
193
+ btn.type = "button";
194
+ btn.style.cssText = `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; padding: 0; display: flex; align-items: center; justify-content: center; background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);`;
195
+ return btn;
196
+ }
197
+ function makeItemDeleteButton(arrayId, index, marked, rect) {
198
+ const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, "#ef4444");
199
+ setIcon(btn, marked ? "restore" : "cross", 10);
200
+ btn.title = marked ? "Keep item" : "Remove item";
201
+ btn.addEventListener("click", (e) => {
202
+ e.preventDefault();
203
+ e.stopPropagation();
204
+ const set = pendingDeletes.get(arrayId) ?? /* @__PURE__ */ new Set();
205
+ if (set.has(index)) set.delete(index);
206
+ else set.add(index);
207
+ if (set.size) pendingDeletes.set(arrayId, set);
208
+ else pendingDeletes.delete(arrayId);
209
+ refreshArrayControls();
210
+ });
211
+ return btn;
212
+ }
213
+ function makeDraftRemoveButton(pill, rect) {
214
+ const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, "#ef4444");
215
+ setIcon(btn, "cross", 10);
216
+ btn.title = "Remove item";
217
+ btn.addEventListener("click", (e) => {
218
+ e.preventDefault();
219
+ e.stopPropagation();
220
+ pill.remove();
221
+ refreshArrayControls();
222
+ });
223
+ return btn;
224
+ }
225
+ function makeAddButton(arrayId, top, left) {
226
+ const btn = baseButton(top, left, 22, "#7270c6");
227
+ setIcon(btn, "plus", 14);
228
+ btn.title = "Add item";
229
+ btn.addEventListener("click", (e) => {
230
+ e.preventDefault();
231
+ e.stopPropagation();
232
+ addDraftPill(arrayId);
233
+ });
234
+ return btn;
235
+ }
236
+ function makeApplyButton(arrayId, top, left) {
237
+ const btn = baseButton(top, left, 22, "#16a34a");
238
+ setIcon(btn, "check", 13);
239
+ btn.title = "Apply changes";
240
+ btn.addEventListener("click", (e) => {
241
+ e.preventDefault();
242
+ e.stopPropagation();
243
+ applyArray(arrayId);
244
+ });
245
+ return btn;
246
+ }
247
+ function makeDragHandle(arrayId, node, rect) {
248
+ const btn = baseButton(rect.top + window.scrollY - 8, rect.left + window.scrollX + rect.width / 2 - 8, 16, "#6b7280");
249
+ setIcon(btn, "grip", 12);
250
+ btn.title = "Drag to reorder";
251
+ btn.style.cursor = "grab";
252
+ btn.addEventListener("pointerdown", (e) => {
253
+ e.preventDefault();
254
+ e.stopPropagation();
255
+ startDrag(arrayId, node);
256
+ });
257
+ return btn;
258
+ }
259
+ function startDrag(arrayId, node) {
260
+ drag = {
261
+ arrayId,
262
+ node
263
+ };
264
+ node.style.opacity = "0.5";
265
+ document.body.style.userSelect = "none";
266
+ document.addEventListener("pointermove", onDragMove);
267
+ document.addEventListener("pointerup", onDragEnd, { once: true });
268
+ }
269
+ function onDragMove(e) {
270
+ if (!drag) return;
271
+ const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);
272
+ if (!target) return;
273
+ const r = target.getBoundingClientRect();
274
+ if (e.clientX > r.left + r.width / 2) target.after(drag.node);
275
+ else target.before(drag.node);
276
+ }
277
+ function onDragEnd() {
278
+ document.removeEventListener("pointermove", onDragMove);
279
+ document.body.style.removeProperty("user-select");
280
+ if (drag) {
281
+ drag.node.style.removeProperty("opacity");
282
+ drag = null;
283
+ }
284
+ refreshArrayControls();
285
+ }
286
+ function itemUnderPointer(arrayId, x, y, exclude) {
287
+ for (const el of orderedItemsFor(arrayId)) {
288
+ if (el === exclude) continue;
289
+ const r = el.getBoundingClientRect();
290
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;
291
+ }
292
+ return null;
293
+ }
294
+ function addDraftPill(arrayId) {
295
+ const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];
296
+ if (!template?.parentElement) return;
297
+ const pill = template.cloneNode(false);
298
+ for (const attr of [...pill.attributes]) if (attr.name.startsWith("data-upstart-") || attr.name === "contenteditable") pill.removeAttribute(attr.name);
299
+ pill.removeAttribute("style");
300
+ pill.setAttribute(DRAFT_ATTR, arrayId);
301
+ pill.setAttribute("contenteditable", "true");
302
+ pill.textContent = DEFAULT_NEW_ITEM;
303
+ const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();
304
+ if (last && last.parentElement === template.parentElement) last.after(pill);
305
+ else template.parentElement.appendChild(pill);
306
+ pill.focus();
307
+ const range = document.createRange();
308
+ range.selectNodeContents(pill);
309
+ const sel = window.getSelection();
310
+ sel?.removeAllRanges();
311
+ sel?.addRange(range);
312
+ refreshArrayControls();
313
+ }
314
+ function applyArray(arrayId) {
315
+ const deletes = pendingDeletes.get(arrayId) ?? /* @__PURE__ */ new Set();
316
+ const items = [];
317
+ for (const el of orderedItemsFor(arrayId)) {
318
+ if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;
319
+ items.push(textOf(el));
320
+ }
321
+ if (items.length === 0) return;
322
+ pendingDeletes.delete(arrayId);
323
+ sendToParent({
324
+ type: "text-edit",
325
+ payload: {
326
+ action: "arraySet",
327
+ arrayId,
328
+ items
329
+ }
330
+ });
331
+ }
332
+ function scheduleRefresh() {
333
+ if (rafId !== null) return;
334
+ rafId = requestAnimationFrame(() => {
335
+ rafId = null;
336
+ refreshArrayControls();
337
+ });
338
+ }
339
+ //#endregion
340
+ export { hideArrayControls, initArrayControls, refreshArrayControls };
341
+
342
+ //# sourceMappingURL=array-controls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"array-controls.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/array-controls.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\n// Editor-only controls for inline-array .map() lists. Add/delete are staged LOCALLY\n// (no server round-trip per action): \"×\" marks an item for removal, \"+\" inserts an\n// editable draft pill, and a green \"✓\" below the \"+\" commits the whole batch at once\n// via a single `arraySet` edit + rebuild. Per-item text editing stays live; arraySet\n// reads the current item texts from the DOM so live edits are preserved.\n\nconst DEFAULT_NEW_ITEM = \"New item\";\nconst DRAFT_ATTR = \"data-upstart-draft-item\";\n\n// Layout of the \"+\"/\"✓\" stack placed to the right of a list.\nconst BTN_SIZE = 22;\nconst STACK_GAP = 6; // vertical gap between \"+\" and \"✓\"\nconst BUTTON_MARGIN = 12; // left margin between the item and the buttons\n\nlet layer: HTMLDivElement | null = null;\nlet observer: MutationObserver | null = null;\nlet resizeObserver: ResizeObserver | null = null;\nlet rafId: number | null = null;\nlet isInitialized = false;\nlet isMutatingDom = false;\n\n// Items observed for size changes (e.g. text edits widen a pill) so the \"×\" stays\n// anchored to the item's corner. WeakSet keeps observe() calls idempotent.\nconst observedForResize = new WeakSet<Element>();\n\n// arrayId -> set of original item indices currently marked for deletion.\nconst pendingDeletes = new Map<string, Set<number>>();\n\n// In-progress drag (reorder). Tracked at document level so it survives overlay\n// rebuilds; the overlay is frozen while a drag is active.\nlet drag: { arrayId: string; node: HTMLElement } | null = null;\n\n// The list item currently hovered — its per-item \"×\"/\"⠿\" controls are the only\n// ones shown (others stay hidden until hovered). buttonOwner lets a button keep its\n// item \"hovered\" while the pointer sits on the button (which overlays the corner).\nlet hoveredItem: HTMLElement | null = null;\nconst buttonOwner = new WeakMap<HTMLElement, HTMLElement>();\n\nexport function initArrayControls(): void {\n if (typeof document === \"undefined\" || isInitialized) return;\n isInitialized = true;\n\n window.addEventListener(\"scroll\", scheduleRefresh, { passive: true, capture: true });\n window.addEventListener(\"resize\", scheduleRefresh, { passive: true });\n document.addEventListener(\"mouseover\", onHoverMove, { passive: true });\n\n observer = new MutationObserver((mutations) => {\n if (isMutatingDom) return;\n for (const m of mutations) {\n const t = m.target as Node;\n if (layer && (t === layer || layer.contains(t))) continue;\n scheduleRefresh();\n return;\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n if (typeof ResizeObserver !== \"undefined\") {\n resizeObserver = new ResizeObserver(() => scheduleRefresh());\n }\n}\n\n// Append a per-item button and record which item it belongs to, so hovering the\n// button (which overlays the item's corner) keeps that item considered hovered.\nfunction appendOwned(root: HTMLElement, btn: HTMLElement, owner: HTMLElement): void {\n buttonOwner.set(btn, owner);\n root.appendChild(btn);\n}\n\n// Track the hovered list item; refresh when it changes so its controls toggle.\nfunction onHoverMove(e: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") return;\n const owner = hoverOwner(e.target as HTMLElement | null);\n if (owner !== hoveredItem) {\n hoveredItem = owner;\n scheduleRefresh();\n }\n}\n\n// The list item under the pointer: either the item itself (or a descendant), or a\n// per-item control button that belongs to one.\nfunction hoverOwner(target: HTMLElement | null): HTMLElement | null {\n if (!target) return null;\n const item = target.closest<HTMLElement>(`[data-upstart-array-id]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}]`);\n if (item) return item;\n if (layer?.contains(target)) {\n const owner = buttonOwner.get(target);\n if (owner && document.contains(owner)) return owner;\n }\n return null;\n}\n\n// Observe an item once so the overlay re-anchors when the item resizes.\nfunction observeResize(el: Element): void {\n if (resizeObserver && !observedForResize.has(el)) {\n observedForResize.add(el);\n resizeObserver.observe(el);\n }\n}\n\nexport function refreshArrayControls(): void {\n if (typeof document === \"undefined\") return;\n\n if (getCurrentMode() !== \"edit\") {\n hideArrayControls();\n return;\n }\n\n // While dragging we move list nodes live; keep the overlay frozen and rebuild it\n // once on drop (the drag is driven by document-level listeners, not the overlay).\n if (drag) return;\n\n isMutatingDom = true;\n try {\n const root = ensureLayer();\n root.replaceChildren();\n\n // Group original (non-draft) items by array id, ordered by loop index.\n const groups = new Map<string, HTMLElement[]>();\n for (const el of document.querySelectorAll<HTMLElement>(\"[data-upstart-array-id]\")) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const id = el.dataset.upstartArrayId;\n if (!id) continue;\n (groups.get(id) ?? groups.set(id, []).get(id)!).push(el);\n }\n\n for (const [arrayId, items] of groups) {\n items.sort((a, b) => itemIndex(a) - itemIndex(b));\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const drafts = draftPillsFor(arrayId);\n const ordered = orderedItemsFor(arrayId);\n // Reordering only makes sense with more than one item.\n const draggable = ordered.length > 1;\n\n // Original items: per-item controls show only on hover; a marked item always\n // keeps its restore \"×\" (it has pointer-events:none, so it can't be hovered).\n for (const item of items) {\n observeResize(item);\n const idx = itemIndex(item);\n const marked = deletes.has(idx);\n applyDeletionStyle(item, marked);\n if (!marked && item !== hoveredItem) continue;\n const rect = item.getBoundingClientRect();\n if (draggable && !marked) appendOwned(root, makeDragHandle(arrayId, item, rect), item);\n appendOwned(root, makeItemDeleteButton(arrayId, idx, marked, rect), item);\n }\n\n // Draft (added) pills: controls show on hover; each has its own \"×\".\n for (const pill of drafts) {\n observeResize(pill);\n if (pill !== hoveredItem) continue;\n const rect = pill.getBoundingClientRect();\n if (draggable) appendOwned(root, makeDragHandle(arrayId, pill, rect), pill);\n appendOwned(root, makeDraftRemoveButton(pill, rect), pill);\n }\n\n // \"+\" (and \"✓\" when there are pending changes) after the last item in DOM\n // order, as a vertically-centred stack with a small left margin.\n const anchor = ordered[ordered.length - 1];\n if (!anchor) continue;\n const a = anchor.getBoundingClientRect();\n\n const finalCount = items.length - deletes.size + drafts.length;\n const dirty = deletes.size > 0 || drafts.length > 0 || isReordered(arrayId);\n const showApply = dirty && finalCount >= 1;\n\n const stackHeight = showApply ? BTN_SIZE * 2 + STACK_GAP : BTN_SIZE;\n const left = a.right + window.scrollX + BUTTON_MARGIN;\n const top = a.top + window.scrollY + (a.height - stackHeight) / 2;\n\n root.appendChild(makeAddButton(arrayId, top, left));\n if (showApply) {\n root.appendChild(makeApplyButton(arrayId, top + BTN_SIZE + STACK_GAP, left));\n }\n }\n } finally {\n isMutatingDom = false;\n }\n}\n\nexport function hideArrayControls(): void {\n if (layer) layer.replaceChildren();\n}\n\n// ---------------------------------------------------------------------------\n// DOM helpers\n// ---------------------------------------------------------------------------\n\nfunction ensureLayer(): HTMLDivElement {\n if (layer) return layer;\n layer = document.createElement(\"div\");\n layer.id = \"upstart-array-controls\";\n layer.style.cssText =\n \"position: absolute; top: 0; left: 0; width: 0; height: 0; \" +\n \"pointer-events: none; z-index: 2147483646;\";\n document.body.appendChild(layer);\n return layer;\n}\n\nfunction itemIndex(el: HTMLElement): number {\n const n = el.dataset.upstartArrayIndex ? Number(el.dataset.upstartArrayIndex) : Number.NaN;\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction originalItemsFor(arrayId: string): HTMLElement[] {\n return [\n ...document.querySelectorAll<HTMLElement>(\n `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}])`,\n ),\n ].sort((a, b) => itemIndex(a) - itemIndex(b));\n}\n\nfunction draftPillsFor(arrayId: string): HTMLElement[] {\n return [...document.querySelectorAll<HTMLElement>(`[${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`)];\n}\n\n// All items of an array (originals + drafts) in DOM order — this is the order the\n// list is rendered in and the order applied to the source on ✓.\nfunction orderedItemsFor(arrayId: string): HTMLElement[] {\n const sel = `[data-upstart-array-id=\"${cssAttr(arrayId)}\"]:not([${DRAFT_ATTR}]), [${DRAFT_ATTR}=\"${cssAttr(arrayId)}\"]`;\n return [...document.querySelectorAll<HTMLElement>(sel)];\n}\n\n// True when the original items no longer appear in their source order (i.e. the\n// user has dragged at least one item), so the ✓ apply button should be offered.\nfunction isReordered(arrayId: string): boolean {\n let prev = -1;\n for (const el of orderedItemsFor(arrayId)) {\n if (el.hasAttribute(DRAFT_ATTR)) continue;\n const idx = itemIndex(el);\n if (idx < prev) return true;\n prev = idx;\n }\n return false;\n}\n\nfunction cssAttr(value: string): string {\n return value.replace(/[\"\\\\]/g, \"\\\\$&\");\n}\n\nfunction textOf(el: HTMLElement): string {\n return (el.textContent ?? \"\").trim();\n}\n\nfunction applyDeletionStyle(item: HTMLElement, marked: boolean): void {\n if (marked) {\n item.style.opacity = \"0.4\";\n item.style.textDecoration = \"line-through\";\n item.style.pointerEvents = \"none\";\n } else {\n item.style.removeProperty(\"opacity\");\n item.style.removeProperty(\"text-decoration\");\n item.style.removeProperty(\"pointer-events\");\n }\n}\n\n// ---------------------------------------------------------------------------\n// Buttons\n// ---------------------------------------------------------------------------\n\n// Geometric SVG icons — centred by the flex container + viewBox, so they don't\n// depend on the host page's font metrics (which left text glyphs mis-aligned).\nconst ICON_PATHS: Record<string, string> = {\n plus: '<path d=\"M12 5v14M5 12h14\"/>',\n cross: '<path d=\"M6 6 18 18M18 6 6 18\"/>',\n check: '<path d=\"M5 12.5 10 17.5 19 7\"/>',\n restore: '<path d=\"M3 4v5h5\"/><path d=\"M3.5 9a8.5 8.5 0 1 0 2.2-3.8L3 9\"/>',\n 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\"/>',\n};\n\nfunction setIcon(btn: HTMLElement, name: keyof typeof ICON_PATHS, px: number): void {\n const dots = name === \"grip\";\n btn.innerHTML =\n `<svg width=\"${px}\" height=\"${px}\" viewBox=\"0 0 24 24\" ` +\n `fill=\"${dots ? \"currentColor\" : \"none\"}\" stroke=\"${dots ? \"none\" : \"currentColor\"}\" ` +\n 'stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\" ' +\n `style=\"display:block;pointer-events:none\">${ICON_PATHS[name]}</svg>`;\n}\n\nfunction baseButton(top: number, left: number, size: number, bg: string): HTMLButtonElement {\n const btn = document.createElement(\"button\");\n btn.type = \"button\";\n btn.style.cssText =\n `position: absolute; top: ${top}px; left: ${left}px; width: ${size}px; height: ${size}px; ` +\n \"padding: 0; display: flex; align-items: center; justify-content: center; \" +\n `background: ${bg}; color: #fff; border: 1px solid #fff; border-radius: 50%; ` +\n \"line-height: 1; cursor: pointer; pointer-events: auto; box-shadow: 0 1px 3px rgba(0,0,0,0.3);\";\n return btn;\n}\n\nfunction makeItemDeleteButton(\n arrayId: string,\n index: number,\n marked: boolean,\n rect: DOMRect,\n): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, marked ? \"restore\" : \"cross\", 10);\n btn.title = marked ? \"Keep item\" : \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n const set = pendingDeletes.get(arrayId) ?? new Set<number>();\n if (set.has(index)) set.delete(index);\n else set.add(index);\n if (set.size) pendingDeletes.set(arrayId, set);\n else pendingDeletes.delete(arrayId);\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeDraftRemoveButton(pill: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const btn = baseButton(rect.top + window.scrollY - 8, rect.right + window.scrollX - 8, 16, \"#ef4444\");\n setIcon(btn, \"cross\", 10);\n btn.title = \"Remove item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n pill.remove();\n refreshArrayControls();\n });\n return btn;\n}\n\nfunction makeAddButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#7270c6\");\n setIcon(btn, \"plus\", 14);\n btn.title = \"Add item\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n addDraftPill(arrayId);\n });\n return btn;\n}\n\nfunction makeApplyButton(arrayId: string, top: number, left: number): HTMLButtonElement {\n const btn = baseButton(top, left, 22, \"#16a34a\");\n setIcon(btn, \"check\", 13);\n btn.title = \"Apply changes\";\n btn.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n applyArray(arrayId);\n });\n return btn;\n}\n\n// Grip centred on the item's top edge (away from the neighbour's \"×\" corners);\n// press and drag to reorder.\nfunction makeDragHandle(arrayId: string, node: HTMLElement, rect: DOMRect): HTMLButtonElement {\n const top = rect.top + window.scrollY - 8;\n const left = rect.left + window.scrollX + rect.width / 2 - 8;\n const btn = baseButton(top, left, 16, \"#6b7280\");\n setIcon(btn, \"grip\", 12);\n btn.title = \"Drag to reorder\";\n btn.style.cursor = \"grab\";\n btn.addEventListener(\"pointerdown\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n startDrag(arrayId, node);\n });\n return btn;\n}\n\n// ---------------------------------------------------------------------------\n// Drag to reorder\n// ---------------------------------------------------------------------------\n\nfunction startDrag(arrayId: string, node: HTMLElement): void {\n drag = { arrayId, node };\n node.style.opacity = \"0.5\";\n document.body.style.userSelect = \"none\";\n // Drive the drag from document level so it survives overlay rebuilds.\n document.addEventListener(\"pointermove\", onDragMove);\n document.addEventListener(\"pointerup\", onDragEnd, { once: true });\n}\n\nfunction onDragMove(e: PointerEvent): void {\n if (!drag) return;\n const target = itemUnderPointer(drag.arrayId, e.clientX, e.clientY, drag.node);\n if (!target) return;\n const r = target.getBoundingClientRect();\n // Insert before/after based on the pointer vs the target's horizontal midpoint.\n if (e.clientX > r.left + r.width / 2) target.after(drag.node);\n else target.before(drag.node);\n}\n\nfunction onDragEnd(): void {\n document.removeEventListener(\"pointermove\", onDragMove);\n document.body.style.removeProperty(\"user-select\");\n if (drag) {\n drag.node.style.removeProperty(\"opacity\");\n drag = null;\n }\n refreshArrayControls();\n}\n\n// The array item (original or draft) whose box contains the pointer, excluding the\n// node being dragged.\nfunction itemUnderPointer(arrayId: string, x: number, y: number, exclude: HTMLElement): HTMLElement | null {\n for (const el of orderedItemsFor(arrayId)) {\n if (el === exclude) continue;\n const r = el.getBoundingClientRect();\n if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return el;\n }\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Actions\n// ---------------------------------------------------------------------------\n\nfunction addDraftPill(arrayId: string): void {\n const template = originalItemsFor(arrayId)[0] ?? draftPillsFor(arrayId)[0];\n if (!template?.parentElement) return;\n\n const pill = template.cloneNode(false) as HTMLElement;\n // Clone keeps the pill styling (class); strip identity/editor attrs so it is not\n // picked up by the text editor, click handler, hover overlay or the array query.\n for (const attr of [...pill.attributes]) {\n if (attr.name.startsWith(\"data-upstart-\") || attr.name === \"contenteditable\") {\n pill.removeAttribute(attr.name);\n }\n }\n pill.removeAttribute(\"style\");\n pill.setAttribute(DRAFT_ATTR, arrayId);\n pill.setAttribute(\"contenteditable\", \"true\");\n pill.textContent = DEFAULT_NEW_ITEM;\n\n // Insert after the current last pill of this array (drafts go at the end).\n const last = draftPillsFor(arrayId).pop() ?? originalItemsFor(arrayId).pop();\n if (last && last.parentElement === template.parentElement) {\n last.after(pill);\n } else {\n template.parentElement.appendChild(pill);\n }\n\n // Focus + select so the user can type the name immediately.\n pill.focus();\n const range = document.createRange();\n range.selectNodeContents(pill);\n const sel = window.getSelection();\n sel?.removeAllRanges();\n sel?.addRange(range);\n\n refreshArrayControls();\n}\n\nfunction applyArray(arrayId: string): void {\n const deletes = pendingDeletes.get(arrayId) ?? new Set<number>();\n const items: string[] = [];\n\n // DOM order = the order the user sees (incl. any drag reordering).\n for (const el of orderedItemsFor(arrayId)) {\n if (!el.hasAttribute(DRAFT_ATTR) && deletes.has(itemIndex(el))) continue;\n items.push(textOf(el));\n }\n\n if (items.length === 0) return;\n\n // The server rewrites the whole array, rebuilds + swaps, then the editor reloads\n // the iframe — which discards the local draft state, so just clear our marks.\n pendingDeletes.delete(arrayId);\n sendToParent({ type: \"text-edit\", payload: { action: \"arraySet\", arrayId, items } });\n}\n\nfunction scheduleRefresh(): void {\n if (rafId !== null) return;\n rafId = requestAnimationFrame(() => {\n rafId = null;\n refreshArrayControls();\n });\n}\n"],"mappings":";;;AASA,MAAM,mBAAmB;AACzB,MAAM,aAAa;AAGnB,MAAM,WAAW;AACjB,MAAM,YAAY;AAClB,MAAM,gBAAgB;AAEtB,IAAI,QAA+B;AACnC,IAAI,WAAoC;AACxC,IAAI,iBAAwC;AAC5C,IAAI,QAAuB;AAC3B,IAAI,gBAAgB;AACpB,IAAI,gBAAgB;AAIpB,MAAM,oCAAoB,IAAI,SAAkB;AAGhD,MAAM,iCAAiB,IAAI,KAA0B;AAIrD,IAAI,OAAsD;AAK1D,IAAI,cAAkC;AACtC,MAAM,8BAAc,IAAI,SAAmC;AAE3D,SAAgB,oBAA0B;CACxC,IAAI,OAAO,aAAa,eAAe,eAAe;CACtD,gBAAgB;CAEhB,OAAO,iBAAiB,UAAU,iBAAiB;EAAE,SAAS;EAAM,SAAS;EAAM,CAAC;CACpF,OAAO,iBAAiB,UAAU,iBAAiB,EAAE,SAAS,MAAM,CAAC;CACrE,SAAS,iBAAiB,aAAa,aAAa,EAAE,SAAS,MAAM,CAAC;CAEtE,WAAW,IAAI,kBAAkB,cAAc;EAC7C,IAAI,eAAe;EACnB,KAAK,MAAM,KAAK,WAAW;GACzB,MAAM,IAAI,EAAE;GACZ,IAAI,UAAU,MAAM,SAAS,MAAM,SAAS,EAAE,GAAG;GACjD,iBAAiB;GACjB;;GAEF;CACF,SAAS,QAAQ,SAAS,MAAM;EAAE,WAAW;EAAM,SAAS;EAAM,CAAC;CAEnE,IAAI,OAAO,mBAAmB,aAC5B,iBAAiB,IAAI,qBAAqB,iBAAiB,CAAC;;AAMhE,SAAS,YAAY,MAAmB,KAAkB,OAA0B;CAClF,YAAY,IAAI,KAAK,MAAM;CAC3B,KAAK,YAAY,IAAI;;AAIvB,SAAS,YAAY,GAAqB;CACxC,IAAI,gBAAgB,KAAK,QAAQ;CACjC,MAAM,QAAQ,WAAW,EAAE,OAA6B;CACxD,IAAI,UAAU,aAAa;EACzB,cAAc;EACd,iBAAiB;;;AAMrB,SAAS,WAAW,QAAgD;CAClE,IAAI,CAAC,QAAQ,OAAO;CACpB,MAAM,OAAO,OAAO,QAAqB,gCAAgC,WAAW,OAAO,WAAW,GAAG;CACzG,IAAI,MAAM,OAAO;CACjB,IAAI,OAAO,SAAS,OAAO,EAAE;EAC3B,MAAM,QAAQ,YAAY,IAAI,OAAO;EACrC,IAAI,SAAS,SAAS,SAAS,MAAM,EAAE,OAAO;;CAEhD,OAAO;;AAIT,SAAS,cAAc,IAAmB;CACxC,IAAI,kBAAkB,CAAC,kBAAkB,IAAI,GAAG,EAAE;EAChD,kBAAkB,IAAI,GAAG;EACzB,eAAe,QAAQ,GAAG;;;AAI9B,SAAgB,uBAA6B;CAC3C,IAAI,OAAO,aAAa,aAAa;CAErC,IAAI,gBAAgB,KAAK,QAAQ;EAC/B,mBAAmB;EACnB;;CAKF,IAAI,MAAM;CAEV,gBAAgB;CAChB,IAAI;EACF,MAAM,OAAO,aAAa;EAC1B,KAAK,iBAAiB;EAGtB,MAAM,yBAAS,IAAI,KAA4B;EAC/C,KAAK,MAAM,MAAM,SAAS,iBAA8B,0BAA0B,EAAE;GAClF,IAAI,GAAG,aAAa,WAAW,EAAE;GACjC,MAAM,KAAK,GAAG,QAAQ;GACtB,IAAI,CAAC,IAAI;GACT,CAAC,OAAO,IAAI,GAAG,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,GAAG,EAAG,KAAK,GAAG;;EAG1D,KAAK,MAAM,CAAC,SAAS,UAAU,QAAQ;GACrC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,GAAG,UAAU,EAAE,CAAC;GACjD,MAAM,UAAU,eAAe,IAAI,QAAQ,oBAAI,IAAI,KAAa;GAChE,MAAM,SAAS,cAAc,QAAQ;GACrC,MAAM,UAAU,gBAAgB,QAAQ;GAExC,MAAM,YAAY,QAAQ,SAAS;GAInC,KAAK,MAAM,QAAQ,OAAO;IACxB,cAAc,KAAK;IACnB,MAAM,MAAM,UAAU,KAAK;IAC3B,MAAM,SAAS,QAAQ,IAAI,IAAI;IAC/B,mBAAmB,MAAM,OAAO;IAChC,IAAI,CAAC,UAAU,SAAS,aAAa;IACrC,MAAM,OAAO,KAAK,uBAAuB;IACzC,IAAI,aAAa,CAAC,QAAQ,YAAY,MAAM,eAAe,SAAS,MAAM,KAAK,EAAE,KAAK;IACtF,YAAY,MAAM,qBAAqB,SAAS,KAAK,QAAQ,KAAK,EAAE,KAAK;;GAI3E,KAAK,MAAM,QAAQ,QAAQ;IACzB,cAAc,KAAK;IACnB,IAAI,SAAS,aAAa;IAC1B,MAAM,OAAO,KAAK,uBAAuB;IACzC,IAAI,WAAW,YAAY,MAAM,eAAe,SAAS,MAAM,KAAK,EAAE,KAAK;IAC3E,YAAY,MAAM,sBAAsB,MAAM,KAAK,EAAE,KAAK;;GAK5D,MAAM,SAAS,QAAQ,QAAQ,SAAS;GACxC,IAAI,CAAC,QAAQ;GACb,MAAM,IAAI,OAAO,uBAAuB;GAExC,MAAM,aAAa,MAAM,SAAS,QAAQ,OAAO,OAAO;GAExD,MAAM,aADQ,QAAQ,OAAO,KAAK,OAAO,SAAS,KAAK,YAAY,QAAQ,KAChD,cAAc;GAEzC,MAAM,cAAc,YAAY,WAAW,IAAI,YAAY;GAC3D,MAAM,OAAO,EAAE,QAAQ,OAAO,UAAU;GACxC,MAAM,MAAM,EAAE,MAAM,OAAO,WAAW,EAAE,SAAS,eAAe;GAEhE,KAAK,YAAY,cAAc,SAAS,KAAK,KAAK,CAAC;GACnD,IAAI,WACF,KAAK,YAAY,gBAAgB,SAAS,MAAM,WAAW,WAAW,KAAK,CAAC;;WAGxE;EACR,gBAAgB;;;AAIpB,SAAgB,oBAA0B;CACxC,IAAI,OAAO,MAAM,iBAAiB;;AAOpC,SAAS,cAA8B;CACrC,IAAI,OAAO,OAAO;CAClB,QAAQ,SAAS,cAAc,MAAM;CACrC,MAAM,KAAK;CACX,MAAM,MAAM,UACV;CAEF,SAAS,KAAK,YAAY,MAAM;CAChC,OAAO;;AAGT,SAAS,UAAU,IAAyB;CAC1C,MAAM,IAAI,GAAG,QAAQ,oBAAoB,OAAO,GAAG,QAAQ,kBAAkB,GAAG;CAChF,OAAO,OAAO,SAAS,EAAE,GAAG,IAAI;;AAGlC,SAAS,iBAAiB,SAAgC;CACxD,OAAO,CACL,GAAG,SAAS,iBACV,2BAA2B,QAAQ,QAAQ,CAAC,UAAU,WAAW,IAClE,CACF,CAAC,MAAM,GAAG,MAAM,UAAU,EAAE,GAAG,UAAU,EAAE,CAAC;;AAG/C,SAAS,cAAc,SAAgC;CACrD,OAAO,CAAC,GAAG,SAAS,iBAA8B,IAAI,WAAW,IAAI,QAAQ,QAAQ,CAAC,IAAI,CAAC;;AAK7F,SAAS,gBAAgB,SAAgC;CACvD,MAAM,MAAM,2BAA2B,QAAQ,QAAQ,CAAC,UAAU,WAAW,OAAO,WAAW,IAAI,QAAQ,QAAQ,CAAC;CACpH,OAAO,CAAC,GAAG,SAAS,iBAA8B,IAAI,CAAC;;AAKzD,SAAS,YAAY,SAA0B;CAC7C,IAAI,OAAO;CACX,KAAK,MAAM,MAAM,gBAAgB,QAAQ,EAAE;EACzC,IAAI,GAAG,aAAa,WAAW,EAAE;EACjC,MAAM,MAAM,UAAU,GAAG;EACzB,IAAI,MAAM,MAAM,OAAO;EACvB,OAAO;;CAET,OAAO;;AAGT,SAAS,QAAQ,OAAuB;CACtC,OAAO,MAAM,QAAQ,UAAU,OAAO;;AAGxC,SAAS,OAAO,IAAyB;CACvC,QAAQ,GAAG,eAAe,IAAI,MAAM;;AAGtC,SAAS,mBAAmB,MAAmB,QAAuB;CACpE,IAAI,QAAQ;EACV,KAAK,MAAM,UAAU;EACrB,KAAK,MAAM,iBAAiB;EAC5B,KAAK,MAAM,gBAAgB;QACtB;EACL,KAAK,MAAM,eAAe,UAAU;EACpC,KAAK,MAAM,eAAe,kBAAkB;EAC5C,KAAK,MAAM,eAAe,iBAAiB;;;AAU/C,MAAM,aAAqC;CACzC,MAAM;CACN,OAAO;CACP,OAAO;CACP,SAAS;CACT,MAAM;CACP;AAED,SAAS,QAAQ,KAAkB,MAA+B,IAAkB;CAClF,MAAM,OAAO,SAAS;CACtB,IAAI,YACF,eAAe,GAAG,YAAY,GAAG,8BACxB,OAAO,iBAAiB,OAAO,YAAY,OAAO,SAAS,eAAe,gHAEtC,WAAW,MAAM;;AAGlE,SAAS,WAAW,KAAa,MAAc,MAAc,IAA+B;CAC1F,MAAM,MAAM,SAAS,cAAc,SAAS;CAC5C,IAAI,OAAO;CACX,IAAI,MAAM,UACR,4BAA4B,IAAI,YAAY,KAAK,aAAa,KAAK,cAAc,KAAK,2FAEvE,GAAG;CAEpB,OAAO;;AAGT,SAAS,qBACP,SACA,OACA,QACA,MACmB;CACnB,MAAM,MAAM,WAAW,KAAK,MAAM,OAAO,UAAU,GAAG,KAAK,QAAQ,OAAO,UAAU,GAAG,IAAI,UAAU;CACrG,QAAQ,KAAK,SAAS,YAAY,SAAS,GAAG;CAC9C,IAAI,QAAQ,SAAS,cAAc;CACnC,IAAI,iBAAiB,UAAU,MAAM;EACnC,EAAE,gBAAgB;EAClB,EAAE,iBAAiB;EACnB,MAAM,MAAM,eAAe,IAAI,QAAQ,oBAAI,IAAI,KAAa;EAC5D,IAAI,IAAI,IAAI,MAAM,EAAE,IAAI,OAAO,MAAM;OAChC,IAAI,IAAI,MAAM;EACnB,IAAI,IAAI,MAAM,eAAe,IAAI,SAAS,IAAI;OACzC,eAAe,OAAO,QAAQ;EACnC,sBAAsB;GACtB;CACF,OAAO;;AAGT,SAAS,sBAAsB,MAAmB,MAAkC;CAClF,MAAM,MAAM,WAAW,KAAK,MAAM,OAAO,UAAU,GAAG,KAAK,QAAQ,OAAO,UAAU,GAAG,IAAI,UAAU;CACrG,QAAQ,KAAK,SAAS,GAAG;CACzB,IAAI,QAAQ;CACZ,IAAI,iBAAiB,UAAU,MAAM;EACnC,EAAE,gBAAgB;EAClB,EAAE,iBAAiB;EACnB,KAAK,QAAQ;EACb,sBAAsB;GACtB;CACF,OAAO;;AAGT,SAAS,cAAc,SAAiB,KAAa,MAAiC;CACpF,MAAM,MAAM,WAAW,KAAK,MAAM,IAAI,UAAU;CAChD,QAAQ,KAAK,QAAQ,GAAG;CACxB,IAAI,QAAQ;CACZ,IAAI,iBAAiB,UAAU,MAAM;EACnC,EAAE,gBAAgB;EAClB,EAAE,iBAAiB;EACnB,aAAa,QAAQ;GACrB;CACF,OAAO;;AAGT,SAAS,gBAAgB,SAAiB,KAAa,MAAiC;CACtF,MAAM,MAAM,WAAW,KAAK,MAAM,IAAI,UAAU;CAChD,QAAQ,KAAK,SAAS,GAAG;CACzB,IAAI,QAAQ;CACZ,IAAI,iBAAiB,UAAU,MAAM;EACnC,EAAE,gBAAgB;EAClB,EAAE,iBAAiB;EACnB,WAAW,QAAQ;GACnB;CACF,OAAO;;AAKT,SAAS,eAAe,SAAiB,MAAmB,MAAkC;CAG5F,MAAM,MAAM,WAFA,KAAK,MAAM,OAAO,UAAU,GAC3B,KAAK,OAAO,OAAO,UAAU,KAAK,QAAQ,IAAI,GACzB,IAAI,UAAU;CAChD,QAAQ,KAAK,QAAQ,GAAG;CACxB,IAAI,QAAQ;CACZ,IAAI,MAAM,SAAS;CACnB,IAAI,iBAAiB,gBAAgB,MAAM;EACzC,EAAE,gBAAgB;EAClB,EAAE,iBAAiB;EACnB,UAAU,SAAS,KAAK;GACxB;CACF,OAAO;;AAOT,SAAS,UAAU,SAAiB,MAAyB;CAC3D,OAAO;EAAE;EAAS;EAAM;CACxB,KAAK,MAAM,UAAU;CACrB,SAAS,KAAK,MAAM,aAAa;CAEjC,SAAS,iBAAiB,eAAe,WAAW;CACpD,SAAS,iBAAiB,aAAa,WAAW,EAAE,MAAM,MAAM,CAAC;;AAGnE,SAAS,WAAW,GAAuB;CACzC,IAAI,CAAC,MAAM;CACX,MAAM,SAAS,iBAAiB,KAAK,SAAS,EAAE,SAAS,EAAE,SAAS,KAAK,KAAK;CAC9E,IAAI,CAAC,QAAQ;CACb,MAAM,IAAI,OAAO,uBAAuB;CAExC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,GAAG,OAAO,MAAM,KAAK,KAAK;MACxD,OAAO,OAAO,KAAK,KAAK;;AAG/B,SAAS,YAAkB;CACzB,SAAS,oBAAoB,eAAe,WAAW;CACvD,SAAS,KAAK,MAAM,eAAe,cAAc;CACjD,IAAI,MAAM;EACR,KAAK,KAAK,MAAM,eAAe,UAAU;EACzC,OAAO;;CAET,sBAAsB;;AAKxB,SAAS,iBAAiB,SAAiB,GAAW,GAAW,SAA0C;CACzG,KAAK,MAAM,MAAM,gBAAgB,QAAQ,EAAE;EACzC,IAAI,OAAO,SAAS;EACpB,MAAM,IAAI,GAAG,uBAAuB;EACpC,IAAI,KAAK,EAAE,QAAQ,KAAK,EAAE,SAAS,KAAK,EAAE,OAAO,KAAK,EAAE,QAAQ,OAAO;;CAEzE,OAAO;;AAOT,SAAS,aAAa,SAAuB;CAC3C,MAAM,WAAW,iBAAiB,QAAQ,CAAC,MAAM,cAAc,QAAQ,CAAC;CACxE,IAAI,CAAC,UAAU,eAAe;CAE9B,MAAM,OAAO,SAAS,UAAU,MAAM;CAGtC,KAAK,MAAM,QAAQ,CAAC,GAAG,KAAK,WAAW,EACrC,IAAI,KAAK,KAAK,WAAW,gBAAgB,IAAI,KAAK,SAAS,mBACzD,KAAK,gBAAgB,KAAK,KAAK;CAGnC,KAAK,gBAAgB,QAAQ;CAC7B,KAAK,aAAa,YAAY,QAAQ;CACtC,KAAK,aAAa,mBAAmB,OAAO;CAC5C,KAAK,cAAc;CAGnB,MAAM,OAAO,cAAc,QAAQ,CAAC,KAAK,IAAI,iBAAiB,QAAQ,CAAC,KAAK;CAC5E,IAAI,QAAQ,KAAK,kBAAkB,SAAS,eAC1C,KAAK,MAAM,KAAK;MAEhB,SAAS,cAAc,YAAY,KAAK;CAI1C,KAAK,OAAO;CACZ,MAAM,QAAQ,SAAS,aAAa;CACpC,MAAM,mBAAmB,KAAK;CAC9B,MAAM,MAAM,OAAO,cAAc;CACjC,KAAK,iBAAiB;CACtB,KAAK,SAAS,MAAM;CAEpB,sBAAsB;;AAGxB,SAAS,WAAW,SAAuB;CACzC,MAAM,UAAU,eAAe,IAAI,QAAQ,oBAAI,IAAI,KAAa;CAChE,MAAM,QAAkB,EAAE;CAG1B,KAAK,MAAM,MAAM,gBAAgB,QAAQ,EAAE;EACzC,IAAI,CAAC,GAAG,aAAa,WAAW,IAAI,QAAQ,IAAI,UAAU,GAAG,CAAC,EAAE;EAChE,MAAM,KAAK,OAAO,GAAG,CAAC;;CAGxB,IAAI,MAAM,WAAW,GAAG;CAIxB,eAAe,OAAO,QAAQ;CAC9B,aAAa;EAAE,MAAM;EAAa,SAAS;GAAE,QAAQ;GAAY;GAAS;GAAO;EAAE,CAAC;;AAGtF,SAAS,kBAAwB;CAC/B,IAAI,UAAU,MAAM;CACpB,QAAQ,4BAA4B;EAClC,QAAQ;EACR,sBAAsB;GACtB"}
@@ -1 +1 @@
1
- {"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;AA+BA;;iBAAgB,gBAAA,CAAA;;;AAiBhB;iBAAgB,mBAAA,CAAA"}
1
+ {"version":3,"file":"click-handler.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"mappings":";;AAkEA;;iBAAgB,gBAAA,CAAA;;;AAiBhB;iBAAgB,mBAAA,CAAA"}
@@ -25,6 +25,30 @@ const DAISY_VAR_NAMES = [
25
25
  "error-content"
26
26
  ];
27
27
  /**
28
+ * Collect editable images (elements carrying data-upstart-image-id) from the
29
+ * clicked element's nearest subtree. Starting at `start`, we climb ancestors
30
+ * until we find a subtree that contains at least one editable image. This lets
31
+ * us surface a background image (e.g. a hero <img> covered by overlays) even
32
+ * when the click lands on a sibling overlay rather than the image itself.
33
+ */
34
+ function collectImages(start) {
35
+ let el = start;
36
+ for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {
37
+ const found = /* @__PURE__ */ new Set();
38
+ if (el.dataset.upstartImageId) found.add(el);
39
+ for (const node of el.querySelectorAll("[data-upstart-image-id]")) found.add(node);
40
+ if (found.size > 0) return Array.from(found).map((node) => {
41
+ const img = node;
42
+ return {
43
+ id: node.dataset.upstartImageId,
44
+ src: img.currentSrc || img.src || "",
45
+ alt: img.alt || ""
46
+ };
47
+ });
48
+ }
49
+ return [];
50
+ }
51
+ /**
28
52
  * Initialize click handler for className editing.
29
53
  */
30
54
  function initClickHandler() {
@@ -49,6 +73,7 @@ function handleClick(event) {
49
73
  console.warn("[Upstart Editor] Click target is not an HTMLElement");
50
74
  return;
51
75
  }
76
+ if (target.closest("#upstart-array-controls, [data-upstart-draft-item]")) return;
52
77
  if (target.closest(".upstart-editor-bubble-menu")) {
53
78
  console.info("[Upstart Editor] Click ignored: target is inside the bubble menu");
54
79
  return;
@@ -120,7 +145,8 @@ function handleClick(event) {
120
145
  height: 0,
121
146
  right: 0,
122
147
  bottom: 0
123
- }
148
+ },
149
+ images: collectImages(datasourceEl)
124
150
  });
125
151
  return;
126
152
  }
@@ -174,7 +200,8 @@ function handleClick(event) {
174
200
  recordId,
175
201
  themeColors,
176
202
  bounds,
177
- viewportWidth: window.innerWidth
203
+ viewportWidth: window.innerWidth,
204
+ images: collectImages(element)
178
205
  });
179
206
  console.log("[Upstart Editor] Element clicked:", componentName, hash);
180
207
  }
@@ -1 +1 @@
1
- {"version":3,"file":"click-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\nlet isInitialized = false;\n\nconst DAISY_VAR_NAMES = [\n \"base-100\",\n \"base-200\",\n \"base-300\",\n \"base-content\",\n \"primary\",\n \"primary-content\",\n \"secondary\",\n \"secondary-content\",\n \"accent\",\n \"accent-content\",\n \"neutral\",\n \"neutral-content\",\n \"info\",\n \"info-content\",\n \"success\",\n \"success-content\",\n \"warning\",\n \"warning-content\",\n \"error\",\n \"error-content\",\n];\n\n/**\n * Initialize click handler for className editing.\n */\nexport function initClickHandler(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing click handler...\");\n document.addEventListener(\"click\", handleClick, true);\n isInitialized = true;\n}\n\n/**\n * Cleanup click handler.\n */\nexport function cleanupClickHandler(): void {\n document.removeEventListener(\"click\", handleClick, true);\n isInitialized = false;\n}\n\nfunction handleClick(event: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") {\n return;\n }\n\n console.debug(\"[Upstart Editor] Click event:\", event);\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n console.warn(\"[Upstart Editor] Click target is not an HTMLElement\");\n return;\n }\n\n if (target.closest(\".upstart-editor-bubble-menu\")) {\n console.info(\"[Upstart Editor] Click ignored: target is inside the bubble menu\");\n return;\n }\n\n const activeLink = target.closest(\"a[data-upstart-editor-active]\");\n const isFormControlClick = Boolean(\n (target as HTMLElement | null)?.closest(\"input, textarea, select, button\"),\n );\n if (activeLink || !isFormControlClick) {\n event.preventDefault();\n }\n\n // Clicks inside contenteditable (TipTap active) — still notify style panel if applicable\n if (target.closest(\"[contenteditable='true']\")) {\n const editableEl = target.closest<HTMLElement>(\"[data-upstart-editable-text='true']\");\n if (editableEl) {\n let classNameId = editableEl.dataset.upstartClassnameId ?? \"\";\n let currentClassName = editableEl.className;\n if (!classNameId) {\n const parentWithClassname = editableEl.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n if (classNameId) {\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n sendToParent({\n type: \"element-clicked\",\n hash: editableEl.dataset.upstartHash ?? \"\",\n componentName: editableEl.dataset.upstartComponent,\n filePath: editableEl.dataset.upstartFile ?? \"\",\n classNameId,\n currentClassName,\n themeColors,\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n });\n }\n }\n console.info(\"[Upstart Editor] Click inside contenteditable: TipTap handles text editing\");\n return;\n }\n\n // Detect a datasource record under the click. We check this BEFORE the\n // data-upstart-hash gate because a record may be rendered without that\n // attribute (e.g. an <a> from <Link to={...}>) and we still want to open\n // the inline datasource editor instead of letting the browser navigate.\n const datasourceEl = target.closest<HTMLElement>(\"[data-upstart-datasource][data-upstart-record-id]\");\n\n // Find the closest element with data-upstart-hash\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n if (datasourceEl) {\n // Datasource record click without a data-upstart-hash ancestor: stop\n // navigation, surface the datasource event so the editor can open the\n // inline datasource item inspector.\n event.preventDefault();\n event.stopPropagation();\n sendToParent({\n type: \"element-clicked\",\n hash: \"\",\n componentName: undefined,\n filePath: \"\",\n classNameId: \"\",\n currentClassName: \"\",\n datasourceId: datasourceEl.dataset.upstartDatasource,\n recordId: datasourceEl.dataset.upstartRecordId,\n themeColors: {},\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n });\n return;\n }\n console.info(\"[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found\");\n return;\n }\n\n // If the clicked element is nested inside a rich-panel editable ancestor, bubble up to it\n // so that clicking any inline child (e.g. <strong>, <em>) edits the whole rich-panel region.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.stopPropagation();\n\n console.log(\"Element clicked dataset:\", element.dataset);\n\n const hash = element.dataset.upstartHash as string;\n const componentName = element.dataset.upstartComponent;\n const filePath = element.dataset.upstartFile ?? \"\";\n\n // If this element has editable-text, focus the TipTap editor (all modes now use inline TipTap)\n if (element.dataset.upstartEditableText === \"true\") {\n const proseMirror = element.querySelector<HTMLElement>(\".ProseMirror\");\n if (proseMirror) {\n proseMirror.focus();\n }\n }\n\n // Find classNameId on the element itself, or walk up to find it on a parent\n let classNameId = element.dataset.upstartClassnameId ?? \"\";\n let currentClassName = element.className;\n if (!classNameId) {\n const parentWithClassname = element.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n\n // Read DaisyUI CSS custom property values from the iframe's document\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n\n const rect = element.getBoundingClientRect();\n const bounds = {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n right: rect.right,\n bottom: rect.bottom,\n };\n\n // datasourceEl was resolved before the data-upstart-hash gate so we could\n // intercept datasource clicks even without a hash ancestor (see above).\n const datasourceId = datasourceEl?.dataset.upstartDatasource;\n const recordId = datasourceEl?.dataset.upstartRecordId;\n\n sendToParent({\n type: \"element-clicked\",\n hash,\n componentName,\n filePath,\n classNameId,\n currentClassName,\n datasourceId,\n recordId,\n themeColors,\n bounds,\n viewportWidth: window.innerWidth,\n });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"click-handler.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/click-handler.ts"],"sourcesContent":["import { getCurrentMode } from \"./state.js\";\nimport { sendToParent } from \"./utils.js\";\n\nlet isInitialized = false;\n\nconst DAISY_VAR_NAMES = [\n \"base-100\",\n \"base-200\",\n \"base-300\",\n \"base-content\",\n \"primary\",\n \"primary-content\",\n \"secondary\",\n \"secondary-content\",\n \"accent\",\n \"accent-content\",\n \"neutral\",\n \"neutral-content\",\n \"info\",\n \"info-content\",\n \"success\",\n \"success-content\",\n \"warning\",\n \"warning-content\",\n \"error\",\n \"error-content\",\n];\n\ninterface EditableImageRef {\n id: string;\n src: string;\n alt: string;\n}\n\n/**\n * Collect editable images (elements carrying data-upstart-image-id) from the\n * clicked element's nearest subtree. Starting at `start`, we climb ancestors\n * until we find a subtree that contains at least one editable image. This lets\n * us surface a background image (e.g. a hero <img> covered by overlays) even\n * when the click lands on a sibling overlay rather than the image itself.\n */\nfunction collectImages(start: HTMLElement): EditableImageRef[] {\n let el: HTMLElement | null = start;\n for (let depth = 0; el && el !== document.body && depth < 8; depth++, el = el.parentElement) {\n const found = new Set<HTMLElement>();\n if (el.dataset.upstartImageId) found.add(el);\n for (const node of el.querySelectorAll<HTMLElement>(\"[data-upstart-image-id]\")) {\n found.add(node);\n }\n if (found.size > 0) {\n return Array.from(found).map((node) => {\n const img = node as HTMLImageElement;\n return {\n id: node.dataset.upstartImageId as string,\n src: img.currentSrc || img.src || \"\",\n alt: img.alt || \"\",\n };\n });\n }\n }\n return [];\n}\n\n/**\n * Initialize click handler for className editing.\n */\nexport function initClickHandler(): void {\n if (typeof document === \"undefined\") {\n return;\n }\n\n if (isInitialized) {\n return;\n }\n\n console.log(\"[Upstart Editor] Initializing click handler...\");\n document.addEventListener(\"click\", handleClick, true);\n isInitialized = true;\n}\n\n/**\n * Cleanup click handler.\n */\nexport function cleanupClickHandler(): void {\n document.removeEventListener(\"click\", handleClick, true);\n isInitialized = false;\n}\n\nfunction handleClick(event: MouseEvent): void {\n if (getCurrentMode() !== \"edit\") {\n return;\n }\n\n console.debug(\"[Upstart Editor] Click event:\", event);\n\n const target = event.target as HTMLElement | null;\n if (!target) {\n console.warn(\"[Upstart Editor] Click target is not an HTMLElement\");\n return;\n }\n\n // Editor-only chrome (the array ×/+/✓ controls) and locally-staged draft list\n // items handle their own clicks and must never be treated as element selection.\n if (target.closest(\"#upstart-array-controls, [data-upstart-draft-item]\")) {\n return;\n }\n\n if (target.closest(\".upstart-editor-bubble-menu\")) {\n console.info(\"[Upstart Editor] Click ignored: target is inside the bubble menu\");\n return;\n }\n\n const activeLink = target.closest(\"a[data-upstart-editor-active]\");\n const isFormControlClick = Boolean(\n (target as HTMLElement | null)?.closest(\"input, textarea, select, button\"),\n );\n if (activeLink || !isFormControlClick) {\n event.preventDefault();\n }\n\n // Clicks inside contenteditable (TipTap active) — still notify style panel if applicable\n if (target.closest(\"[contenteditable='true']\")) {\n const editableEl = target.closest<HTMLElement>(\"[data-upstart-editable-text='true']\");\n if (editableEl) {\n let classNameId = editableEl.dataset.upstartClassnameId ?? \"\";\n let currentClassName = editableEl.className;\n if (!classNameId) {\n const parentWithClassname = editableEl.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n if (classNameId) {\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n sendToParent({\n type: \"element-clicked\",\n hash: editableEl.dataset.upstartHash ?? \"\",\n componentName: editableEl.dataset.upstartComponent,\n filePath: editableEl.dataset.upstartFile ?? \"\",\n classNameId,\n currentClassName,\n themeColors,\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n });\n }\n }\n console.info(\"[Upstart Editor] Click inside contenteditable: TipTap handles text editing\");\n return;\n }\n\n // Detect a datasource record under the click. We check this BEFORE the\n // data-upstart-hash gate because a record may be rendered without that\n // attribute (e.g. an <a> from <Link to={...}>) and we still want to open\n // the inline datasource editor instead of letting the browser navigate.\n const datasourceEl = target.closest<HTMLElement>(\"[data-upstart-datasource][data-upstart-record-id]\");\n\n // Find the closest element with data-upstart-hash\n const rawElement = target.closest<HTMLElement>(\"[data-upstart-hash]\");\n if (!rawElement) {\n if (datasourceEl) {\n // Datasource record click without a data-upstart-hash ancestor: stop\n // navigation, surface the datasource event so the editor can open the\n // inline datasource item inspector.\n event.preventDefault();\n event.stopPropagation();\n sendToParent({\n type: \"element-clicked\",\n hash: \"\",\n componentName: undefined,\n filePath: \"\",\n classNameId: \"\",\n currentClassName: \"\",\n datasourceId: datasourceEl.dataset.upstartDatasource,\n recordId: datasourceEl.dataset.upstartRecordId,\n themeColors: {},\n bounds: { top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0 },\n images: collectImages(datasourceEl),\n });\n return;\n }\n console.info(\"[Upstart Editor] Click ignored: no ancestral element with data-upstart-hash found\");\n return;\n }\n\n // If the clicked element is nested inside a rich-panel editable ancestor, bubble up to it\n // so that clicking any inline child (e.g. <strong>, <em>) edits the whole rich-panel region.\n const richPanelAncestor = rawElement.parentElement?.closest<HTMLElement>(\n '[data-upstart-editable-text-mode=\"rich-panel\"]',\n );\n const element = richPanelAncestor ?? rawElement;\n\n event.stopPropagation();\n\n console.log(\"Element clicked dataset:\", element.dataset);\n\n const hash = element.dataset.upstartHash as string;\n const componentName = element.dataset.upstartComponent;\n const filePath = element.dataset.upstartFile ?? \"\";\n\n // If this element has editable-text, focus the TipTap editor (all modes now use inline TipTap)\n if (element.dataset.upstartEditableText === \"true\") {\n const proseMirror = element.querySelector<HTMLElement>(\".ProseMirror\");\n if (proseMirror) {\n proseMirror.focus();\n }\n }\n\n // Find classNameId on the element itself, or walk up to find it on a parent\n let classNameId = element.dataset.upstartClassnameId ?? \"\";\n let currentClassName = element.className;\n if (!classNameId) {\n const parentWithClassname = element.closest<HTMLElement>(\"[data-upstart-classname-id]\");\n if (parentWithClassname) {\n classNameId = parentWithClassname.dataset.upstartClassnameId ?? \"\";\n currentClassName = parentWithClassname.className;\n }\n }\n\n // Read DaisyUI CSS custom property values from the iframe's document\n const computedStyle = getComputedStyle(document.documentElement);\n const themeColors: Record<string, string> = {};\n for (const name of DAISY_VAR_NAMES) {\n const val = computedStyle.getPropertyValue(`--color-${name}`).trim();\n if (val) themeColors[name] = val;\n }\n\n const rect = element.getBoundingClientRect();\n const bounds = {\n top: rect.top,\n left: rect.left,\n width: rect.width,\n height: rect.height,\n right: rect.right,\n bottom: rect.bottom,\n };\n\n // datasourceEl was resolved before the data-upstart-hash gate so we could\n // intercept datasource clicks even without a hash ancestor (see above).\n const datasourceId = datasourceEl?.dataset.upstartDatasource;\n const recordId = datasourceEl?.dataset.upstartRecordId;\n\n sendToParent({\n type: \"element-clicked\",\n hash,\n componentName,\n filePath,\n classNameId,\n currentClassName,\n datasourceId,\n recordId,\n themeColors,\n bounds,\n viewportWidth: window.innerWidth,\n images: collectImages(element),\n });\n\n console.log(\"[Upstart Editor] Element clicked:\", componentName, hash);\n}\n"],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ //#region src/vite-plugin-upstart-editor/runtime/form-guard.ts
2
+ let isInitialized = false;
3
+ const SAFE_METHODS = new Set(["get", "dialog"]);
4
+ const TOOLTIP_MESSAGE = "This action isn’t available in the editor preview";
5
+ const TOOLTIP_DURATION_MS = 2500;
6
+ const SUPPORTS_POPOVER = typeof HTMLElement !== "undefined" && "showPopover" in HTMLElement.prototype;
7
+ let tooltip = null;
8
+ let tooltipTimer = null;
9
+ /**
10
+ * Block mutating form submissions inside the previewed site so the user can't
11
+ * trigger data changes (which only hit the ephemeral preview DB and never persist)
12
+ * from the editor. This matters most in PREVIEW mode, where the site's buttons are
13
+ * live — in EDIT mode a click edits the button's text instead of submitting.
14
+ *
15
+ * We block the `submit` event rather than disabling buttons: one place covers native
16
+ * POST forms, React Router <Form>, button clicks and Enter-key submits. The
17
+ * capture-phase listener + stopImmediatePropagation runs before React's delegated
18
+ * handler, so React Router never processes the submission; preventDefault blocks a
19
+ * plain form's POST. GET and dialog submissions are left untouched. When a mutating
20
+ * submission is blocked, a tooltip on the triggering button explains why.
21
+ */
22
+ function initFormGuard() {
23
+ if (typeof document === "undefined" || isInitialized) return;
24
+ isInitialized = true;
25
+ document.addEventListener("submit", (event) => {
26
+ const form = event.target;
27
+ const method = (event.submitter?.getAttribute("formmethod") || form?.getAttribute("method") || "get").toLowerCase();
28
+ if (SAFE_METHODS.has(method)) return;
29
+ event.preventDefault();
30
+ event.stopImmediatePropagation();
31
+ console.debug("[Upstart Editor] Blocked a mutating form submission in the editor:", method);
32
+ const anchor = event.submitter ?? form;
33
+ if (anchor) showBlockedTooltip(anchor);
34
+ }, true);
35
+ }
36
+ function showBlockedTooltip(anchor) {
37
+ const tip = ensureTooltip();
38
+ const rect = anchor.getBoundingClientRect();
39
+ if (SUPPORTS_POPOVER) {
40
+ tip.style.left = `${rect.left + rect.width / 2}px`;
41
+ tip.style.top = `${rect.top - 8}px`;
42
+ try {
43
+ tip.hidePopover();
44
+ } catch {}
45
+ try {
46
+ tip.showPopover();
47
+ } catch {}
48
+ } else {
49
+ tip.style.left = `${rect.left + window.scrollX + rect.width / 2}px`;
50
+ tip.style.top = `${rect.top + window.scrollY - 8}px`;
51
+ tip.style.opacity = "1";
52
+ }
53
+ if (tooltipTimer !== null) clearTimeout(tooltipTimer);
54
+ tooltipTimer = window.setTimeout(() => {
55
+ tooltipTimer = null;
56
+ if (!tooltip) return;
57
+ if (SUPPORTS_POPOVER) try {
58
+ tooltip.hidePopover();
59
+ } catch {}
60
+ else tooltip.style.opacity = "0";
61
+ }, TOOLTIP_DURATION_MS);
62
+ }
63
+ function ensureTooltip() {
64
+ if (tooltip) return tooltip;
65
+ tooltip = document.createElement("div");
66
+ tooltip.id = "upstart-form-guard-tooltip";
67
+ tooltip.textContent = TOOLTIP_MESSAGE;
68
+ let css = "background: #111827; color: #fff; font-size: 12px; line-height: 1.3; padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; transform: translate(-50%, -100%); z-index: 2147483647;";
69
+ if (SUPPORTS_POPOVER) {
70
+ tooltip.setAttribute("popover", "manual");
71
+ css += "position: fixed; margin: 0; inset: auto; overflow: visible; border: 0;";
72
+ } else css += "position: absolute; opacity: 0; transition: opacity 0.15s ease;";
73
+ tooltip.style.cssText = css;
74
+ document.body.appendChild(tooltip);
75
+ return tooltip;
76
+ }
77
+ //#endregion
78
+ export { initFormGuard };
79
+
80
+ //# sourceMappingURL=form-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-guard.js","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/form-guard.ts"],"sourcesContent":["let isInitialized = false;\n\n// Methods that don't mutate data and must keep working: GET forms (search /\n// navigation) and method=\"dialog\" (e.g. a modal's close/backdrop buttons).\nconst SAFE_METHODS = new Set([\"get\", \"dialog\"]);\n\nconst TOOLTIP_MESSAGE = \"This action isn’t available in the editor preview\";\nconst TOOLTIP_DURATION_MS = 2500;\n\n// Popover API renders in the browser top layer, so the tooltip sits above modal\n// <dialog>s opened with showModal() (which also live in the top layer and beat any\n// z-index). Fall back to absolute positioning where it isn't supported.\nconst SUPPORTS_POPOVER = typeof HTMLElement !== \"undefined\" && \"showPopover\" in HTMLElement.prototype;\n\nlet tooltip: HTMLDivElement | null = null;\nlet tooltipTimer: number | null = null;\n\n/**\n * Block mutating form submissions inside the previewed site so the user can't\n * trigger data changes (which only hit the ephemeral preview DB and never persist)\n * from the editor. This matters most in PREVIEW mode, where the site's buttons are\n * live — in EDIT mode a click edits the button's text instead of submitting.\n *\n * We block the `submit` event rather than disabling buttons: one place covers native\n * POST forms, React Router <Form>, button clicks and Enter-key submits. The\n * capture-phase listener + stopImmediatePropagation runs before React's delegated\n * handler, so React Router never processes the submission; preventDefault blocks a\n * plain form's POST. GET and dialog submissions are left untouched. When a mutating\n * submission is blocked, a tooltip on the triggering button explains why.\n */\nexport function initFormGuard(): void {\n if (typeof document === \"undefined\" || isInitialized) {\n return;\n }\n isInitialized = true;\n\n document.addEventListener(\n \"submit\",\n (event: SubmitEvent) => {\n const form = event.target as HTMLFormElement | null;\n const method = (\n event.submitter?.getAttribute(\"formmethod\") ||\n form?.getAttribute(\"method\") ||\n \"get\"\n ).toLowerCase();\n\n if (SAFE_METHODS.has(method)) {\n return;\n }\n\n event.preventDefault();\n event.stopImmediatePropagation();\n console.debug(\"[Upstart Editor] Blocked a mutating form submission in the editor:\", method);\n\n const anchor = (event.submitter as HTMLElement | null) ?? form;\n if (anchor) showBlockedTooltip(anchor);\n },\n true,\n );\n}\n\nfunction showBlockedTooltip(anchor: HTMLElement): void {\n const tip = ensureTooltip();\n const rect = anchor.getBoundingClientRect();\n\n if (SUPPORTS_POPOVER) {\n // Popover is position:fixed → viewport coordinates (no scroll offset).\n tip.style.left = `${rect.left + rect.width / 2}px`;\n tip.style.top = `${rect.top - 8}px`;\n try {\n tip.hidePopover();\n } catch {}\n try {\n tip.showPopover();\n } catch {}\n } else {\n tip.style.left = `${rect.left + window.scrollX + rect.width / 2}px`;\n tip.style.top = `${rect.top + window.scrollY - 8}px`;\n tip.style.opacity = \"1\";\n }\n\n if (tooltipTimer !== null) {\n clearTimeout(tooltipTimer);\n }\n tooltipTimer = window.setTimeout(() => {\n tooltipTimer = null;\n if (!tooltip) return;\n if (SUPPORTS_POPOVER) {\n try {\n tooltip.hidePopover();\n } catch {}\n } else {\n tooltip.style.opacity = \"0\";\n }\n }, TOOLTIP_DURATION_MS);\n}\n\nfunction ensureTooltip(): HTMLDivElement {\n if (tooltip) return tooltip;\n tooltip = document.createElement(\"div\");\n tooltip.id = \"upstart-form-guard-tooltip\";\n tooltip.textContent = TOOLTIP_MESSAGE;\n\n let css =\n \"background: #111827; color: #fff; font-size: 12px; line-height: 1.3; \" +\n \"padding: 6px 10px; border-radius: 6px; max-width: 240px; text-align: center; \" +\n \"box-shadow: 0 4px 12px rgba(0,0,0,0.3); pointer-events: none; white-space: normal; \" +\n \"transform: translate(-50%, -100%); z-index: 2147483647;\";\n\n if (SUPPORTS_POPOVER) {\n tooltip.setAttribute(\"popover\", \"manual\");\n // Neutralise the UA popover centering so our top/left apply.\n css += \"position: fixed; margin: 0; inset: auto; overflow: visible; border: 0;\";\n } else {\n css += \"position: absolute; opacity: 0; transition: opacity 0.15s ease;\";\n }\n\n tooltip.style.cssText = css;\n document.body.appendChild(tooltip);\n return tooltip;\n}\n"],"mappings":";AAAA,IAAI,gBAAgB;AAIpB,MAAM,eAAe,IAAI,IAAI,CAAC,OAAO,SAAS,CAAC;AAE/C,MAAM,kBAAkB;AACxB,MAAM,sBAAsB;AAK5B,MAAM,mBAAmB,OAAO,gBAAgB,eAAe,iBAAiB,YAAY;AAE5F,IAAI,UAAiC;AACrC,IAAI,eAA8B;;;;;;;;;;;;;;AAelC,SAAgB,gBAAsB;CACpC,IAAI,OAAO,aAAa,eAAe,eACrC;CAEF,gBAAgB;CAEhB,SAAS,iBACP,WACC,UAAuB;EACtB,MAAM,OAAO,MAAM;EACnB,MAAM,UACJ,MAAM,WAAW,aAAa,aAAa,IAC3C,MAAM,aAAa,SAAS,IAC5B,OACA,aAAa;EAEf,IAAI,aAAa,IAAI,OAAO,EAC1B;EAGF,MAAM,gBAAgB;EACtB,MAAM,0BAA0B;EAChC,QAAQ,MAAM,sEAAsE,OAAO;EAE3F,MAAM,SAAU,MAAM,aAAoC;EAC1D,IAAI,QAAQ,mBAAmB,OAAO;IAExC,KACD;;AAGH,SAAS,mBAAmB,QAA2B;CACrD,MAAM,MAAM,eAAe;CAC3B,MAAM,OAAO,OAAO,uBAAuB;CAE3C,IAAI,kBAAkB;EAEpB,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,KAAK,QAAQ,EAAE;EAC/C,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,EAAE;EAChC,IAAI;GACF,IAAI,aAAa;UACX;EACR,IAAI;GACF,IAAI,aAAa;UACX;QACH;EACL,IAAI,MAAM,OAAO,GAAG,KAAK,OAAO,OAAO,UAAU,KAAK,QAAQ,EAAE;EAChE,IAAI,MAAM,MAAM,GAAG,KAAK,MAAM,OAAO,UAAU,EAAE;EACjD,IAAI,MAAM,UAAU;;CAGtB,IAAI,iBAAiB,MACnB,aAAa,aAAa;CAE5B,eAAe,OAAO,iBAAiB;EACrC,eAAe;EACf,IAAI,CAAC,SAAS;EACd,IAAI,kBACF,IAAI;GACF,QAAQ,aAAa;UACf;OAER,QAAQ,MAAM,UAAU;IAEzB,oBAAoB;;AAGzB,SAAS,gBAAgC;CACvC,IAAI,SAAS,OAAO;CACpB,UAAU,SAAS,cAAc,MAAM;CACvC,QAAQ,KAAK;CACb,QAAQ,cAAc;CAEtB,IAAI,MACF;CAKF,IAAI,kBAAkB;EACpB,QAAQ,aAAa,WAAW,SAAS;EAEzC,OAAO;QAEP,OAAO;CAGT,QAAQ,MAAM,UAAU;CACxB,SAAS,KAAK,YAAY,QAAQ;CAClC,OAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"hover-overlay.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"mappings":";;AAYA;;iBAAgB,gBAAA,CAAA;;;AAiDhB;iBAAgB,YAAA,CAAA"}
1
+ {"version":3,"file":"hover-overlay.d.ts","names":[],"sources":["../../../src/vite-plugin-upstart-editor/runtime/hover-overlay.ts"],"mappings":";;AAYA;;iBAAgB,gBAAA,CAAA;;;AAkDhB;iBAAgB,YAAA,CAAA"}
@@ -16,6 +16,7 @@ function initHoverOverlay() {
16
16
  style.id = "upstart-hover-edit-styles";
17
17
  style.textContent = [
18
18
  ":root[data-upstart-edit-mode] [data-upstart-classname-id]:not([data-upstart-editable-text='true']),",
19
+ ":root[data-upstart-edit-mode] [data-upstart-image-id],",
19
20
  ":root[data-upstart-edit-mode] [data-upstart-datasource][data-upstart-record-id]:not([data-upstart-editable-text='true']),",
20
21
  ":root[data-upstart-edit-mode] [data-upstart-editor-active] {",
21
22
  " transition: outline 150ms, outline-offset 150ms;",
@@ -56,7 +57,7 @@ function hideOverlays() {
56
57
  }
57
58
  function isEligible(el) {
58
59
  if (el.dataset.upstartEditableText === "true") return false;
59
- return !!(el.dataset.upstartClassnameId || el.dataset.upstartDatasource && el.dataset.upstartRecordId);
60
+ return !!(el.dataset.upstartClassnameId || el.dataset.upstartImageId || el.dataset.upstartDatasource && el.dataset.upstartRecordId);
60
61
  }
61
62
  function clearEditableHover() {
62
63
  for (const el of document.querySelectorAll("[data-upstart-hovered],[data-upstart-hovered-ancestor]")) {