vibespot 1.7.7 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +11 -0
  2. package/assets/whats-new.json +27 -0
  3. package/dist/index.js +482 -648
  4. package/dist/index.js.map +1 -1
  5. package/package.json +7 -4
  6. package/ui/chat.js +21 -8
  7. package/ui/dashboard.js +53 -12
  8. package/ui/docs/index.html +10 -1
  9. package/ui/email-preview.js +1 -5
  10. package/ui/escape-html.js +14 -0
  11. package/ui/field-editor.js +6 -12
  12. package/ui/field-save.js +82 -0
  13. package/ui/index.html +6 -3
  14. package/ui/inline-edit.js +116 -570
  15. package/ui/plan.js +19 -13
  16. package/ui/preview-agent.js +1050 -0
  17. package/ui/preview.js +248 -265
  18. package/ui/section-controls.js +16 -622
  19. package/ui/setup.js +73 -20
  20. package/ui/styles.css +177 -0
  21. package/ui/upload-panel.js +7 -8
  22. package/ui/whats-new.js +249 -0
  23. package/assets/readme/00-hero-banner.png +0 -0
  24. package/assets/readme/00-hero-banner.svg +0 -59
  25. package/assets/readme/01-vibe-coding-hero.png +0 -0
  26. package/assets/readme/02-checkpoints.png +0 -0
  27. package/assets/readme/02-plan-mode.png +0 -0
  28. package/assets/readme/03-figma-import.png +0 -0
  29. package/assets/readme/04-multi-page-sites.png +0 -0
  30. package/assets/readme/05-inline-wysiwyg.png +0 -0
  31. package/assets/readme/06-hubspot-upload.png +0 -0
  32. package/ui/docs/screenshots/brand-kit-preview.png +0 -0
  33. package/ui/docs/screenshots/checkpoint-card.png +0 -0
  34. package/ui/docs/screenshots/content-type-dropdown.png +0 -0
  35. package/ui/docs/screenshots/editor-full-layout.png +0 -0
  36. package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
  37. package/ui/docs/screenshots/module-overview-slideout.png +0 -0
  38. package/ui/docs/screenshots/multi-page-tree.png +0 -0
  39. package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
  40. package/ui/docs/screenshots/split-pane-view.png +0 -0
  41. package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
  42. package/ui/docs/screenshots/workspace-tabs.png +0 -0
@@ -0,0 +1,1050 @@
1
+ /**
2
+ * Trusted in-frame preview agent (VIB-1892).
3
+ *
4
+ * Served by the separate preview origin and injected at the top of <head> of
5
+ * every composed preview doc, so it boots BEFORE any AI-generated module
6
+ * script. It owns all in-document interaction (hover labels, inline editing,
7
+ * section controls, change highlights, working overlays) and talks to the app
8
+ * over the narrow vs:* postMessage protocol — see src/server/preview-protocol.ts
9
+ * for the contract. The verb strings here are locked to that module by
10
+ * test/preview-protocol-parity.test.ts.
11
+ *
12
+ * Trust model: this script and the AI page share one JS realm, so nothing in
13
+ * here is a secret from the page. Security comes from the origin split + the
14
+ * protocol's tiny write vocabulary (field edits only), not from hiding state.
15
+ * Still, the handshake token is scrubbed from the URL and config block before
16
+ * AI code runs so it can't leak into rendered output.
17
+ *
18
+ * Pointer scope (VIB-1921): all in-frame chrome (hover labels, inline editors,
19
+ * section toolbar/popovers) is desktop-pointer-only by design — it is
20
+ * hover-driven with small targets. Touch/mobile users edit through the chat
21
+ * and field-editor panels instead. Touch-first affordances are tracked
22
+ * separately; do not bolt tap handlers onto the hover paths here.
23
+ */
24
+ (() => {
25
+ "use strict";
26
+
27
+ // ---- bootstrap config (runs at parse time, before any AI script) --------
28
+ var TOKEN = "";
29
+ var cfgEl = document.getElementById("vs-preview-config");
30
+ if (cfgEl) {
31
+ try { TOKEN = String(JSON.parse(cfgEl.textContent).token || ""); } catch { /* no token — agent stays mute */ }
32
+ cfgEl.remove();
33
+ }
34
+ var FIELDS = {};
35
+ var fieldsEl = document.getElementById("vs-preview-fields");
36
+ if (fieldsEl) {
37
+ try { FIELDS = JSON.parse(fieldsEl.textContent) || {}; } catch { /* no controls */ }
38
+ fieldsEl.remove();
39
+ }
40
+ // Scrub the access token out of the address bar so DOM-reading AI code
41
+ // can't echo it into rendered output.
42
+ try { history.replaceState(null, "", location.pathname); } catch { /* sandboxed */ }
43
+
44
+ var PROTOCOL_V = 1;
45
+ var INBOUND = {
46
+ INIT: "vs:init",
47
+ SET_MODE: "vs:set-mode",
48
+ HIGHLIGHT_CHANGED: "vs:highlight-changed",
49
+ MARK_WORKING: "vs:mark-working",
50
+ CLEAR_WORKING: "vs:clear-working",
51
+ SCROLL_TO: "vs:scroll-to",
52
+ };
53
+ var INBOUND_TYPES = new Set(Object.values(INBOUND));
54
+ var OUT = {
55
+ READY: "vs:ready",
56
+ EMPTY_STATE: "vs:empty-state",
57
+ SELECT_MODULE: "vs:select-module",
58
+ EDIT_COMMIT: "vs:edit-commit",
59
+ FIELD_COMMIT: "vs:field-commit",
60
+ REQUEST_MODE: "vs:request-mode",
61
+ };
62
+
63
+ /** Pinned on the first valid vs:init; later messages must match it. */
64
+ var parentOrigin = null;
65
+ var mode = "view";
66
+
67
+ function send(type, payload) {
68
+ if (!TOKEN) return;
69
+ // frame-ancestors pins who may embed this doc, so window.parent is the
70
+ // app; "*" is only used for the pre-handshake READY.
71
+ window.parent.postMessage(
72
+ { v: PROTOCOL_V, token: TOKEN, type: type, payload: payload },
73
+ parentOrigin || "*"
74
+ );
75
+ }
76
+
77
+ window.addEventListener("message", function (e) {
78
+ // Only the embedding app window — never the AI page posting to itself.
79
+ if (e.source !== window.parent) return;
80
+ var d = e.data;
81
+ if (!d || typeof d !== "object") return;
82
+ if (d.v !== PROTOCOL_V) return;
83
+ if (!TOKEN || typeof d.token !== "string" || d.token !== TOKEN) return;
84
+ if (!INBOUND_TYPES.has(d.type)) return;
85
+ if (parentOrigin === null) {
86
+ if (d.type !== INBOUND.INIT) return; // must handshake first
87
+ parentOrigin = e.origin;
88
+ } else if (e.origin !== parentOrigin) {
89
+ return;
90
+ }
91
+ handleCommand(d.type, d.payload || {});
92
+ });
93
+
94
+ function handleCommand(type, p) {
95
+ switch (type) {
96
+ case INBOUND.INIT:
97
+ case INBOUND.SET_MODE:
98
+ setMode(typeof p.mode === "string" ? p.mode : "view");
99
+ break;
100
+ case INBOUND.HIGHLIGHT_CHANGED:
101
+ highlightChanged(strList(p.changed), strList(p.fresh));
102
+ break;
103
+ case INBOUND.MARK_WORKING:
104
+ markWorking(strList(p.modules));
105
+ break;
106
+ case INBOUND.CLEAR_WORKING:
107
+ if (Array.isArray(p.modules)) strList(p.modules).forEach(clearOneWorking);
108
+ else clearAllWorking();
109
+ break;
110
+ case INBOUND.SCROLL_TO:
111
+ if (typeof p.module === "string") scrollToModule(p.module);
112
+ break;
113
+ }
114
+ }
115
+
116
+ function strList(v) {
117
+ return Array.isArray(v) ? v.filter(function (x) { return typeof x === "string"; }) : [];
118
+ }
119
+
120
+ function cssEscape(value) {
121
+ if (typeof CSS !== "undefined" && typeof CSS.escape === "function") return CSS.escape(value);
122
+ return String(value).replace(/["\\]/g, "\\$&");
123
+ }
124
+
125
+ function moduleEl(name) {
126
+ return document.querySelector('[data-module="' + cssEscape(name) + '"]');
127
+ }
128
+
129
+ // ---- handshake -----------------------------------------------------------
130
+ function onDomReady(fn) {
131
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", fn);
132
+ else fn();
133
+ }
134
+
135
+ onDomReady(function () {
136
+ send(OUT.READY, {});
137
+ send(OUT.EMPTY_STATE, { hasModules: !!document.querySelector("[data-module]") });
138
+ });
139
+
140
+ // ---- chrome color tokens (VIB-1921) ---------------------------------------
141
+ // Single source of truth for the injected chrome palette, mirroring the app
142
+ // tokens in ui/styles.css: --vs-accent = app --accent (brand coral),
143
+ // --vs-edit = app --info (blue). The two-color split is deliberate —
144
+ // coral marks section-style / AI-selection chrome, blue marks "you are
145
+ // editing this content directly". The -rgb triplets feed rgba() variants.
146
+ function ensureTokenStyles() {
147
+ if (document.getElementById("vibespot-chrome-tokens-css")) return;
148
+ var style = document.createElement("style");
149
+ style.id = "vibespot-chrome-tokens-css";
150
+ style.textContent = ":root{" +
151
+ "--vs-accent:#e8613a;--vs-accent-hover:#d4522e;--vs-accent-rgb:232,97,58;" +
152
+ "--vs-edit:#3b82f6;--vs-edit-rgb:59,130,246}";
153
+ document.head.appendChild(style);
154
+ }
155
+
156
+ // ---- change highlighting (ported from ui/preview.js) ---------------------
157
+ function ensureHighlightStyles() {
158
+ if (document.getElementById("vibespot-change-highlight-css")) return;
159
+ ensureTokenStyles();
160
+ var style = document.createElement("style");
161
+ style.id = "vibespot-change-highlight-css";
162
+ style.textContent = "\n" +
163
+ "@keyframes vibespot-change-glow{0%{outline-color:rgba(var(--vs-accent-rgb),.85);box-shadow:0 0 0 6px rgba(var(--vs-accent-rgb),.18)}70%{outline-color:rgba(var(--vs-accent-rgb),.55);box-shadow:0 0 0 4px rgba(var(--vs-accent-rgb),.1)}100%{outline-color:rgba(var(--vs-accent-rgb),0);box-shadow:0 0 0 0 rgba(var(--vs-accent-rgb),0)}}\n" +
164
+ ".vibespot-module--changed{outline:2px solid rgba(var(--vs-accent-rgb),.85);outline-offset:4px;border-radius:2px;animation:vibespot-change-glow 2s ease-out forwards}\n" +
165
+ "@keyframes vibespot-module-slide-in{0%{opacity:0;transform:translateY(24px)}100%{opacity:1;transform:translateY(0)}}\n" +
166
+ ".vibespot-module--new{animation:vibespot-module-slide-in .6s cubic-bezier(.2,.8,.2,1) both}\n" +
167
+ "@media (prefers-reduced-motion:reduce){.vibespot-module--changed,.vibespot-module--new{animation:none}.vibespot-module--changed{outline-color:transparent}}";
168
+ document.head.appendChild(style);
169
+ }
170
+
171
+ function highlightChanged(changed, fresh) {
172
+ if (!changed.length && !fresh.length) return;
173
+ ensureHighlightStyles();
174
+ fresh.forEach(function (name) {
175
+ var el = moduleEl(name);
176
+ if (!el) return;
177
+ el.classList.add("vibespot-module--new");
178
+ setTimeout(function () { el.classList.remove("vibespot-module--new"); }, 700);
179
+ });
180
+ var freshSet = new Set(fresh);
181
+ changed.forEach(function (name) {
182
+ if (freshSet.has(name)) return; // slide-in is the stronger signal
183
+ var el = moduleEl(name);
184
+ if (!el) return;
185
+ el.classList.remove("vibespot-module--changed");
186
+ void el.offsetWidth; // restart the animation
187
+ el.classList.add("vibespot-module--changed");
188
+ setTimeout(function () { el.classList.remove("vibespot-module--changed"); }, 2200);
189
+ });
190
+ }
191
+
192
+ function scrollToModule(name) {
193
+ var el = moduleEl(name);
194
+ if (!el) return;
195
+ el.scrollIntoView({ behavior: "smooth", block: "start" });
196
+ el.style.outline = "2px solid var(--vs-accent, #e8613a)";
197
+ el.style.outlineOffset = "4px";
198
+ el.style.transition = "outline-color 0.5s ease";
199
+ setTimeout(function () {
200
+ el.style.outlineColor = "transparent";
201
+ setTimeout(function () { el.style.outline = ""; el.style.outlineOffset = ""; }, 500);
202
+ }, 1500);
203
+ }
204
+
205
+ // ---- "working on it" overlays (ported from ui/preview.js) ----------------
206
+ var WORKING_MESSAGES = [
207
+ "Rethinking this section…", "Rewriting the code…",
208
+ "Giving this a fresh coat of paint…", "Making it better…",
209
+ "Tweaking the layout…", "Polishing the details…",
210
+ "Almost there…", "Updating the design…",
211
+ "Improving the copy…", "Refining the structure…",
212
+ ];
213
+ var workingIntervals = new Map();
214
+
215
+ function ensureOverlayStyles() {
216
+ if (document.getElementById("vibespot-working-css")) return;
217
+ ensureTokenStyles();
218
+ var style = document.createElement("style");
219
+ style.id = "vibespot-working-css";
220
+ style.textContent = "\n" +
221
+ ".vibespot-module--working{position:relative}\n" +
222
+ ".vibespot-module--working > *:not(.vibespot-working-overlay){filter:blur(3px) saturate(.4);opacity:.5;transition:filter .4s ease,opacity .4s ease;pointer-events:none}\n" +
223
+ ".vibespot-working-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:9999;pointer-events:none}\n" +
224
+ ".vibespot-working-overlay__spinner{width:36px;height:36px;border:3px solid rgba(var(--vs-accent-rgb),.15);border-top-color:rgba(var(--vs-accent-rgb),.8);border-radius:50%;animation:vw-spin .8s linear infinite;margin-bottom:12px}\n" +
225
+ ".vibespot-working-overlay__text{font-family:system-ui,-apple-system,sans-serif;font-size:14px;font-weight:500;color:rgba(255,255,255,.7);text-align:center;max-width:280px;transition:opacity .5s ease}\n" +
226
+ ".vibespot-working-overlay__text.fade{opacity:0}\n" +
227
+ "@keyframes vw-spin{to{transform:rotate(360deg)}}";
228
+ document.head.appendChild(style);
229
+ }
230
+
231
+ function markWorking(names) {
232
+ if (!document.body) return;
233
+ ensureOverlayStyles();
234
+ names.forEach(function (name, i) {
235
+ var el = moduleEl(name);
236
+ if (!el || el.classList.contains("vibespot-module--working")) return;
237
+ el.classList.add("vibespot-module--working");
238
+
239
+ var overlay = document.createElement("div");
240
+ overlay.className = "vibespot-working-overlay";
241
+ overlay.innerHTML =
242
+ '<div class="vibespot-working-overlay__spinner"></div>' +
243
+ '<div class="vibespot-working-overlay__text"></div>';
244
+ el.appendChild(overlay);
245
+
246
+ if (i === 0) el.scrollIntoView({ behavior: "smooth", block: "center" });
247
+
248
+ var textEl = overlay.querySelector(".vibespot-working-overlay__text");
249
+ var idx = Math.floor(Math.random() * WORKING_MESSAGES.length);
250
+ textEl.textContent = WORKING_MESSAGES[idx];
251
+ var interval = setInterval(function () {
252
+ textEl.classList.add("fade");
253
+ setTimeout(function () {
254
+ idx = (idx + 1) % WORKING_MESSAGES.length;
255
+ textEl.textContent = WORKING_MESSAGES[idx];
256
+ textEl.classList.remove("fade");
257
+ }, 500);
258
+ }, 3500);
259
+ workingIntervals.set(name, interval);
260
+ });
261
+ }
262
+
263
+ function clearOneWorking(name) {
264
+ var interval = workingIntervals.get(name);
265
+ if (interval) { clearInterval(interval); workingIntervals.delete(name); }
266
+ var el = moduleEl(name);
267
+ if (!el) return;
268
+ el.classList.remove("vibespot-module--working");
269
+ var overlay = el.querySelector(".vibespot-working-overlay");
270
+ if (overlay) overlay.remove();
271
+ }
272
+
273
+ function clearAllWorking() {
274
+ workingIntervals.forEach(function (interval) { clearInterval(interval); });
275
+ workingIntervals.clear();
276
+ document.querySelectorAll(".vibespot-module--working").forEach(function (el) {
277
+ el.classList.remove("vibespot-module--working");
278
+ var overlay = el.querySelector(".vibespot-working-overlay");
279
+ if (overlay) overlay.remove();
280
+ });
281
+ }
282
+
283
+ // ---- interact mode (ported from ui/inline-edit.js) ------------------------
284
+ var interactAttached = false;
285
+ var interactState = null;
286
+ /** Set by the section-controls block; force-closes its toolbar/popover. */
287
+ var hideSectionChrome = null;
288
+
289
+ function setMode(next) {
290
+ if (next !== "view" && next !== "interact" && next !== "section") next = "view";
291
+ if (next === mode) return;
292
+ mode = next;
293
+ if (mode === "interact") {
294
+ // An already-open section toolbar would otherwise stack on top of the
295
+ // inline-edit affordances until the next mouse-out (VIB-1920 review).
296
+ if (hideSectionChrome) hideSectionChrome();
297
+ attachInteract();
298
+ } else {
299
+ detachInteract();
300
+ }
301
+ }
302
+
303
+ function ensureInteractStyles() {
304
+ if (document.getElementById("vibespot-interact-css")) return;
305
+ ensureTokenStyles();
306
+ var style = document.createElement("style");
307
+ style.id = "vibespot-interact-css";
308
+ style.textContent = "\n" +
309
+ "html.vibespot-interact-mode{cursor:default}\n" +
310
+ ".vibespot-editable-hover{outline:2px dashed rgba(var(--vs-edit-rgb),.6)!important;outline-offset:2px!important;cursor:text!important}\n" +
311
+ '.vibespot-editable-hover[data-edit-type="image"]{cursor:pointer!important}\n' +
312
+ '.vibespot-editable-hover[data-edit-type="select"]{outline-color:var(--vs-accent)!important;outline-style:solid!important;background-color:rgba(var(--vs-accent-rgb),.08)!important;cursor:crosshair!important}\n' +
313
+ ".vibespot-editing{outline:2px solid rgba(var(--vs-edit-rgb),.9)!important;outline-offset:2px!important;background-color:rgba(var(--vs-edit-rgb),.04)!important;min-height:1em}\n" +
314
+ '.vibespot-edit-label{position:fixed;z-index:2147483647;pointer-events:none;font:500 11px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;color:#fff;padding:2px 7px;border-radius:3px;box-shadow:0 2px 6px rgba(0,0,0,.2);white-space:nowrap}\n' +
315
+ ".vibespot-edit-label--edit{background:var(--vs-edit)}\n" +
316
+ ".vibespot-edit-label--select{background:var(--vs-accent)}\n" +
317
+ ".vibespot-image-edit-input{position:fixed;z-index:2147483647;font:13px/1.4 -apple-system,BlinkMacSystemFont,sans-serif;padding:6px 10px;border:2px solid var(--vs-edit);border-radius:6px;background:#1c1917;color:#fff;width:320px;outline:none}\n" +
318
+ ".vibespot-link-edit-popup{position:fixed;z-index:2147483647;background:#1c1917;border:1px solid rgba(255,255,255,.15);border-radius:8px;padding:10px;box-shadow:0 4px 16px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:6px}\n" +
319
+ ".vibespot-link-edit-popup label{font:500 11px/1.4 -apple-system,sans-serif;color:rgba(255,255,255,.6)}\n" +
320
+ ".vibespot-link-edit-popup input{font:13px/1.4 -apple-system,sans-serif;padding:5px 8px;border:1px solid rgba(255,255,255,.2);border-radius:4px;background:#0c0a09;color:#fff;outline:none;width:260px}\n" +
321
+ ".vibespot-link-edit-popup input:focus{border-color:var(--vs-edit)}\n" +
322
+ ".vibespot-link-edit-popup .vibespot-link-edit-actions{display:flex;gap:6px;justify-content:flex-end;margin-top:4px}\n" +
323
+ ".vibespot-link-edit-popup button{font:500 12px/1 -apple-system,sans-serif;padding:5px 12px;border:none;border-radius:4px;cursor:pointer}\n" +
324
+ ".vibespot-link-edit-popup .vibespot-btn-save{background:var(--vs-edit);color:#fff}\n" +
325
+ ".vibespot-link-edit-popup .vibespot-btn-cancel{background:rgba(255,255,255,.1);color:rgba(255,255,255,.7)}";
326
+ document.head.appendChild(style);
327
+ }
328
+
329
+ function getInteractInfo(el) {
330
+ if (!el) return null;
331
+ var tag = (el.tagName || "").toLowerCase();
332
+
333
+ var annotated = el.closest("[data-vs-field]");
334
+ if (annotated) {
335
+ var vsType = annotated.getAttribute("data-vs-type");
336
+ if (vsType === "image") return { type: "image", el: annotated, action: "edit" };
337
+ if (vsType === "link") return { type: "link", el: annotated, action: "edit" };
338
+ return { type: "text", el: annotated, action: "edit" };
339
+ }
340
+ var linkAnnotated = el.closest("[data-vs-link]");
341
+ if (linkAnnotated) return { type: "link", el: linkAnnotated, action: "edit" };
342
+
343
+ if (tag === "img") return { type: "image", el: el, action: "edit" };
344
+ if (tag === "a" || (el.closest && el.closest("a"))) {
345
+ var anchor = tag === "a" ? el : el.closest("a");
346
+ return { type: "link", el: anchor, action: "edit" };
347
+ }
348
+ if (/^h[1-6]$/.test(tag) || tag === "p" || tag === "span" || tag === "li" || tag === "td" || tag === "th") {
349
+ if (el.children.length === 0 || (el.children.length === 1 && el.children[0].tagName === "BR")) {
350
+ return { type: "text", el: el, action: "edit" };
351
+ }
352
+ if (el.textContent && el.textContent.trim().length > 0 && el.childElementCount <= 2) {
353
+ return { type: "text", el: el, action: "edit" };
354
+ }
355
+ }
356
+ if (tag === "button") return { type: "text", el: el, action: "edit" };
357
+
358
+ var modEl = el.closest("[data-module]");
359
+ if (modEl) return { type: "select", el: modEl, action: "select" };
360
+ return null;
361
+ }
362
+
363
+ function describeElement(el) {
364
+ if (!el) return "";
365
+ var modEl = el.closest("[data-module]");
366
+ var moduleName = modEl ? modEl.getAttribute("data-module") : null;
367
+ var tag = (el.tagName || "").toLowerCase();
368
+ var kind = tag;
369
+ if (/^h[1-6]$/.test(tag)) kind = "headline";
370
+ else if (tag === "p") kind = "paragraph";
371
+ else if (tag === "a") kind = "link";
372
+ else if (tag === "button") kind = "button";
373
+ else if (tag === "img") kind = "image";
374
+ else if (tag === "ul" || tag === "ol") kind = "list";
375
+ else if (tag === "li") kind = "list item";
376
+ if (moduleName && modEl === el) return moduleName;
377
+ if (moduleName) return moduleName + " > " + kind;
378
+ return kind;
379
+ }
380
+
381
+ function buildChatPrefill(el) {
382
+ if (!el) return "";
383
+ var modEl = el.closest("[data-module]");
384
+ var moduleName = modEl ? modEl.getAttribute("data-module") : null;
385
+ var tag = (el.tagName || "").toLowerCase();
386
+ var text = (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
387
+ var elementPart;
388
+ if (/^h[1-6]$/.test(tag)) elementPart = "the headline";
389
+ else if (tag === "p") elementPart = "the paragraph";
390
+ else if (tag === "a") elementPart = "the link";
391
+ else if (tag === "button") elementPart = "the button";
392
+ else if (tag === "img") elementPart = "the image";
393
+ else elementPart = "the " + tag;
394
+ if (modEl === el && moduleName) return "In the " + moduleName + " module, ";
395
+ if (moduleName) {
396
+ var quote = text ? ' ("' + text + (text.length === 80 ? "…" : "") + '")' : "";
397
+ return "In the " + moduleName + " module, " + elementPart + quote + " ";
398
+ }
399
+ return elementPart + " ";
400
+ }
401
+
402
+ function attachInteract() {
403
+ if (interactAttached || !document.body) return;
404
+ interactAttached = true;
405
+ ensureInteractStyles();
406
+ document.documentElement.classList.add("vibespot-interact-mode");
407
+
408
+ var hoveredEl = null;
409
+ var labelEl = null;
410
+ var activeEditor = null;
411
+
412
+ var cleanup = function () {
413
+ if (hoveredEl) {
414
+ hoveredEl.classList.remove("vibespot-editable-hover");
415
+ hoveredEl.removeAttribute("data-edit-type");
416
+ hoveredEl = null;
417
+ }
418
+ if (labelEl && labelEl.parentNode) labelEl.parentNode.removeChild(labelEl);
419
+ labelEl = null;
420
+ };
421
+
422
+ var closeActiveEditor = function () {
423
+ if (!activeEditor) return false;
424
+ if (activeEditor.cleanup) activeEditor.cleanup();
425
+ activeEditor = null;
426
+ return true;
427
+ };
428
+
429
+ var onMouseOver = function (e) {
430
+ if (activeEditor) return;
431
+ var info = getInteractInfo(e.target);
432
+ if (!info) { cleanup(); return; }
433
+ if (hoveredEl === info.el) return;
434
+ cleanup();
435
+ hoveredEl = info.el;
436
+ hoveredEl.classList.add("vibespot-editable-hover");
437
+ hoveredEl.setAttribute("data-edit-type", info.action === "select" ? "select" : info.type);
438
+
439
+ if (!labelEl) {
440
+ labelEl = document.createElement("div");
441
+ labelEl.className = "vibespot-edit-label";
442
+ document.body.appendChild(labelEl);
443
+ }
444
+ if (info.action === "select") {
445
+ labelEl.className = "vibespot-edit-label vibespot-edit-label--select";
446
+ labelEl.textContent = describeElement(info.el);
447
+ } else {
448
+ labelEl.className = "vibespot-edit-label vibespot-edit-label--edit";
449
+ labelEl.textContent = info.type === "image" ? "Click to edit image" : info.type === "link" ? "Click to edit link" : "Click to edit";
450
+ }
451
+ var rect = info.el.getBoundingClientRect();
452
+ labelEl.style.top = Math.max(4, rect.top - 20) + "px";
453
+ labelEl.style.left = Math.max(4, rect.left) + "px";
454
+ };
455
+
456
+ var onMouseOut = function (e) {
457
+ if (activeEditor) return;
458
+ if (!e.relatedTarget) cleanup();
459
+ };
460
+
461
+ function startTextEdit(el, moduleName) {
462
+ var originalText = el.textContent;
463
+ el.setAttribute("contenteditable", "true");
464
+ el.classList.add("vibespot-editing");
465
+ el.focus();
466
+
467
+ var range = document.createRange();
468
+ range.selectNodeContents(el);
469
+ var sel = window.getSelection();
470
+ sel.removeAllRanges();
471
+ sel.addRange(range);
472
+
473
+ var save = function () {
474
+ el.removeAttribute("contenteditable");
475
+ el.classList.remove("vibespot-editing");
476
+ var newText = el.textContent.trim();
477
+ if (newText !== originalText.trim()) {
478
+ send(OUT.EDIT_COMMIT, {
479
+ kind: "text",
480
+ moduleName: moduleName,
481
+ fieldPath: el.getAttribute("data-vs-field") || null,
482
+ originalText: originalText.trim(),
483
+ tag: (el.tagName || "").toLowerCase(),
484
+ value: newText,
485
+ });
486
+ }
487
+ activeEditor = null;
488
+ };
489
+
490
+ el.addEventListener("blur", save, { once: true });
491
+ el.addEventListener("keydown", function (e) {
492
+ if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); el.blur(); }
493
+ if (e.key === "Escape") { el.textContent = originalText; el.blur(); }
494
+ });
495
+
496
+ activeEditor = {
497
+ cleanup: function () {
498
+ el.removeAttribute("contenteditable");
499
+ el.classList.remove("vibespot-editing");
500
+ },
501
+ };
502
+ }
503
+
504
+ function startImageEdit(imgEl, moduleName) {
505
+ var rect = imgEl.getBoundingClientRect();
506
+ var input = document.createElement("input");
507
+ input.className = "vibespot-image-edit-input";
508
+ input.type = "text";
509
+ input.placeholder = "Enter image URL...";
510
+ input.value = imgEl.src || "";
511
+ input.style.top = rect.bottom + 4 + "px";
512
+ input.style.left = Math.max(4, rect.left) + "px";
513
+ document.body.appendChild(input);
514
+ input.focus();
515
+ input.select();
516
+
517
+ var save = function () {
518
+ var newSrc = input.value.trim();
519
+ if (input.parentNode) input.parentNode.removeChild(input);
520
+ if (newSrc && newSrc !== imgEl.src) {
521
+ var oldSrc = imgEl.src || "";
522
+ imgEl.src = newSrc; // optimistic in-frame update
523
+ send(OUT.EDIT_COMMIT, {
524
+ kind: "image",
525
+ moduleName: moduleName,
526
+ fieldPath: imgEl.getAttribute("data-vs-field") || null,
527
+ alt: imgEl.getAttribute("alt") || "",
528
+ oldSrc: oldSrc,
529
+ value: newSrc,
530
+ });
531
+ }
532
+ activeEditor = null;
533
+ };
534
+
535
+ input.addEventListener("blur", save, { once: true });
536
+ input.addEventListener("keydown", function (e) {
537
+ if (e.key === "Enter") { e.preventDefault(); input.blur(); }
538
+ if (e.key === "Escape") {
539
+ if (input.parentNode) input.parentNode.removeChild(input);
540
+ activeEditor = null;
541
+ }
542
+ });
543
+
544
+ activeEditor = {
545
+ cleanup: function () { if (input.parentNode) input.parentNode.removeChild(input); },
546
+ };
547
+ }
548
+
549
+ function startLinkEdit(anchorEl, moduleName) {
550
+ var origText = (anchorEl.textContent || "").trim();
551
+ var origHref = anchorEl.href || "";
552
+ var rect = anchorEl.getBoundingClientRect();
553
+
554
+ var popup = document.createElement("div");
555
+ popup.className = "vibespot-link-edit-popup";
556
+ popup.style.top = rect.bottom + 6 + "px";
557
+ popup.style.left = Math.max(4, rect.left) + "px";
558
+
559
+ var textLabel = document.createElement("label");
560
+ textLabel.textContent = "Link text";
561
+ var textInput = document.createElement("input");
562
+ textInput.type = "text";
563
+ textInput.value = origText;
564
+
565
+ var urlLabel = document.createElement("label");
566
+ urlLabel.textContent = "URL";
567
+ var urlInput = document.createElement("input");
568
+ urlInput.type = "text";
569
+ urlInput.value = origHref;
570
+
571
+ var actions = document.createElement("div");
572
+ actions.className = "vibespot-link-edit-actions";
573
+ var cancelBtn = document.createElement("button");
574
+ cancelBtn.className = "vibespot-btn-cancel";
575
+ cancelBtn.textContent = "Cancel";
576
+ var saveBtn = document.createElement("button");
577
+ saveBtn.className = "vibespot-btn-save";
578
+ saveBtn.textContent = "Save";
579
+ actions.appendChild(cancelBtn);
580
+ actions.appendChild(saveBtn);
581
+
582
+ popup.appendChild(textLabel);
583
+ popup.appendChild(textInput);
584
+ popup.appendChild(urlLabel);
585
+ popup.appendChild(urlInput);
586
+ popup.appendChild(actions);
587
+ document.body.appendChild(popup);
588
+ textInput.focus();
589
+
590
+ var close = function () {
591
+ if (popup.parentNode) popup.parentNode.removeChild(popup);
592
+ activeEditor = null;
593
+ };
594
+
595
+ cancelBtn.addEventListener("click", close);
596
+ saveBtn.addEventListener("click", function () {
597
+ var newText = textInput.value.trim();
598
+ var newUrl = urlInput.value.trim();
599
+ if (newText) anchorEl.textContent = newText;
600
+ if (newUrl) anchorEl.href = newUrl;
601
+ if (newText !== origText || newUrl !== origHref) {
602
+ send(OUT.EDIT_COMMIT, {
603
+ kind: "link",
604
+ moduleName: moduleName,
605
+ linkField: anchorEl.getAttribute("data-vs-link") || null,
606
+ textField: anchorEl.getAttribute("data-vs-field") || null,
607
+ origText: origText,
608
+ origHref: origHref,
609
+ newText: newText,
610
+ newUrl: newUrl,
611
+ });
612
+ }
613
+ close();
614
+ });
615
+
616
+ popup.addEventListener("keydown", function (e) {
617
+ if (e.key === "Escape") close();
618
+ if (e.key === "Enter") { e.preventDefault(); saveBtn.click(); }
619
+ });
620
+
621
+ activeEditor = { cleanup: close };
622
+ }
623
+
624
+ var onClick = function (e) {
625
+ // Clicks inside the agent's own edit UI (link popup, image URL input)
626
+ // must reach their own listeners — Save/Cancel live there. Guard before
627
+ // preventDefault/stopPropagation or the popup can never commit.
628
+ if (e.target.closest && e.target.closest(".vibespot-link-edit-popup, .vibespot-image-edit-input")) return;
629
+ e.preventDefault();
630
+ e.stopPropagation();
631
+
632
+ var info = getInteractInfo(e.target);
633
+ if (!info) return;
634
+
635
+ closeActiveEditor();
636
+ cleanup();
637
+
638
+ if (info.action === "select") {
639
+ send(OUT.SELECT_MODULE, {
640
+ module: info.el.getAttribute("data-module") || "",
641
+ prefill: buildChatPrefill(info.el),
642
+ });
643
+ return;
644
+ }
645
+
646
+ var modEl = info.el.closest("[data-module]");
647
+ if (!modEl) return;
648
+ var moduleName = modEl.getAttribute("data-module");
649
+
650
+ if (info.type === "text") startTextEdit(info.el, moduleName);
651
+ else if (info.type === "image") startImageEdit(info.el, moduleName);
652
+ else if (info.type === "link") startLinkEdit(info.el, moduleName);
653
+ };
654
+
655
+ var onKeyDown = function (e) {
656
+ // Escape closes an open editor first; a second Escape asks the parent
657
+ // to leave interact mode. vs:request-mode is read-only — the parent
658
+ // validates it, flips its own toolbar state, and answers vs:set-mode.
659
+ if (e.key !== "Escape") return;
660
+ if (closeActiveEditor()) return;
661
+ send(OUT.REQUEST_MODE, { mode: "view" });
662
+ };
663
+
664
+ document.addEventListener("mouseover", onMouseOver, true);
665
+ document.addEventListener("mouseout", onMouseOut, true);
666
+ document.addEventListener("click", onClick, true);
667
+ document.addEventListener("keydown", onKeyDown, true);
668
+
669
+ interactState = {
670
+ onMouseOver: onMouseOver,
671
+ onMouseOut: onMouseOut,
672
+ onClick: onClick,
673
+ onKeyDown: onKeyDown,
674
+ cleanup: cleanup,
675
+ closeActiveEditor: closeActiveEditor,
676
+ };
677
+ }
678
+
679
+ function detachInteract() {
680
+ if (!interactAttached || !interactState) { interactAttached = false; return; }
681
+ interactState.closeActiveEditor();
682
+ interactState.cleanup();
683
+ document.documentElement.classList.remove("vibespot-interact-mode");
684
+ document.removeEventListener("mouseover", interactState.onMouseOver, true);
685
+ document.removeEventListener("mouseout", interactState.onMouseOut, true);
686
+ document.removeEventListener("click", interactState.onClick, true);
687
+ document.removeEventListener("keydown", interactState.onKeyDown, true);
688
+ interactState = null;
689
+ interactAttached = false;
690
+ }
691
+
692
+ // ---- section controls (ported from ui/section-controls.js) ---------------
693
+ (function sectionControls() {
694
+ var activeToolbar = null;
695
+ var activeModule = null;
696
+ var activePopover = null;
697
+ var hideTimer = null;
698
+ var updateTimer = null;
699
+ var interacting = false;
700
+ var pendingUpdate = null;
701
+
702
+ onDomReady(function () {
703
+ if (!document.body) return;
704
+ ensureSectionStyles();
705
+
706
+ document.addEventListener("mouseover", function (e) {
707
+ if (mode === "interact") return; // inline editing owns the pointer
708
+ if (interacting) return;
709
+ var modEl = e.target.closest("[data-module]");
710
+ if (!modEl) return;
711
+ if (modEl === activeModule) { clearTimeout(hideTimer); return; }
712
+ clearTimeout(hideTimer);
713
+ showToolbar(modEl);
714
+ }, true);
715
+
716
+ document.addEventListener("mouseout", function (e) {
717
+ if (!activeToolbar || interacting) return;
718
+ var related = e.relatedTarget;
719
+ if (related) {
720
+ if (activeToolbar.contains(related)) return;
721
+ if (activeModule && activeModule.contains(related)) return;
722
+ if (activePopover && activePopover.contains(related)) return;
723
+ }
724
+ hideTimer = setTimeout(hideToolbar, 250);
725
+ }, true);
726
+
727
+ document.addEventListener("keydown", function (e) {
728
+ if (e.key === "Escape" && activePopover) closePopover();
729
+ }, true);
730
+ });
731
+
732
+ function showToolbar(modEl) {
733
+ hideToolbar();
734
+ var moduleName = modEl.getAttribute("data-module");
735
+ if (!moduleName) return;
736
+ var fields = FIELDS[moduleName];
737
+ if (!Array.isArray(fields)) return;
738
+ activeModule = modEl;
739
+
740
+ var controls = categorizeFields(fields, "");
741
+ if (!controls.length) return;
742
+
743
+ var toolbar = document.createElement("div");
744
+ toolbar.className = "vs-sc-toolbar";
745
+ controls.forEach(function (ctrl) {
746
+ toolbar.appendChild(createControl(ctrl, moduleName));
747
+ });
748
+
749
+ var computed = window.getComputedStyle(modEl);
750
+ if (computed.position === "static") modEl.style.position = "relative";
751
+ modEl.appendChild(toolbar);
752
+ activeToolbar = toolbar;
753
+
754
+ toolbar.addEventListener("mouseenter", function () { clearTimeout(hideTimer); });
755
+ toolbar.addEventListener("mouseleave", function (e) {
756
+ if (interacting) return;
757
+ if (activeModule && activeModule.contains(e.relatedTarget)) return;
758
+ if (activePopover && activePopover.contains(e.relatedTarget)) return;
759
+ hideTimer = setTimeout(hideToolbar, 250);
760
+ });
761
+ }
762
+
763
+ function hideToolbar() {
764
+ if (interacting) return;
765
+ closePopover();
766
+ if (activeToolbar && activeToolbar.parentNode) activeToolbar.parentNode.removeChild(activeToolbar);
767
+ activeToolbar = null;
768
+ activeModule = null;
769
+ }
770
+
771
+ // Mode switches must clear section chrome even mid-popover (a pending
772
+ // change is still flushed by closePopover).
773
+ hideSectionChrome = function () {
774
+ clearTimeout(hideTimer);
775
+ closePopover();
776
+ interacting = false;
777
+ hideToolbar();
778
+ };
779
+
780
+ function categorizeFields(fields, prefix) {
781
+ var controls = [];
782
+ fields.forEach(function (field) {
783
+ if (!field || typeof field !== "object") return;
784
+ var path = prefix ? prefix + "." + field.name : field.name;
785
+ if (field.type === "group" && field.children) {
786
+ controls.push.apply(controls, categorizeFields(field.children, path));
787
+ return;
788
+ }
789
+ var n = String(field.name || "").toLowerCase();
790
+ if (field.type === "color") controls.push({ kind: "color", field: field, path: path, label: field.label || field.name });
791
+ else if (field.type === "image") controls.push({ kind: "image", field: field, path: path, label: field.label || field.name });
792
+ else if (field.type === "number" && /padding|margin|spacing/i.test(n)) controls.push({ kind: "spacing", field: field, path: path, label: field.label || field.name });
793
+ else if (field.type === "number" && /font.?size|text.?size/i.test(n)) controls.push({ kind: "fontsize", field: field, path: path, label: field.label || field.name });
794
+ });
795
+ return controls;
796
+ }
797
+
798
+ function shortLabel(label) {
799
+ label = String(label || "");
800
+ return label.length <= 12 ? label : label.slice(0, 10) + "…";
801
+ }
802
+
803
+ function createControl(ctrl, moduleName) {
804
+ var btn = document.createElement("button");
805
+ btn.className = "vs-sc-btn";
806
+ btn.setAttribute("data-kind", ctrl.kind);
807
+ btn.title = ctrl.label;
808
+
809
+ switch (ctrl.kind) {
810
+ case "color": {
811
+ var swatch = document.createElement("span");
812
+ swatch.className = "vs-sc-swatch";
813
+ swatch.style.background = (ctrl.field.default && ctrl.field.default.color) || "#888";
814
+ btn.appendChild(swatch);
815
+ var lbl = document.createElement("span");
816
+ lbl.className = "vs-sc-label";
817
+ lbl.textContent = shortLabel(ctrl.label);
818
+ btn.appendChild(lbl);
819
+ break;
820
+ }
821
+ case "spacing":
822
+ btn.innerHTML =
823
+ '<svg class="vs-sc-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3v18M3 12h18M7 8l5-5 5 5M7 16l5 5 5-5"/></svg>' +
824
+ '<span class="vs-sc-label">' + (ctrl.field.default != null ? ctrl.field.default : 0) + "px</span>";
825
+ break;
826
+ case "image":
827
+ btn.innerHTML =
828
+ '<svg class="vs-sc-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><path d="M21 15l-5-5L5 21"/></svg>' +
829
+ '<span class="vs-sc-label">' + shortLabel(ctrl.label) + "</span>";
830
+ break;
831
+ case "fontsize":
832
+ btn.innerHTML =
833
+ '<span class="vs-sc-icon" style="font-weight:600;font-size:11px">Aa</span>' +
834
+ '<span class="vs-sc-label">' + (ctrl.field.default != null ? ctrl.field.default : 16) + "px</span>";
835
+ break;
836
+ }
837
+
838
+ btn.addEventListener("click", function (e) {
839
+ e.preventDefault();
840
+ e.stopPropagation();
841
+ openPopover(btn, ctrl, moduleName);
842
+ });
843
+ return btn;
844
+ }
845
+
846
+ function openPopover(anchor, ctrl, moduleName) {
847
+ closePopover();
848
+ interacting = true;
849
+
850
+ var pop = document.createElement("div");
851
+ pop.className = "vs-sc-popover";
852
+
853
+ switch (ctrl.kind) {
854
+ case "color": buildColorPopover(pop, anchor, ctrl, moduleName); break;
855
+ case "spacing": buildSliderPopover(pop, anchor, ctrl, moduleName, 0, 120, 0, true); break;
856
+ case "image": buildImagePopover(pop, ctrl, moduleName); break;
857
+ case "fontsize": buildSliderPopover(pop, anchor, ctrl, moduleName, 8, 96, 16, false); break;
858
+ }
859
+
860
+ var rect = anchor.getBoundingClientRect();
861
+ pop.style.top = rect.bottom + 6 + "px";
862
+ pop.style.left = Math.max(4, rect.left) + "px";
863
+ document.body.appendChild(pop);
864
+ activePopover = pop;
865
+
866
+ pop.addEventListener("mouseenter", function () { clearTimeout(hideTimer); });
867
+ pop.addEventListener("mouseleave", function (e) {
868
+ if (activeToolbar && activeToolbar.contains(e.relatedTarget)) return;
869
+ if (activeModule && activeModule.contains(e.relatedTarget)) return;
870
+ closePopover();
871
+ });
872
+ }
873
+
874
+ function closePopover() {
875
+ if (activePopover && activePopover.parentNode) activePopover.parentNode.removeChild(activePopover);
876
+ activePopover = null;
877
+ if (interacting) {
878
+ interacting = false;
879
+ flushUpdate(true);
880
+ }
881
+ }
882
+
883
+ function buildColorPopover(pop, anchor, ctrl, moduleName) {
884
+ var picker = document.createElement("input");
885
+ picker.type = "color";
886
+ picker.className = "vs-sc-color-picker";
887
+ picker.value = (ctrl.field.default && ctrl.field.default.color) || "#000000";
888
+
889
+ var hex = document.createElement("input");
890
+ hex.type = "text";
891
+ hex.className = "vs-sc-hex";
892
+ hex.value = picker.value;
893
+
894
+ var row = document.createElement("div");
895
+ row.className = "vs-sc-color-row";
896
+ row.appendChild(picker);
897
+ row.appendChild(hex);
898
+ pop.appendChild(row);
899
+
900
+ var apply = function (color) {
901
+ var swatch = anchor.querySelector(".vs-sc-swatch");
902
+ if (swatch) swatch.style.background = color;
903
+ queueUpdate(moduleName, ctrl.path, {
904
+ color: color,
905
+ opacity: (ctrl.field.default && ctrl.field.default.opacity) != null ? ctrl.field.default.opacity : 100,
906
+ });
907
+ };
908
+
909
+ picker.addEventListener("input", function () {
910
+ hex.value = picker.value;
911
+ apply(picker.value);
912
+ });
913
+ hex.addEventListener("change", function () {
914
+ if (/^#[0-9a-f]{3,8}$/i.test(hex.value)) {
915
+ picker.value = hex.value.length === 4
916
+ ? "#" + hex.value[1] + hex.value[1] + hex.value[2] + hex.value[2] + hex.value[3] + hex.value[3]
917
+ : hex.value;
918
+ apply(hex.value);
919
+ }
920
+ });
921
+ }
922
+
923
+ function buildSliderPopover(pop, anchor, ctrl, moduleName, min, max, fallback, withLabel) {
924
+ if (withLabel) {
925
+ var lbl = document.createElement("div");
926
+ lbl.className = "vs-sc-pop-label";
927
+ lbl.textContent = ctrl.label;
928
+ pop.appendChild(lbl);
929
+ }
930
+ var row = document.createElement("div");
931
+ row.className = "vs-sc-slider-row";
932
+
933
+ var slider = document.createElement("input");
934
+ slider.type = "range";
935
+ slider.className = "vs-sc-slider";
936
+ slider.min = String(min);
937
+ slider.max = String(max);
938
+ slider.step = "1";
939
+ slider.value = ctrl.field.default != null ? ctrl.field.default : fallback;
940
+
941
+ var val = document.createElement("span");
942
+ val.className = "vs-sc-slider-val";
943
+ val.textContent = slider.value + "px";
944
+
945
+ slider.addEventListener("input", function () {
946
+ val.textContent = slider.value + "px";
947
+ var btnLabel = anchor.querySelector(".vs-sc-label");
948
+ if (btnLabel) btnLabel.textContent = slider.value + "px";
949
+ queueUpdate(moduleName, ctrl.path, Number(slider.value));
950
+ });
951
+
952
+ row.appendChild(slider);
953
+ row.appendChild(val);
954
+ pop.appendChild(row);
955
+ }
956
+
957
+ function buildImagePopover(pop, ctrl, moduleName) {
958
+ var lbl = document.createElement("div");
959
+ lbl.className = "vs-sc-pop-label";
960
+ lbl.textContent = ctrl.label;
961
+
962
+ var input = document.createElement("input");
963
+ input.type = "text";
964
+ input.className = "vs-sc-url-input";
965
+ input.placeholder = "Enter image URL…";
966
+ input.value = (ctrl.field.default && ctrl.field.default.src) || "";
967
+
968
+ var btn = document.createElement("button");
969
+ btn.className = "vs-sc-apply-btn";
970
+ btn.textContent = "Apply";
971
+
972
+ btn.addEventListener("click", function () {
973
+ var src = input.value.trim();
974
+ if (src) {
975
+ queueUpdate(moduleName, ctrl.path, {
976
+ src: src,
977
+ alt: (ctrl.field.default && ctrl.field.default.alt) || "",
978
+ });
979
+ closePopover();
980
+ }
981
+ });
982
+
983
+ input.addEventListener("keydown", function (e) {
984
+ if (e.key === "Enter") { e.preventDefault(); btn.click(); }
985
+ if (e.key === "Escape") closePopover();
986
+ });
987
+
988
+ pop.appendChild(lbl);
989
+ pop.appendChild(input);
990
+ pop.appendChild(btn);
991
+ input.focus();
992
+ }
993
+
994
+ function queueUpdate(moduleName, fieldPath, value) {
995
+ pendingUpdate = { moduleName: moduleName, fieldPath: fieldPath, value: value };
996
+ clearTimeout(updateTimer);
997
+ updateTimer = setTimeout(function () { flushUpdate(false); }, 300);
998
+ }
999
+
1000
+ function flushUpdate(popoverClosed) {
1001
+ clearTimeout(updateTimer);
1002
+ var upd = pendingUpdate;
1003
+ if (!upd) return;
1004
+ pendingUpdate = null;
1005
+ send(OUT.FIELD_COMMIT, {
1006
+ moduleName: upd.moduleName,
1007
+ fieldPath: upd.fieldPath,
1008
+ value: upd.value,
1009
+ // While the popover is open the user is still dragging — the parent
1010
+ // persists but defers the reload so the controls don't vanish.
1011
+ refresh: popoverClosed || !interacting,
1012
+ });
1013
+ }
1014
+
1015
+ function ensureSectionStyles() {
1016
+ if (document.getElementById("vs-section-controls-css")) return;
1017
+ ensureTokenStyles();
1018
+ var s = document.createElement("style");
1019
+ s.id = "vs-section-controls-css";
1020
+ s.textContent = "\n" +
1021
+ '.vs-sc-toolbar{position:absolute;top:8px;right:8px;z-index:10000;display:flex;align-items:center;gap:2px;background:rgba(15,13,12,.88);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:3px;box-shadow:0 4px 16px rgba(0,0,0,.35);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;animation:vs-sc-fadein .15s ease;pointer-events:auto}\n' +
1022
+ "@keyframes vs-sc-fadein{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}\n" +
1023
+ ".vs-sc-btn{display:flex;align-items:center;gap:4px;padding:4px 8px;background:transparent;border:none;border-radius:5px;color:rgba(255,255,255,.8);font:500 11px/1.3 -apple-system,BlinkMacSystemFont,sans-serif;cursor:pointer;white-space:nowrap;transition:background .15s}\n" +
1024
+ ".vs-sc-btn:hover{background:rgba(255,255,255,.1);color:#fff}\n" +
1025
+ ".vs-sc-swatch{display:inline-block;width:14px;height:14px;border-radius:3px;border:1px solid rgba(255,255,255,.25);flex-shrink:0}\n" +
1026
+ ".vs-sc-icon{flex-shrink:0;opacity:.7}\n" +
1027
+ ".vs-sc-label{max-width:72px;overflow:hidden;text-overflow:ellipsis}\n" +
1028
+ '.vs-sc-popover{position:fixed;z-index:10001;background:rgba(15,13,12,.95);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.12);border-radius:8px;padding:10px;box-shadow:0 6px 24px rgba(0,0,0,.45);font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;animation:vs-sc-fadein .12s ease;min-width:180px}\n' +
1029
+ ".vs-sc-pop-label{font:500 11px/1.4 -apple-system,sans-serif;color:rgba(255,255,255,.5);margin-bottom:6px;text-transform:uppercase;letter-spacing:.4px}\n" +
1030
+ ".vs-sc-color-row{display:flex;align-items:center;gap:8px}\n" +
1031
+ ".vs-sc-color-picker{width:36px;height:28px;border:none;border-radius:4px;padding:0;cursor:pointer;background:transparent}\n" +
1032
+ ".vs-sc-color-picker::-webkit-color-swatch-wrapper{padding:0}\n" +
1033
+ ".vs-sc-color-picker::-webkit-color-swatch{border:1px solid rgba(255,255,255,.2);border-radius:4px}\n" +
1034
+ ".vs-sc-color-picker::-moz-color-swatch{border:1px solid rgba(255,255,255,.2);border-radius:4px}\n" +
1035
+ ".vs-sc-hex{flex:1;font:12px/1.4 -apple-system,sans-serif;padding:4px 6px;border:1px solid rgba(255,255,255,.15);border-radius:4px;background:rgba(0,0,0,.3);color:#fff;outline:none;width:80px}\n" +
1036
+ ".vs-sc-hex:focus{border-color:var(--vs-accent)}\n" +
1037
+ ".vs-sc-slider-row{display:flex;align-items:center;gap:8px}\n" +
1038
+ ".vs-sc-slider{flex:1;height:4px;-webkit-appearance:none;appearance:none;background:rgba(255,255,255,.15);border-radius:2px;outline:none;cursor:pointer}\n" +
1039
+ ".vs-sc-slider::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%;background:var(--vs-accent);border:2px solid #fff;cursor:pointer}\n" +
1040
+ ".vs-sc-slider::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--vs-accent);border:2px solid #fff;cursor:pointer}\n" +
1041
+ ".vs-sc-slider-val{font:500 12px/1 -apple-system,sans-serif;color:rgba(255,255,255,.7);min-width:36px;text-align:right}\n" +
1042
+ ".vs-sc-url-input{width:100%;font:12px/1.4 -apple-system,sans-serif;padding:6px 8px;border:1px solid rgba(255,255,255,.15);border-radius:5px;background:rgba(0,0,0,.3);color:#fff;outline:none;margin-bottom:6px;box-sizing:border-box}\n" +
1043
+ ".vs-sc-url-input:focus{border-color:var(--vs-accent)}\n" +
1044
+ ".vs-sc-apply-btn{display:block;width:100%;font:500 12px/1 -apple-system,sans-serif;padding:6px 12px;border:none;border-radius:5px;background:var(--vs-accent);color:#fff;cursor:pointer;transition:background .15s}\n" +
1045
+ ".vs-sc-apply-btn:hover{background:var(--vs-accent-hover)}\n" +
1046
+ "@media (prefers-reduced-motion:reduce){.vs-sc-toolbar,.vs-sc-popover{animation:none}}";
1047
+ document.head.appendChild(s);
1048
+ }
1049
+ })();
1050
+ })();