vibespot 1.2.0 → 1.3.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.
- package/README.md +44 -5
- package/assets/blog-rules.md +251 -0
- package/assets/email-rules.md +390 -0
- package/assets/humanify-guide.md +300 -101
- package/assets/plan-templates/blog-content-hub.md +18 -9
- package/assets/plan-templates/email-announcement.md +41 -0
- package/assets/plan-templates/email-event-invite.md +43 -0
- package/assets/plan-templates/email-newsletter.md +41 -0
- package/assets/plan-templates/email-re-engagement.md +42 -0
- package/assets/plan-templates/email-welcome.md +41 -0
- package/dist/index.js +1460 -387
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/starters/06-blog-content-hub.json +75 -0
- package/starters/06-email-welcome.json +60 -0
- package/starters/07-email-announcement.json +60 -0
- package/starters/08-email-newsletter.json +52 -0
- package/ui/chat.js +777 -63
- package/ui/code-editor.js +49 -7
- package/ui/dashboard.js +379 -93
- package/ui/docs/docs.css +29 -0
- package/ui/docs/index.html +186 -108
- package/ui/docs/screenshots/brand-kit-preview.png +0 -0
- package/ui/docs/screenshots/content-type-dropdown.png +0 -0
- package/ui/docs/screenshots/editor-full-layout.png +0 -0
- package/ui/docs/screenshots/inline-wysiwyg-editing.png +0 -0
- package/ui/docs/screenshots/multi-page-tree.png +0 -0
- package/ui/docs/screenshots/onboarding-walkthrough.png +0 -0
- package/ui/docs/screenshots/split-pane-view.png +0 -0
- package/ui/docs/screenshots/visual-controls-toolbar.png +0 -0
- package/ui/docs/screenshots/workspace-tabs.png +0 -0
- package/ui/email-preview.js +109 -0
- package/ui/field-editor.js +72 -1
- package/ui/icons.js +120 -0
- package/ui/index.html +877 -629
- package/ui/inline-edit.js +710 -0
- package/ui/plan.js +0 -0
- package/ui/preview.js +101 -198
- package/ui/section-controls.js +628 -0
- package/ui/settings.js +58 -16
- package/ui/setup.js +750 -140
- package/ui/styles.css +3430 -952
- package/ui/upload-panel.js +47 -20
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified interact mode — merged Select + Edit into one contextual model.
|
|
3
|
+
*
|
|
4
|
+
* Editable elements (text, images, links) get inline editing.
|
|
5
|
+
* Module-level containers prefill the chat input for referencing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const interactBtn = document.getElementById("interact-mode-toggle");
|
|
9
|
+
let interactModeActive = false;
|
|
10
|
+
let interactHandlers = null;
|
|
11
|
+
|
|
12
|
+
function ensureInteractStyles(doc) {
|
|
13
|
+
if (doc.getElementById("vibespot-interact-css")) return;
|
|
14
|
+
const style = doc.createElement("style");
|
|
15
|
+
style.id = "vibespot-interact-css";
|
|
16
|
+
style.textContent = `
|
|
17
|
+
html.vibespot-interact-mode { cursor: default; }
|
|
18
|
+
.vibespot-editable-hover {
|
|
19
|
+
outline: 2px dashed rgba(59, 130, 246, 0.6) !important;
|
|
20
|
+
outline-offset: 2px !important;
|
|
21
|
+
cursor: text !important;
|
|
22
|
+
}
|
|
23
|
+
.vibespot-editable-hover[data-edit-type="image"] {
|
|
24
|
+
cursor: pointer !important;
|
|
25
|
+
}
|
|
26
|
+
.vibespot-editable-hover[data-edit-type="select"] {
|
|
27
|
+
outline-color: #e8613a !important;
|
|
28
|
+
outline-style: solid !important;
|
|
29
|
+
background-color: rgba(232, 97, 58, 0.08) !important;
|
|
30
|
+
cursor: crosshair !important;
|
|
31
|
+
}
|
|
32
|
+
.vibespot-editing {
|
|
33
|
+
outline: 2px solid rgba(59, 130, 246, 0.9) !important;
|
|
34
|
+
outline-offset: 2px !important;
|
|
35
|
+
background-color: rgba(59, 130, 246, 0.04) !important;
|
|
36
|
+
min-height: 1em;
|
|
37
|
+
}
|
|
38
|
+
.vibespot-edit-label {
|
|
39
|
+
position: fixed;
|
|
40
|
+
z-index: 2147483647;
|
|
41
|
+
pointer-events: none;
|
|
42
|
+
font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
43
|
+
color: #fff;
|
|
44
|
+
padding: 2px 7px;
|
|
45
|
+
border-radius: 3px;
|
|
46
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.2);
|
|
47
|
+
white-space: nowrap;
|
|
48
|
+
}
|
|
49
|
+
.vibespot-edit-label--edit {
|
|
50
|
+
background: #3b82f6;
|
|
51
|
+
}
|
|
52
|
+
.vibespot-edit-label--select {
|
|
53
|
+
background: #e8613a;
|
|
54
|
+
}
|
|
55
|
+
.vibespot-image-edit-input {
|
|
56
|
+
position: fixed;
|
|
57
|
+
z-index: 2147483647;
|
|
58
|
+
font: 13px/1.4 -apple-system, BlinkMacSystemFont, sans-serif;
|
|
59
|
+
padding: 6px 10px;
|
|
60
|
+
border: 2px solid #3b82f6;
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
background: #1c1917;
|
|
63
|
+
color: #fff;
|
|
64
|
+
width: 320px;
|
|
65
|
+
outline: none;
|
|
66
|
+
}
|
|
67
|
+
.vibespot-link-edit-popup {
|
|
68
|
+
position: fixed;
|
|
69
|
+
z-index: 2147483647;
|
|
70
|
+
background: #1c1917;
|
|
71
|
+
border: 1px solid rgba(255,255,255,0.15);
|
|
72
|
+
border-radius: 8px;
|
|
73
|
+
padding: 10px;
|
|
74
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.4);
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: 6px;
|
|
78
|
+
}
|
|
79
|
+
.vibespot-link-edit-popup label {
|
|
80
|
+
font: 500 11px/1.4 -apple-system, sans-serif;
|
|
81
|
+
color: rgba(255,255,255,0.6);
|
|
82
|
+
}
|
|
83
|
+
.vibespot-link-edit-popup input {
|
|
84
|
+
font: 13px/1.4 -apple-system, sans-serif;
|
|
85
|
+
padding: 5px 8px;
|
|
86
|
+
border: 1px solid rgba(255,255,255,0.2);
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
background: #0c0a09;
|
|
89
|
+
color: #fff;
|
|
90
|
+
outline: none;
|
|
91
|
+
width: 260px;
|
|
92
|
+
}
|
|
93
|
+
.vibespot-link-edit-popup input:focus { border-color: #3b82f6; }
|
|
94
|
+
.vibespot-link-edit-popup .vibespot-link-edit-actions {
|
|
95
|
+
display: flex; gap: 6px; justify-content: flex-end; margin-top: 4px;
|
|
96
|
+
}
|
|
97
|
+
.vibespot-link-edit-popup button {
|
|
98
|
+
font: 500 12px/1 -apple-system, sans-serif;
|
|
99
|
+
padding: 5px 12px;
|
|
100
|
+
border: none; border-radius: 4px;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
}
|
|
103
|
+
.vibespot-link-edit-popup .vibespot-btn-save {
|
|
104
|
+
background: #3b82f6; color: #fff;
|
|
105
|
+
}
|
|
106
|
+
.vibespot-link-edit-popup .vibespot-btn-cancel {
|
|
107
|
+
background: rgba(255,255,255,0.1); color: rgba(255,255,255,0.7);
|
|
108
|
+
}
|
|
109
|
+
`;
|
|
110
|
+
doc.head.appendChild(style);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Determine if element is directly editable (text/image/link) or module-level
|
|
114
|
+
function getInteractInfo(el) {
|
|
115
|
+
if (!el) return null;
|
|
116
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
117
|
+
|
|
118
|
+
// Check for explicit field annotations first
|
|
119
|
+
const annotated = el.closest("[data-vs-field]");
|
|
120
|
+
if (annotated) {
|
|
121
|
+
const vsType = annotated.getAttribute("data-vs-type");
|
|
122
|
+
if (vsType === "image") return { type: "image", el: annotated, action: "edit" };
|
|
123
|
+
if (vsType === "link") return { type: "link", el: annotated, action: "edit" };
|
|
124
|
+
return { type: "text", el: annotated, action: "edit" };
|
|
125
|
+
}
|
|
126
|
+
const linkAnnotated = el.closest("[data-vs-link]");
|
|
127
|
+
if (linkAnnotated) return { type: "link", el: linkAnnotated, action: "edit" };
|
|
128
|
+
|
|
129
|
+
// Heuristic detection for editable elements
|
|
130
|
+
if (tag === "img") return { type: "image", el, action: "edit" };
|
|
131
|
+
if (tag === "a" || (el.closest && el.closest("a"))) {
|
|
132
|
+
const anchor = tag === "a" ? el : el.closest("a");
|
|
133
|
+
return { type: "link", el: anchor, action: "edit" };
|
|
134
|
+
}
|
|
135
|
+
if (tag.match(/^h[1-6]$/) || tag === "p" || tag === "span" || tag === "li" || tag === "td" || tag === "th") {
|
|
136
|
+
if (el.children.length === 0 || (el.children.length === 1 && el.children[0].tagName === "BR")) {
|
|
137
|
+
return { type: "text", el, action: "edit" };
|
|
138
|
+
}
|
|
139
|
+
if (el.textContent && el.textContent.trim().length > 0 && el.childElementCount <= 2) {
|
|
140
|
+
return { type: "text", el, action: "edit" };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (tag === "button") return { type: "text", el, action: "edit" };
|
|
144
|
+
|
|
145
|
+
// Module-level container → select mode (prefill chat)
|
|
146
|
+
const moduleEl = el.closest("[data-module]");
|
|
147
|
+
if (moduleEl) return { type: "select", el: moduleEl, action: "select" };
|
|
148
|
+
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function describeElement(el) {
|
|
153
|
+
if (!el) return "";
|
|
154
|
+
const moduleEl = el.closest("[data-module]");
|
|
155
|
+
const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
|
|
156
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
157
|
+
let kind = tag;
|
|
158
|
+
if (tag.match(/^h[1-6]$/)) kind = "headline";
|
|
159
|
+
else if (tag === "p") kind = "paragraph";
|
|
160
|
+
else if (tag === "a") kind = "link";
|
|
161
|
+
else if (tag === "button") kind = "button";
|
|
162
|
+
else if (tag === "img") kind = "image";
|
|
163
|
+
else if (tag === "ul" || tag === "ol") kind = "list";
|
|
164
|
+
else if (tag === "li") kind = "list item";
|
|
165
|
+
|
|
166
|
+
if (moduleName && moduleEl === el) return moduleName;
|
|
167
|
+
if (moduleName) return `${moduleName} > ${kind}`;
|
|
168
|
+
return kind;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function buildChatPrefill(el) {
|
|
172
|
+
if (!el) return "";
|
|
173
|
+
const moduleEl = el.closest("[data-module]");
|
|
174
|
+
const moduleName = moduleEl ? moduleEl.getAttribute("data-module") : null;
|
|
175
|
+
const tag = (el.tagName || "").toLowerCase();
|
|
176
|
+
const text = (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 80);
|
|
177
|
+
|
|
178
|
+
let elementPart;
|
|
179
|
+
if (tag.match(/^h[1-6]$/)) elementPart = "the headline";
|
|
180
|
+
else if (tag === "p") elementPart = "the paragraph";
|
|
181
|
+
else if (tag === "a") elementPart = "the link";
|
|
182
|
+
else if (tag === "button") elementPart = "the button";
|
|
183
|
+
else if (tag === "img") elementPart = "the image";
|
|
184
|
+
else elementPart = `the ${tag}`;
|
|
185
|
+
|
|
186
|
+
if (moduleEl === el && moduleName) {
|
|
187
|
+
return `In the ${moduleName} module, `;
|
|
188
|
+
}
|
|
189
|
+
if (moduleName) {
|
|
190
|
+
const quote = text ? ` ("${text}${text.length === 80 ? "…" : ""}")` : "";
|
|
191
|
+
return `In the ${moduleName} module, ${elementPart}${quote} `;
|
|
192
|
+
}
|
|
193
|
+
return `${elementPart} `;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function attachInteractHandlers() {
|
|
197
|
+
let doc;
|
|
198
|
+
try {
|
|
199
|
+
doc = previewFrame.contentDocument || previewFrame.contentWindow?.document;
|
|
200
|
+
} catch { return; }
|
|
201
|
+
if (!doc || !doc.body) return;
|
|
202
|
+
|
|
203
|
+
ensureInteractStyles(doc);
|
|
204
|
+
doc.documentElement.classList.add("vibespot-interact-mode");
|
|
205
|
+
|
|
206
|
+
let hoveredEl = null;
|
|
207
|
+
let labelEl = null;
|
|
208
|
+
let activeEditor = null;
|
|
209
|
+
|
|
210
|
+
const cleanup = () => {
|
|
211
|
+
if (hoveredEl) {
|
|
212
|
+
hoveredEl.classList.remove("vibespot-editable-hover", "vibespot-select-hover");
|
|
213
|
+
hoveredEl.removeAttribute("data-edit-type");
|
|
214
|
+
hoveredEl = null;
|
|
215
|
+
}
|
|
216
|
+
if (labelEl && labelEl.parentNode) {
|
|
217
|
+
labelEl.parentNode.removeChild(labelEl);
|
|
218
|
+
}
|
|
219
|
+
labelEl = null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const onMouseOver = (e) => {
|
|
223
|
+
if (activeEditor) return;
|
|
224
|
+
const info = getInteractInfo(e.target);
|
|
225
|
+
if (!info) {
|
|
226
|
+
cleanup();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (hoveredEl === info.el) return;
|
|
230
|
+
cleanup();
|
|
231
|
+
hoveredEl = info.el;
|
|
232
|
+
hoveredEl.classList.add("vibespot-editable-hover");
|
|
233
|
+
hoveredEl.setAttribute("data-edit-type", info.action === "select" ? "select" : info.type);
|
|
234
|
+
|
|
235
|
+
if (!labelEl) {
|
|
236
|
+
labelEl = doc.createElement("div");
|
|
237
|
+
labelEl.className = "vibespot-edit-label";
|
|
238
|
+
doc.body.appendChild(labelEl);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (info.action === "select") {
|
|
242
|
+
labelEl.className = "vibespot-edit-label vibespot-edit-label--select";
|
|
243
|
+
labelEl.textContent = describeElement(info.el);
|
|
244
|
+
} else {
|
|
245
|
+
labelEl.className = "vibespot-edit-label vibespot-edit-label--edit";
|
|
246
|
+
const typeLabel = info.type === "image" ? "Click to edit image" : info.type === "link" ? "Click to edit link" : "Click to edit";
|
|
247
|
+
labelEl.textContent = typeLabel;
|
|
248
|
+
}
|
|
249
|
+
const rect = info.el.getBoundingClientRect();
|
|
250
|
+
labelEl.style.top = Math.max(4, rect.top - 20) + "px";
|
|
251
|
+
labelEl.style.left = Math.max(4, rect.left) + "px";
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const onMouseOut = (e) => {
|
|
255
|
+
if (activeEditor) return;
|
|
256
|
+
if (!e.relatedTarget) cleanup();
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const closeActiveEditor = () => {
|
|
260
|
+
if (!activeEditor) return;
|
|
261
|
+
if (activeEditor.cleanup) activeEditor.cleanup();
|
|
262
|
+
activeEditor = null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const onClick = (e) => {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
e.stopPropagation();
|
|
268
|
+
|
|
269
|
+
const info = getInteractInfo(e.target);
|
|
270
|
+
if (!info) return;
|
|
271
|
+
|
|
272
|
+
closeActiveEditor();
|
|
273
|
+
cleanup();
|
|
274
|
+
|
|
275
|
+
if (info.action === "select") {
|
|
276
|
+
const prefill = buildChatPrefill(info.el);
|
|
277
|
+
if (typeof window.prefillChatInput === "function") {
|
|
278
|
+
window.prefillChatInput(prefill);
|
|
279
|
+
}
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const moduleEl = info.el.closest("[data-module]");
|
|
284
|
+
if (!moduleEl) return;
|
|
285
|
+
const moduleName = moduleEl.getAttribute("data-module");
|
|
286
|
+
|
|
287
|
+
if (info.type === "text") {
|
|
288
|
+
startTextEdit(doc, info.el, moduleName);
|
|
289
|
+
} else if (info.type === "image") {
|
|
290
|
+
startImageEdit(doc, info.el, moduleName);
|
|
291
|
+
} else if (info.type === "link") {
|
|
292
|
+
startLinkEdit(doc, info.el, moduleName);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
function startTextEdit(doc, el, moduleName) {
|
|
297
|
+
const originalText = el.textContent;
|
|
298
|
+
el.setAttribute("data-original-text", originalText.trim());
|
|
299
|
+
el.setAttribute("contenteditable", "true");
|
|
300
|
+
el.classList.add("vibespot-editing");
|
|
301
|
+
el.focus();
|
|
302
|
+
|
|
303
|
+
const range = doc.createRange();
|
|
304
|
+
range.selectNodeContents(el);
|
|
305
|
+
const sel = doc.defaultView.getSelection();
|
|
306
|
+
sel.removeAllRanges();
|
|
307
|
+
sel.addRange(range);
|
|
308
|
+
|
|
309
|
+
const save = () => {
|
|
310
|
+
el.removeAttribute("contenteditable");
|
|
311
|
+
el.classList.remove("vibespot-editing");
|
|
312
|
+
const newText = el.textContent.trim();
|
|
313
|
+
if (newText !== originalText.trim()) {
|
|
314
|
+
saveTextChange(moduleName, el, newText);
|
|
315
|
+
}
|
|
316
|
+
activeEditor = null;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
el.addEventListener("blur", save, { once: true });
|
|
320
|
+
el.addEventListener("keydown", (e) => {
|
|
321
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
322
|
+
e.preventDefault();
|
|
323
|
+
el.blur();
|
|
324
|
+
}
|
|
325
|
+
if (e.key === "Escape") {
|
|
326
|
+
el.textContent = originalText;
|
|
327
|
+
el.blur();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
activeEditor = {
|
|
332
|
+
cleanup: () => {
|
|
333
|
+
el.removeAttribute("contenteditable");
|
|
334
|
+
el.classList.remove("vibespot-editing");
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function startImageEdit(doc, imgEl, moduleName) {
|
|
340
|
+
const rect = imgEl.getBoundingClientRect();
|
|
341
|
+
const input = doc.createElement("input");
|
|
342
|
+
input.className = "vibespot-image-edit-input";
|
|
343
|
+
input.type = "text";
|
|
344
|
+
input.placeholder = "Enter image URL...";
|
|
345
|
+
input.value = imgEl.src || "";
|
|
346
|
+
input.style.top = (rect.bottom + 4) + "px";
|
|
347
|
+
input.style.left = Math.max(4, rect.left) + "px";
|
|
348
|
+
doc.body.appendChild(input);
|
|
349
|
+
input.focus();
|
|
350
|
+
input.select();
|
|
351
|
+
|
|
352
|
+
const save = () => {
|
|
353
|
+
const newSrc = input.value.trim();
|
|
354
|
+
if (input.parentNode) input.parentNode.removeChild(input);
|
|
355
|
+
if (newSrc && newSrc !== imgEl.src) {
|
|
356
|
+
imgEl.src = newSrc;
|
|
357
|
+
saveImageChange(moduleName, imgEl, newSrc);
|
|
358
|
+
}
|
|
359
|
+
activeEditor = null;
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
input.addEventListener("blur", save, { once: true });
|
|
363
|
+
input.addEventListener("keydown", (e) => {
|
|
364
|
+
if (e.key === "Enter") { e.preventDefault(); input.blur(); }
|
|
365
|
+
if (e.key === "Escape") {
|
|
366
|
+
if (input.parentNode) input.parentNode.removeChild(input);
|
|
367
|
+
activeEditor = null;
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
activeEditor = {
|
|
372
|
+
cleanup: () => { if (input.parentNode) input.parentNode.removeChild(input); },
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function startLinkEdit(doc, anchorEl, moduleName) {
|
|
377
|
+
const origText = (anchorEl.textContent || "").trim();
|
|
378
|
+
const origHref = anchorEl.href || "";
|
|
379
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
380
|
+
|
|
381
|
+
const popup = doc.createElement("div");
|
|
382
|
+
popup.className = "vibespot-link-edit-popup";
|
|
383
|
+
popup.style.top = (rect.bottom + 6) + "px";
|
|
384
|
+
popup.style.left = Math.max(4, rect.left) + "px";
|
|
385
|
+
|
|
386
|
+
const textLabel = doc.createElement("label");
|
|
387
|
+
textLabel.textContent = "Link text";
|
|
388
|
+
const textInput = doc.createElement("input");
|
|
389
|
+
textInput.type = "text";
|
|
390
|
+
textInput.className = "vibespot-link-text";
|
|
391
|
+
textInput.value = origText;
|
|
392
|
+
|
|
393
|
+
const urlLabel = doc.createElement("label");
|
|
394
|
+
urlLabel.textContent = "URL";
|
|
395
|
+
const urlInput = doc.createElement("input");
|
|
396
|
+
urlInput.type = "text";
|
|
397
|
+
urlInput.className = "vibespot-link-url";
|
|
398
|
+
urlInput.value = origHref;
|
|
399
|
+
|
|
400
|
+
const actions = doc.createElement("div");
|
|
401
|
+
actions.className = "vibespot-link-edit-actions";
|
|
402
|
+
const cancelBtn = doc.createElement("button");
|
|
403
|
+
cancelBtn.className = "vibespot-btn-cancel";
|
|
404
|
+
cancelBtn.textContent = "Cancel";
|
|
405
|
+
const saveBtn = doc.createElement("button");
|
|
406
|
+
saveBtn.className = "vibespot-btn-save";
|
|
407
|
+
saveBtn.textContent = "Save";
|
|
408
|
+
actions.appendChild(cancelBtn);
|
|
409
|
+
actions.appendChild(saveBtn);
|
|
410
|
+
|
|
411
|
+
popup.appendChild(textLabel);
|
|
412
|
+
popup.appendChild(textInput);
|
|
413
|
+
popup.appendChild(urlLabel);
|
|
414
|
+
popup.appendChild(urlInput);
|
|
415
|
+
popup.appendChild(actions);
|
|
416
|
+
doc.body.appendChild(popup);
|
|
417
|
+
textInput.focus();
|
|
418
|
+
|
|
419
|
+
const close = () => {
|
|
420
|
+
if (popup.parentNode) popup.parentNode.removeChild(popup);
|
|
421
|
+
activeEditor = null;
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
cancelBtn.addEventListener("click", close);
|
|
425
|
+
saveBtn.addEventListener("click", () => {
|
|
426
|
+
const newText = textInput.value.trim();
|
|
427
|
+
const newUrl = urlInput.value.trim();
|
|
428
|
+
if (newText) anchorEl.textContent = newText;
|
|
429
|
+
if (newUrl) anchorEl.href = newUrl;
|
|
430
|
+
if (newText !== origText || newUrl !== origHref) {
|
|
431
|
+
saveLinkChange(moduleName, anchorEl, newText, newUrl);
|
|
432
|
+
}
|
|
433
|
+
close();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
popup.addEventListener("keydown", (e) => {
|
|
437
|
+
if (e.key === "Escape") close();
|
|
438
|
+
if (e.key === "Enter") {
|
|
439
|
+
e.preventDefault();
|
|
440
|
+
saveBtn.click();
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
activeEditor = { cleanup: close };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const onKeyDown = (e) => {
|
|
448
|
+
if (e.key === "Escape" && !activeEditor) deactivateInteractMode();
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
doc.addEventListener("mouseover", onMouseOver, true);
|
|
452
|
+
doc.addEventListener("mouseout", onMouseOut, true);
|
|
453
|
+
doc.addEventListener("click", onClick, true);
|
|
454
|
+
doc.addEventListener("keydown", onKeyDown, true);
|
|
455
|
+
|
|
456
|
+
interactHandlers = {
|
|
457
|
+
doc,
|
|
458
|
+
onMouseOver,
|
|
459
|
+
onMouseOut,
|
|
460
|
+
onClick,
|
|
461
|
+
onKeyDown,
|
|
462
|
+
cleanup,
|
|
463
|
+
closeActiveEditor,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function detachInteractHandlers() {
|
|
468
|
+
if (!interactHandlers) return;
|
|
469
|
+
const { doc, onMouseOver, onMouseOut, onClick, onKeyDown, cleanup, closeActiveEditor } = interactHandlers;
|
|
470
|
+
try {
|
|
471
|
+
closeActiveEditor();
|
|
472
|
+
cleanup();
|
|
473
|
+
doc.documentElement.classList.remove("vibespot-interact-mode");
|
|
474
|
+
doc.removeEventListener("mouseover", onMouseOver, true);
|
|
475
|
+
doc.removeEventListener("mouseout", onMouseOut, true);
|
|
476
|
+
doc.removeEventListener("click", onClick, true);
|
|
477
|
+
doc.removeEventListener("keydown", onKeyDown, true);
|
|
478
|
+
} catch { /* cross-origin */ }
|
|
479
|
+
interactHandlers = null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function activateInteractMode() {
|
|
483
|
+
if (interactModeActive) return;
|
|
484
|
+
interactModeActive = true;
|
|
485
|
+
if (interactBtn) interactBtn.setAttribute("aria-pressed", "true");
|
|
486
|
+
const previewContainer = document.getElementById("preview-container");
|
|
487
|
+
if (previewContainer) previewContainer.classList.add("preview--interact-mode");
|
|
488
|
+
attachInteractHandlers();
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function deactivateInteractMode() {
|
|
492
|
+
if (!interactModeActive) return;
|
|
493
|
+
interactModeActive = false;
|
|
494
|
+
if (interactBtn) interactBtn.setAttribute("aria-pressed", "false");
|
|
495
|
+
const previewContainer = document.getElementById("preview-container");
|
|
496
|
+
if (previewContainer) previewContainer.classList.remove("preview--interact-mode");
|
|
497
|
+
detachInteractHandlers();
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function setInteractModeDisabled(disabled) {
|
|
501
|
+
if (!interactBtn) return;
|
|
502
|
+
if (disabled) {
|
|
503
|
+
deactivateInteractMode();
|
|
504
|
+
interactBtn.disabled = true;
|
|
505
|
+
} else {
|
|
506
|
+
interactBtn.disabled = false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (interactBtn) {
|
|
511
|
+
interactBtn.addEventListener("click", () => {
|
|
512
|
+
if (interactBtn.disabled) return;
|
|
513
|
+
if (interactModeActive) deactivateInteractMode();
|
|
514
|
+
else activateInteractMode();
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (typeof previewFrame !== "undefined" && previewFrame) {
|
|
519
|
+
previewFrame.addEventListener("load", () => {
|
|
520
|
+
if (interactModeActive) attachInteractHandlers();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Backward-compat aliases
|
|
525
|
+
window.setEditModeDisabled = setInteractModeDisabled;
|
|
526
|
+
window.deactivateEditMode = deactivateInteractMode;
|
|
527
|
+
window.setSelectModeDisabled = setInteractModeDisabled;
|
|
528
|
+
window.deactivateSelectMode = deactivateInteractMode;
|
|
529
|
+
|
|
530
|
+
// ---------------------------------------------------------------------------
|
|
531
|
+
// Save helpers — find matching field in fields.json and update via /api/field
|
|
532
|
+
// ---------------------------------------------------------------------------
|
|
533
|
+
|
|
534
|
+
async function findModuleFields(moduleName) {
|
|
535
|
+
try {
|
|
536
|
+
const res = await fetch("/api/modules");
|
|
537
|
+
const data = await res.json();
|
|
538
|
+
const mod = data.modules.find((m) => m.moduleName === moduleName);
|
|
539
|
+
if (!mod) return null;
|
|
540
|
+
return { fields: JSON.parse(mod.fieldsJson), moduleName };
|
|
541
|
+
} catch { return null; }
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function findFieldByText(fields, originalText, tag, prefix = "") {
|
|
545
|
+
for (const field of fields) {
|
|
546
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
547
|
+
|
|
548
|
+
if (field.type === "group" && field.children) {
|
|
549
|
+
const found = findFieldByText(field.children, originalText, tag, path);
|
|
550
|
+
if (found) return found;
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const val = typeof field.default === "string" ? field.default : "";
|
|
555
|
+
const stripped = val.replace(/<[^>]*>/g, "").trim();
|
|
556
|
+
if (stripped && originalText && stripped === originalText) {
|
|
557
|
+
return { path, field, isHtml: val !== stripped };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function findImageField(fields, imgSrc, prefix = "") {
|
|
564
|
+
for (const field of fields) {
|
|
565
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
566
|
+
if (field.type === "group" && field.children) {
|
|
567
|
+
const found = findImageField(field.children, imgSrc, path);
|
|
568
|
+
if (found) return found;
|
|
569
|
+
}
|
|
570
|
+
if (field.type === "image" && field.default?.src) {
|
|
571
|
+
return { path, field };
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function findLinkField(fields, href, prefix = "") {
|
|
578
|
+
for (const field of fields) {
|
|
579
|
+
const path = prefix ? `${prefix}.${field.name}` : field.name;
|
|
580
|
+
if (field.type === "group" && field.children) {
|
|
581
|
+
const found = findLinkField(field.children, href, path);
|
|
582
|
+
if (found) return found;
|
|
583
|
+
}
|
|
584
|
+
if (field.type === "link") return { path, field };
|
|
585
|
+
}
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function saveTextChange(moduleName, el, newText) {
|
|
590
|
+
const fieldPath = el.getAttribute("data-vs-field");
|
|
591
|
+
if (fieldPath) {
|
|
592
|
+
await fetch("/api/field", {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers: { "Content-Type": "application/json" },
|
|
595
|
+
body: JSON.stringify({ moduleName, fieldPath, value: newText }),
|
|
596
|
+
});
|
|
597
|
+
refreshPreview();
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const data = await findModuleFields(moduleName);
|
|
602
|
+
if (!data) { refreshPreview(); return; }
|
|
603
|
+
|
|
604
|
+
const originalText = el.getAttribute("data-original-text") || newText;
|
|
605
|
+
const match = findFieldByText(data.fields, originalText, el.tagName.toLowerCase());
|
|
606
|
+
|
|
607
|
+
if (match) {
|
|
608
|
+
await fetch("/api/field", {
|
|
609
|
+
method: "POST",
|
|
610
|
+
headers: { "Content-Type": "application/json" },
|
|
611
|
+
body: JSON.stringify({ moduleName, fieldPath: match.path, value: newText }),
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
refreshPreview();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function saveImageChange(moduleName, imgEl, newSrc) {
|
|
618
|
+
const fieldPath = imgEl.getAttribute("data-vs-field");
|
|
619
|
+
if (fieldPath) {
|
|
620
|
+
await fetch("/api/field", {
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: { "Content-Type": "application/json" },
|
|
623
|
+
body: JSON.stringify({
|
|
624
|
+
moduleName,
|
|
625
|
+
fieldPath,
|
|
626
|
+
value: { src: newSrc, alt: imgEl.getAttribute("alt") || "" },
|
|
627
|
+
}),
|
|
628
|
+
});
|
|
629
|
+
refreshPreview();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const data = await findModuleFields(moduleName);
|
|
634
|
+
if (!data) { refreshPreview(); return; }
|
|
635
|
+
|
|
636
|
+
const match = findImageField(data.fields, imgEl.src);
|
|
637
|
+
if (match) {
|
|
638
|
+
await fetch("/api/field", {
|
|
639
|
+
method: "POST",
|
|
640
|
+
headers: { "Content-Type": "application/json" },
|
|
641
|
+
body: JSON.stringify({
|
|
642
|
+
moduleName,
|
|
643
|
+
fieldPath: match.path,
|
|
644
|
+
value: { src: newSrc, alt: match.field.default?.alt || "" },
|
|
645
|
+
}),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
refreshPreview();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function saveLinkChange(moduleName, anchorEl, newText, newUrl) {
|
|
652
|
+
const linkField = anchorEl.getAttribute("data-vs-link");
|
|
653
|
+
const textField = anchorEl.getAttribute("data-vs-field");
|
|
654
|
+
|
|
655
|
+
if (linkField || textField) {
|
|
656
|
+
if (linkField && newUrl) {
|
|
657
|
+
await fetch("/api/field", {
|
|
658
|
+
method: "POST",
|
|
659
|
+
headers: { "Content-Type": "application/json" },
|
|
660
|
+
body: JSON.stringify({
|
|
661
|
+
moduleName,
|
|
662
|
+
fieldPath: linkField,
|
|
663
|
+
value: { url: { href: newUrl, type: "EXTERNAL" }, open_in_new_tab: false, no_follow: false },
|
|
664
|
+
}),
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
if (textField && newText) {
|
|
668
|
+
await fetch("/api/field", {
|
|
669
|
+
method: "POST",
|
|
670
|
+
headers: { "Content-Type": "application/json" },
|
|
671
|
+
body: JSON.stringify({ moduleName, fieldPath: textField, value: newText }),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
refreshPreview();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const data = await findModuleFields(moduleName);
|
|
679
|
+
if (!data) { refreshPreview(); return; }
|
|
680
|
+
|
|
681
|
+
const match = findLinkField(data.fields, anchorEl.href);
|
|
682
|
+
if (match) {
|
|
683
|
+
await fetch("/api/field", {
|
|
684
|
+
method: "POST",
|
|
685
|
+
headers: { "Content-Type": "application/json" },
|
|
686
|
+
body: JSON.stringify({
|
|
687
|
+
moduleName,
|
|
688
|
+
fieldPath: match.path,
|
|
689
|
+
value: {
|
|
690
|
+
url: { href: newUrl, type: "EXTERNAL" },
|
|
691
|
+
open_in_new_tab: match.field.default?.open_in_new_tab ?? false,
|
|
692
|
+
no_follow: match.field.default?.no_follow ?? false,
|
|
693
|
+
},
|
|
694
|
+
}),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (newText) {
|
|
699
|
+
const textMatch = findFieldByText(data.fields, anchorEl.textContent, "a");
|
|
700
|
+
if (textMatch) {
|
|
701
|
+
await fetch("/api/field", {
|
|
702
|
+
method: "POST",
|
|
703
|
+
headers: { "Content-Type": "application/json" },
|
|
704
|
+
body: JSON.stringify({ moduleName, fieldPath: textMatch.path, value: newText }),
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
refreshPreview();
|
|
710
|
+
}
|
package/ui/plan.js
CHANGED
|
Binary file
|